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 9 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"
25 changes: 24 additions & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ distribution-version = "2201.9.0"
[[package]]
org = "ballerina"
name = "crypto"
version = "2.7.0"
version = "2.7.1"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.array"},
{org = "ballerina", name = "test"},
Expand All @@ -21,6 +22,19 @@ modules = [
{org = "ballerina", packageName = "crypto", moduleName = "crypto"}
]

[[package]]
org = "ballerina"
name = "io"
version = "1.6.0"
scope = "testOnly"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.value"}
]
modules = [
{org = "ballerina", packageName = "io", moduleName = "io"}
]

[[package]]
org = "ballerina"
name = "jballerina.java"
Expand Down Expand Up @@ -67,6 +81,15 @@ name = "lang.object"
version = "0.0.0"
scope = "testOnly"

[[package]]
org = "ballerina"
name = "lang.value"
version = "0.0.0"
scope = "testOnly"
dependencies = [
{org = "ballerina", name = "jballerina.java"}
]

[[package]]
org = "ballerina"
name = "test"
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
38 changes: 38 additions & 0 deletions ballerina/encrypt_decrypt.bal
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,41 @@ 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[] publicKey = check io:fileReadBytes("public_key.asc"); // provide the path to the public key
# byte[] cipherText = check crypto:encryptPgp(message, publicKey);
# ```
#
# + plainText - The content to be encrypted
# + publicKey - 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, byte[] publicKey, *PgpEncryptionOptions options)
Copy link
Member

Choose a reason for hiding this comment

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

Let's take in the publicKey and privateKey as file paths for the moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same should applied for the params privateKey and passphrase in decryptPgp as well right? Should we keep the return type to byte [] as it is for those two methods encryptPgp and decryptPgp?

Copy link
Member

Choose a reason for hiding this comment

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

Let's have privateKey as a string file path in decryptPgp function as well. The passphrase should be a byte[] and what we return from the APIs also should be byte[] s.

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 addressed this in 78e3fb4

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[] publicKey = check io:fileReadBytes("public_key.asc"); // provide the path to the public key
# byte[] cipherText = check crypto:encryptPgp(message, publicKey);
#
# byte[] privateKey = check io:fileReadBytes("private_key.asc"); // provide the path to the private key
# byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); // provide the path to the passphrase
# byte[] decryptedMessage = check crypto:decryptPgp(cipherText, privateKey, passphrase);
# ```
#
# + cipherText - The encrypted content to be decrypted
# + privateKey - 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, byte[] privateKey, 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Represents the PGP encryption options
# 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 PgpEncryptionOptions record {|
Copy link
Member

Choose a reason for hiding this comment

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

Add API doc comments to all public types

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 just rename this to Options?

public isolated function encryptPgp(byte[] plainText, byte[] publicKey, *Options options)
                                       returns byte[]|Error = @java:Method {

IMO, the words pgp and encrypt are redundant as this API name itself provides an understanding that we are going to do a PGP encryption. So it is obvious that these options are related to PGP encryption.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we can reduce the redundancy by renaming it to Options. I have addressed it here 557f72b

CompressionAlgorithmTags compressionAlgorithm = ZIP;
SymmetricKeyAlgorithmTags symmetricKeyAlgorithm = AES_256;
boolean armor = true;
boolean withIntegrityCheck = true;
|};

# Represents the compressions algorithms available in PGP
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Represents the compressions algorithms available in PGP
# 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Represent the symmetric key algorithms available in PGP
# 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"
}
40 changes: 40 additions & 0 deletions ballerina/tests/encrypt_decrypt_pgp_test.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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;
import ballerina/io;

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

@test:Config {}
isolated function testEncryptAndDecryptWithPgpWithOptions() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = check io:fileReadBytes(PGP_PRIVATE_KEY_PASSPHRASE_PATH);
byte[] publicKey = check io:fileReadBytes(PGP_PUBLIC_KEY_PATH);
byte[] privateKey = check io:fileReadBytes(PGP_PRIVATE_KEY_PATH);
byte[] cipherText = check encryptPgp(message, publicKey, symmetricKeyAlgorithm = AES_128, armor = false);
byte[] plainText = check decryptPgp(cipherText, privateKey, 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

1 change: 1 addition & 0 deletions ballerina/tests/resources/pgp_private_key_passphrase.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
qCr3bv@5mj5n4eY
99 changes: 99 additions & 0 deletions ballerina/tests/resources/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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
4WbzDTObZg6ttIYG6+s=
=4NKk
-----END PGP PRIVATE KEY BLOCK-----
Loading
Loading