Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for PGP Encryption/Decryption #555

Merged
merged 13 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "crypto"
version = "2.7.0"
version = "2.7.1"
authors = ["Ballerina"]
keywords = ["security", "hash", "hmac", "sign", "encrypt", "decrypt", "private key", "public key"]
repository = "https://github.com/ballerina-platform/module-ballerina-crypto"
Expand All @@ -15,8 +15,8 @@ graalvmCompatible = true
[[platform.java17.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "crypto-native"
version = "2.7.0"
path = "../native/build/libs/crypto-native-2.7.0.jar"
version = "2.7.1"
path = "../native/build/libs/crypto-native-2.7.1-SNAPSHOT.jar"

[[platform.java17.dependency]]
groupId = "org.bouncycastle"
Expand All @@ -35,3 +35,9 @@ groupId = "org.bouncycastle"
artifactId = "bcutil-jdk18on"
version = "1.78"
path = "./lib/bcutil-jdk18on-1.78.jar"

[[platform.java17.dependency]]
groupId = "org.bouncycastle"
artifactId = "bcpg-jdk18on"
version = "1.78"
path = "./lib/bcpg-jdk18on-1.78.jar"
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ distribution-version = "2201.9.0"
[[package]]
org = "ballerina"
name = "crypto"
version = "2.7.0"
version = "2.7.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.array"},
Expand Down
3 changes: 3 additions & 0 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ dependencies {
externalJars(group: 'org.bouncycastle', name: 'bcutil-jdk18on', version: "${bouncycastleVersion}") {
transitive = false
}
externalJars(group: 'org.bouncycastle', name: 'bcpg-jdk18on', version: "${bouncycastleVersion}") {
transitive = false
}
}

task updateTomlFiles {
Expand Down
35 changes: 35 additions & 0 deletions ballerina/encrypt_decrypt.bal
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,38 @@ public isolated function decryptAesGcm(byte[] input, byte[] key, byte[] iv, AesP
name: "decryptAesGcm",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt"
} external;

# Returns the PGP-encrypted value for the given data.
# ```ballerina
# byte[] message = "Hello Ballerina!".toBytes();
# byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc");
# ```
#
# + plainText - The content to be encrypted
# + publicKeyPath - Path to the public key
# + options - PGP encryption options
# + return - Encrypted data or else a `crypto:Error` if the key is invalid
public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Options options)
returns byte[]|Error = @java:Method {
name: "encryptPgp",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt"
} external;

# Returns the PGP-decrypted value of the given PGP-encrypted data.
# ```ballerina
# byte[] message = "Hello Ballerina!".toBytes();
# byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc");
#
# byte[] passphrase = check io:fileReadBytes("pass_phrase.txt");
# byte[] decryptedMessage = check crypto:decryptPgp(cipherText, "private_key.asc", passphrase);
# ```
#
# + cipherText - The encrypted content to be decrypted
# + privateKeyPath - Path to the private key
# + passphrase - passphrase of the private key
# + return - Decrypted data or else a `crypto:Error` if the key or passphrase is invalid
public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, byte[] passphrase)
returns byte[]|Error = @java:Method {
name: "decryptPgp",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt"
} external;
74 changes: 74 additions & 0 deletions ballerina/pgp_utils.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

# Represents the PGP encryption options.
#
# + compressionAlgorithm - Specifies the compression algorithm used for PGP encryption
# + symmetricKeyAlgorithm - Specifies the symmetric key algorithm used for encryption
# + armor - Indicates whether ASCII armor is enabled for the encrypted output
# + withIntegrityCheck - Indicates whether integrity check is included in the encryption
public type Options record {|
CompressionAlgorithmTags compressionAlgorithm = ZIP;
SymmetricKeyAlgorithmTags symmetricKeyAlgorithm = AES_256;
boolean armor = true;
boolean withIntegrityCheck = true;
|};

# Represents the compression algorithms available in PGP.
#
# + UNCOMPRESSED - No compression
# + ZIP - Uses (RFC 1951) compression
# + ZLIB - Uses (RFC 1950) compression
# + BZIP2 - Uses Burrows–Wheeler algorithm
public enum CompressionAlgorithmTags {
UNCOMPRESSED = "0",
ZIP = "1",
ZLIB = "2",
BZIP2= "3"
}

# Represent the symmetric key algorithms available in PGP.
#
# + NULL - No encryption
# + IDEA - IDEA symmetric key algorithm
# + TRIPLE_DES - Triple DES symmetric key algorithm
# + CAST5 - CAST5 symmetric key algorithm
# + BLOWFISH - Blowfish symmetric key algorithm
# + SAFER - SAFER symmetric key algorithm
# + DES - DES symmetric key algorithm
# + AES_128 - AES 128-bit symmetric key algorithm
# + AES_192 - AES 192-bit symmetric key algorithm
# + AES_256 - AES 256-bit symmetric key algorithm
# + TWOFISH - Twofish symmetric key algorithm
# + CAMELLIA_128 - Camellia 128-bit symmetric key algorithm
# + CAMELLIA_192 - Camellia 192-bit symmetric key algorithm
# + CAMMELIA_256 - Camellia 256-bit symmetric key algorithm
public enum SymmetricKeyAlgorithmTags {
NULL = "0",
IDEA = "1",
TRIPLE_DES = "2",
CAST5 = "3",
BLOWFISH = "4",
SAFER = "5",
DES = "6",
AES_128 = "7",
AES_192 = "8",
AES_256 = "9",
TWOFISH = "10",
CAMELLIA_128 = "11",
CAMELLIA_192 = "12",
CAMELLIA_256 = "13"
}
62 changes: 62 additions & 0 deletions ballerina/tests/encrypt_decrypt_pgp_test.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/test;

@test:Config {}
isolated function testEncryptAndDecryptWithPgp() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH);
byte[] plainText = check decryptPgp(cipherText, PGP_PRIVATE_KEY_PATH, passphrase);
test:assertEquals(plainText.toBase16(), message.toBase16());
}

@test:Config {}
isolated function testEncryptAndDecryptWithPgpWithOptions() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false);
byte[] plainText = check decryptPgp(cipherText, PGP_PRIVATE_KEY_PATH, passphrase);
test:assertEquals(plainText.toBase16(), message.toBase16());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we also add a negative test case by creating another PGP key pair, like encrypting with a public key and trying to decrypt using a wrong private key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added two more test cases for following in here 78e3fb4

  • Invalid Private key for decryption with it's passphrase
  • Valid Private key for decryption with invalid passphrase


@test:Config {}
isolated function testNegativeEncryptAndDecryptWithPgpInvalidPrivateKey() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH);
byte[]|Error plainText = decryptPgp(cipherText, PGP_INVALID_PRIVATE_KEY_PATH, passphrase);
if plainText is Error {
test:assertEquals(plainText.message(), "Error occurred while PGP decrypt: Could Not Extract private key");
} else {
test:assertFail("Should return a crypto Error");
}
}

@test:Config {}
isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH);
byte[]|Error plainText = decryptPgp(cipherText, PGP_PRIVATE_KEY_PATH, passphrase);
if plainText is Error {
test:assertEquals(plainText.message(),
"Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes");
} else {
test:assertFail("Should return a crypto Error");
}
}
99 changes: 99 additions & 0 deletions ballerina/tests/resources/invalid_private_key.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: Keybase OpenPGP v2.0.76
Comment: https://keybase.io/crypto

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
yRE=
=QlIR
-----END PGP PRIVATE KEY BLOCK-----
Loading
Loading