From 1897673e8dd49e9ba369b2ab0c937bec346ea328 Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Mon, 6 May 2024 10:19:01 +0530 Subject: [PATCH 01/12] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index c534328..18c9ac2 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240502-141200-0a49fa42" [[package]] org = "ballerina" From 07115d4837f6700fd275e377fb3e4b51988ec7e4 Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Wed, 8 May 2024 10:12:00 +0530 Subject: [PATCH 02/12] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index cf7100d..cd0823c 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -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" From fd569305408c3f680109e1a7c0bba03a119eadbc Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Wed, 8 May 2024 10:36:04 +0530 Subject: [PATCH 03/12] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 18c9ac2..9342c29 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -12,6 +12,7 @@ org = "ballerina" name = "crypto" version = "2.7.0" dependencies = [ + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "test"}, @@ -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" @@ -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" From 9110389e0bb5281b52057bdaf45ca81fcb231d9d Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Thu, 9 May 2024 18:07:38 +0530 Subject: [PATCH 04/12] Add pgp encryption and decryption --- ballerina/build.gradle | 3 + ballerina/encrypt_decrypt.bal | 38 +++++ ballerina/pgp_utils.bal | 46 ++++++ ballerina/tests/encrypt_decrypt_pgp_test.bal | 40 +++++ .../resources/pgp_private_key_passphrase.txt | 1 + ballerina/tests/resources/private_key.asc | 99 ++++++++++++ ballerina/tests/resources/public_key.asc | 55 +++++++ ballerina/tests/test_utils.bal | 4 + build-config/resources/Ballerina.toml | 6 + native/build.gradle | 1 + .../ballerina/stdlib/crypto/CryptoUtils.java | 1 + .../stdlib/crypto/PgpDecryptionGenerator.java | 145 ++++++++++++++++++ .../stdlib/crypto/PgpEncryptionGenerator.java | 145 ++++++++++++++++++ .../stdlib/crypto/nativeimpl/Decrypt.java | 22 +++ .../stdlib/crypto/nativeimpl/Encrypt.java | 34 ++++ native/src/main/java/module-info.java | 1 + 16 files changed, 641 insertions(+) create mode 100644 ballerina/pgp_utils.bal create mode 100644 ballerina/tests/encrypt_decrypt_pgp_test.bal create mode 100644 ballerina/tests/resources/pgp_private_key_passphrase.txt create mode 100644 ballerina/tests/resources/private_key.asc create mode 100644 ballerina/tests/resources/public_key.asc create mode 100644 native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java create mode 100644 native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 48a206d..783835b 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -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 { diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index ae4b198..895f694 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -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); +# ``` +# +# + input - The content to be encrypted +# + key - Public key +# + options - PGP encryption options +# + return - Encrypted data or else a `crypto:Error` if the key is invalid +public isolated function encryptPgp(byte[] input, byte[] key, *PgpEncryptionOptions options) + returns byte[]|Error = @java:Method { + name: "encryptPgp", + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" +} external; + +# Returns the PGP-decrypted value for 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); +# ``` +# +# + input - The encrypted content to be decrypted +# + key - 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[] input, byte[] key, byte[] passphrase) + returns byte[]|Error = @java:Method { + name: "decryptPgp", + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" +} external; diff --git a/ballerina/pgp_utils.bal b/ballerina/pgp_utils.bal new file mode 100644 index 0000000..b787806 --- /dev/null +++ b/ballerina/pgp_utils.bal @@ -0,0 +1,46 @@ +// 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. + +public type PgpEncryptionOptions record {| + CompressionAlgorithmTags compressionAlgorithm = ZIP; + SymmetricKeyAlgorithmTags symmetricKeyAlgorithm = AES_256; + boolean armor = true; + boolean withIntegrityCheck = true; +|}; + +public enum CompressionAlgorithmTags { + UNCOMPRESSED = "0", + ZIP = "1", + ZLIB = "2", + BZIP2= "3" +} + +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" +} \ No newline at end of file diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal new file mode 100644 index 0000000..3644fb0 --- /dev/null +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -0,0 +1,40 @@ +// Copyright (c) 2020 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. 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()); +} diff --git a/ballerina/tests/resources/pgp_private_key_passphrase.txt b/ballerina/tests/resources/pgp_private_key_passphrase.txt new file mode 100644 index 0000000..9b60eb8 --- /dev/null +++ b/ballerina/tests/resources/pgp_private_key_passphrase.txt @@ -0,0 +1 @@ +qCr3bv@5mj5n4eY \ No newline at end of file diff --git a/ballerina/tests/resources/private_key.asc b/ballerina/tests/resources/private_key.asc new file mode 100644 index 0000000..fa91919 --- /dev/null +++ b/ballerina/tests/resources/private_key.asc @@ -0,0 +1,99 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: Keybase OpenPGP v2.0.76 +Comment: https://keybase.io/crypto + +xcMFBGY7OqUBCADQO4C2dbXISbB9EZKX8hEysDKk89o+5C2SvCUBChBOwV036dDQ +R+4gCf2UE+/uER702r2QzDtpeCr/7uraNFf7x05Teng0ACk5aBTMunYF4KnmuicP +G/qQW0vW04Tv7l/qAGjGAaf8wZkFlQRIb0zMASyxLnxYSuRn09CeNLy+3fNBQxc0 +cYUByZKb3IG0tMBUkr63RvCWwRGesQD3aqKrsvmKkTVWnosHt1Il6zFMGrY0M6Pr +S/lps9T677nu3C9ySiviem/sC4UORllfCtBO22eWKlC9KicbhR084vmrtT/s2cPd +vBIOxdm1Dczi2VrcIOsWOZiXfoIwHYsGL/+xABEBAAH+CQMIsIoEwtjUyBJge7cP +Mg/okGCw9kSjrhtoJKwMSRpXqQUkp5A8r96NlmlU5YW2bJIwX8ZxhRjcxpUXGDKS +I5OvRe3lNLoacXrtlmxud+dcmoB+ax++x7Q/dz+1ApEqGET9BBmf+7/gkiPt/MCK +xwaoQvU5D0KG1+Gxt0V5qBrpTlOtaPg6oc0AQERcU6lSkax5tbwK2po/KunJkLXk +sMwNjaBjct/PuqQNwzxC5LccB5aYZzzX7SsXC7H+ynMXGsabGMd80QQ461OlBBG0 ++FgR2H+4H/xIw0j3awvB3IT8VFv/eDtE5h2ZyxY36jKm1n0UHRjNfL6QF3LIcoc7 +WUM3aOTmLwEkJnvIDr70hiYfSCWH+MJd+QmA0sts+wpuqcFPcGxBBzIqrIbeQ4we +lYU0BRPFa4TORBJmgTSGYh/0qgQYQT8U1ljllI2vEDtcRKa92BGugwWs/BS1jd4t +wpuwPLybAdXum2e7jIzH9hpl/OQkmCi1MfIcyV8miRaRrDz8ELIz7TT78ykOz9m6 +r417CAr/eL0JkVMJ4ZwQ6yltYrKBjGtFoGYv5a2i4Gj5LgM/51+bjcnKFmOI5W6N +VWbuC7bZrB1HhmR90ddhhf5H8gUJbTRl+Keq4LWBz6PeewCRzcwQMb+KAwYlO48N +t+j3MDmUhBZxwFGUO75wb2UJI0AyMFm8bZ5S7tGnttCppgPMVKgdMN0fLpm9Fw/z +JOBKTSIGrsY2clW05Dlhds5LuYqeJCypRc7YO7IxEaimcrq7j20e6RZh0evXJCmj +a1DZqdLjzyD0M49V6jd7C9pfjs7kFN7wr6aDzWEh0mRDjXdJL/6kIlAiJH/Dg67B +YEm8NPyHzjccyK3a/IzcJase3m6lS1wX7iniCeJJIOegXvOxA6q+drywvVlC12Bk +1hs/rOg5TTjNJ0RpbnVrYSA8ZGludWthYW1hcmFzaW5naGU4MTdAZ21haWwuY29t +PsLAegQTAQoAJAUCZjs6pQIbLwMLCQcDFQoIAh4BAheAAxYCAQIZAQUJAeEzgAAK +CRCJFrwU1IvKDOStB/oDjBO8/1cxFaI+e7PDuzm6bzMVXVMqe6qAjZ8SMy4KNT8K +bDccNFJcMS7iGTVXf/kxppeFJDq3RclVeuEWFRG4bSDbjKyQtXkJvDAMLaA8obWs +ZUz/FI6yQHEk8JaQlgVlDyA87YA76G8u1iKCp59L5sPGLCwbnmjZM3bkFzDp96V4 +mAh//3JJKZMD1xDTB2QVK+QZZfX3TIgVeaLW0lpYbRYo5OD7x1/PGoP5l6oNXAC6 +bAxEAZXwFaY1YvUkUGJ5McvyPU0O9j33LKrvnauRHoIQ/MgFw+XmDqRUoXTCysbW +UFNvKv3/Y/62CV1/S/+ttN44GZ6dznV78BwlsKGOx8MGBGY7OqUBCADV0WNXdHNC +7sfi0HgKefAVWPdQYVFfl+yePb5Orjr/IIOAJSdi2mkdTnzNFVMdFes7jicR2Sr/ +56NxiaZSFImBto8iUdFFskIIx9gj/52MEixIABBwEG9YHNL2EqSn+Htcfa90/fon +t2waHLlB4sZdeUrIZnXlOldXn7zC9bWPrPftd/OgH07I+cqy4kAR9XIchGHTotR4 +U5ZPNSqaeuj5DirQy0y9G6jAPVP6JDYuXr7Qy+3NBwvTpDrIZtm87ZueUKP27EnA +/WxKwtyEkwBAa7hGCZDSnXSdLvjIgwGUNhYhxprspc5FWxVZGwJ9Va8URFPKFbSM +p42iTzbJWewHABEBAAH+CQMIn13mI36i1XdgWZhXZKMa0qEBhcvGlVnhh4XxtDKc +ez0QvlAcofzfrKUZ9vH66F3PrSyXMX6pLm6HtMPG7c5TPpmr4fjBW3Qd5C3tyKOR +pZmZhJpiALB9KqVu8UdisfXuGoU1gzzFfxqncDlJ+Wphi6N8ASALxLMmw3r45aSJ +g0WDtsigPFz7TuSZc56KA4kVo8Arnw7lGXNAVYMU3ZsilYqzD1VIbZ0l4UOAbICZ +2Xj8+rsvWVuBbuRK+DyZWRbtXnQRVNEeGgW9HY9DHjTTS5/XA9rg8SJ0fUEC5u2+ +eZSG2fyNWbydkVTYpkzLAKhpB7Mx1y4cHNpxnAX1C7uw7JHy2vAKywQiMS7vx2/E +rfYoWcDB/Idmb6RONOTScNA/IeUS7Qi/w5l5TQu45SBynpp9L5vpg+9kV44MV4nM +vvYqKE0hcz++eF5Z0rcxv4QBA0V6GAkS+ky9Ads0wlAUrlebWpNWGK8XOoAV8ORG +jLAHJEXLaTucyYEkpAWCHjcbcjRFEht0Lx9SBGP/W8TEAJzAe9tzyuSAaoNzpUHL +MnIa3DX7WljdpMglQej66t5o75JfWXdqmd8OAFKv6t+uo9yDw6R3WBCi8V6Atesj +iLKo694N4QwmNBYko5dF8mWcZDxC+++4TR/wDTWKC3vVkVLwjKGU+SAr+LRBmrLR +19szDNmpwG87IfaUZmXFaq3WBNP1W7GDkbic+5ca8bn6MZA1nlrl4OYZqh4JYQRR +6LMQlPAeiGJqUU+KVFqNEuGvbwIsR49O3z5QW/8PhblxT9XKa0bKch4FTocPKKi0 +PGkmLHEn8QG5E2+LeRADLmgBg/Cp62BAylc4FjCHYHdt2qRdRhp4qLlBZbTU+7Eq +eedUCdfAiibXc3Mq6gxmgCWpFmkxAqg3kP3VyQ9D+yruYghqnSbawsGEBBgBCgAP +BQJmOzqlBQkB4TOAAhsuASkJEIkWvBTUi8oMwF0gBBkBCgAGBQJmOzqlAAoJEKCs +yaK2PFVG5owH/0WYFTMj5lzF3fzNaMIHivZU3eUWtxUuba7SgVCtYULawUHJyBIX +P8trCMyt6/ZOd/brcUaD4wIFidBNCqtQg6FsxL7+WGZPFWGc8MJBTRaoSDmS6sAV +cSzPIsX3G8lyh5o6Ut7jk+Z7bqvI9sk5dVGembMWKYMCkESVh2bVQ2J3rbQ4v6Be +j17XOrAekJbHxkktExUy4zCRI1M7KNPJUeWMwSrfOtgdEWW/PduEGxRu/kLfa9z/ +lentgZw+/DLYs7dhi0tQdhNqY9ayJF19dk+cLs6uxwi1cgFd1faBb6OqNTr5ymHH +lquIra+p6pcAqiRKC5x9Hy5sxSQKUglTjH9deAf8DBcPbAPbYLhYj/PmAWXl2fVl +85iyYQEoI+v3pRi3NFNp/onThDjp8r8EAHvQjnl2Ctvc9OfR2neNIh43b7Pw4oRq +xD9y/9ezgyFLaPLiDkd8aBSrSEz/OGKazVF7aGuevKYJ4QVqQFXFNEilhCIM/PVe +FVmEhYReSi0WDyvhA1o9W5V4I2iJ/pHlVnAV756s2Ge5VfYu6VYvQmkK/MA2Dyny +6lrwRYpViftPbeovzT3suQV3mH/W8xhcCpTuDUMZlEa2QzUe5NYfOTA8Aro37eKM +w2/VhBUTc5tcCx7qa3rfOu/quam7/uOFxPULN5Vk/IAxlUqW34DhAmySG58h68fD +BgRmOzqlAQgA1NwmWwNXiVdGQqM/Yy4YRyexeFpcgBZQvsMUTKccuEsa/WSNq+3A +MM1el3rZK1kfDw54/iwcms9/Vfqt3KRAsmjzNOfbyYH1JtesqVz5BCig8bE6O2lY +N5CqWpMM8D69sXnZSMjyFtAyX5Ss2lniJUwF98rwd35tG7bIQs9j7+t2k9Sts9AF +sUYbBh6sSWGNd6HVUYhwHZ6mINGRGIV1k7AZVwap8A1o+wQY5Xfug/iNrzoLGOES +hLW2jpzdLbtMoOb/jys9coR153RIs+WY9UO2YugkHiNPs/jhpsHQVixlkanXMW0N +JPy/wiIh9v9KDE9shYa8K3+eWQW4Bc+DZwARAQAB/gkDCAPDpy0ne0J2YHDJg4Bf +J0+cVrySNTL3KuhGYYrR3RZXT6m7jkS46RzRw70nuhxkLWYeDYM0cJW1dUA7wWaj +jnBMXJiSwGjVL4thJ9SGY9zECoW5saih7y20LrQDfiK/RdmW3tVyMpBjHyDIsQaU +L8YYUueeaPKtwUCjmay3Q/ox4bl6zk+mAJ5g/3gjIN5ZEiT7aP6q1/dGLiWDr8qC +OqQ/XjkR8hXqtvHAt7Lz61qXhoTvu++K+ejq/eFDBeR1Zp5p9iS72uRPFElvnZ0R +bdpJOaagoj6pknoRTAaKTcbEynvi7MLVnwfCI76LlQ9gwzSBVHa1frR6qRoNT6m7 +MrRW/xAEk9niw/VDRD5jjUOp7SYO5chdF0cvI7cWtrKvHvFz1hqdqG+oMxzJsWzR +PJbCFlGmf68DOTmvvVU9vL3LuEjT3tvvHlJ4n2+to1Y8euXvHrY8qePtsogeQrP9 +lioKpUdOOoAuw+xLMyVhYfQEhqj+MOIenV+ac7GSVTovhwJlKNcAbN2I33HwTD9X +SxMpwtQGX85bZswCFVvIJuDeF/ce8HWoG+qsABRyo82sg1FvbB/WAfE1qs8SDMfe +vGICVP6I13BedgR5nKjr+hhCPBMD/zV4nMhuFTZytOGAjmKQN6pxsFgEsfAw645H +QwRxSKi2PFcPQlDVuCTb/BF+I6tGMwrKlCfEt75hd+UmVcq3wJccV+AyxtdG0q8t +iDd2Du7r+6ynZBlKVX75QGDNzDglH8Q44CDfVYKMkdNRa8YYxTh9yN0KHIvINEyd +HJVwL5VLgXz6VtohM0KZyL9+UQW0xbX/xSuTPtbVjSuQX0W6eyYhen/n4r45fH+p +YKRzaFF2MdpPtzXs4UfB6NLPpLdYEU/F7/k52r/wLRNzime4Ic0ER8WpVaASqC2y +A3BZ4FPiFMLBhAQYAQoADwUCZjs6pQUJAeEzgAIbLgEpCRCJFrwU1IvKDMBdIAQZ +AQoABgUCZjs6pQAKCRAuhLOLdi7N4H7BCACjhI3o6ixJgw5dVtCB244eZZh3OgWX +HZWVgf+WmnI39Yq/LGnzNQoga7SFw8+GWeg6SFrHIrUShYXfoOWlLkOZ9eyUK/+d +Om/iKlIoA1LU1+Ba0AbXN45xotBrdP2sNC1DKy3NymcKF1HoHZ5PQ9E3Ixp3TnsS +gy3MEb1z2UyAH5fF7AfDRhPDfhJtl5sJ04EnM9yPOruREmTlcxeJvyJ5lnlDRGK8 +jN/C3R/zSpAjSpA/vY4EeUOQuMni5PG5SxfFZupQSus3r0qZx5BJdnyY276yDKSi +s/CP5qP5zE8YmJjLMdkwAfJuFi5EkigjOnOgGYymivYiURbpCxmn83rXBjoH/RPw +0Q5Wj0+5+Obc7yw8uUSor1m11kyI8/sv7G8dSDQLMpE5V7VKaj4OQ1fHMzpBPXP4 +H8WqQx0XPUEScOJ8zDIS2p1VRMnMr2EerEw/QV/A3B5AwMDZkhd3tK0liP95xZ9v +y+pWPGAFIQrDYcmRMRBL2WB+jxm49VOgcesKl1OP3NO2DpHu/p4RS6N6+2w6H/De +x39rIv+7B6mdK54nHdomM0AdDc8qkks42Qr3kXbpLtnefZirWPs2QcNr9kXPtpfp +IyMpGoeudZWlaBMLcGlkEEO0MJKyTe2iKhIl+UAQwBoJEMbPc8L3m6oNtQotRzJv +4WbzDTObZg6ttIYG6+s= +=4NKk +-----END PGP PRIVATE KEY BLOCK----- diff --git a/ballerina/tests/resources/public_key.asc b/ballerina/tests/resources/public_key.asc new file mode 100644 index 0000000..6c88e61 --- /dev/null +++ b/ballerina/tests/resources/public_key.asc @@ -0,0 +1,55 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: Keybase OpenPGP v2.0.76 +Comment: https://keybase.io/crypto + +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 +=DRCu +-----END PGP PUBLIC KEY BLOCK----- diff --git a/ballerina/tests/test_utils.bal b/ballerina/tests/test_utils.bal index 4aeb36f..59a1e2b 100644 --- a/ballerina/tests/test_utils.bal +++ b/ballerina/tests/test_utils.bal @@ -33,3 +33,7 @@ const string MLKEM_PRIVATE_KEY_PATH = "tests/resources/mlkem-key.pem"; const string INVALID_KEYSTORE_PATH = "tests/resources/cert/keyStore.p12.invalid"; const string INVALID_PRIVATE_KEY_PATH = "tests/resources/cert/private.key.invalid"; const string INVALID_PUBLIC_CERT_PATH = "tests/resources/cert/public.crt.invalid"; + +const string PGP_PUBLIC_KEY_PATH = "tests/resources/public_key.asc"; +const string PGP_PRIVATE_KEY_PATH = "tests/resources/private_key.asc"; +const string PGP_PRIVATE_KEY_PASSPHRASE_PATH = "tests/resources/pgp_private_key_passphrase.txt"; diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 2dadb32..2f59bda 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -35,3 +35,9 @@ groupId = "org.bouncycastle" artifactId = "bcutil-jdk18on" version = "@bouncycastle.version@" path = "./lib/bcutil-jdk18on-@bouncycastle.version@.jar" + +[[platform.java17.dependency]] +groupId = "org.bouncycastle" +artifactId = "bcpg-jdk18on" +version = "@bouncycastle.version@" +path = "./lib/bcpg-jdk18on-@bouncycastle.version@.jar" diff --git a/native/build.gradle b/native/build.gradle index 5fbf4ee..ed24295 100644 --- a/native/build.gradle +++ b/native/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation group: 'org.bouncycastle', name: 'bcpkix-jdk18on', version: "${bouncycastleVersion}" implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: "${bouncycastleVersion}" implementation group: 'org.bouncycastle', name: 'bcutil-jdk18on', version: "${bouncycastleVersion}" + implementation group: 'org.bouncycastle', name: 'bcpg-jdk18on', version: "${bouncycastleVersion}" compileOnly group: 'org.graalvm.nativeimage', name: 'svm', version: "${nativeImageVersion}" } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java index f34e024..d0376c3 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java @@ -448,4 +448,5 @@ private static Digest selectHash(String algorithm) { } throw CryptoUtils.createError("Unsupported algorithm: " + algorithm); } + } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java new file mode 100644 index 0000000..28590d8 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -0,0 +1,145 @@ +/* + * 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. + */ +package io.ballerina.stdlib.crypto; + +import io.ballerina.runtime.api.creators.ValueCreator; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.util.Iterator; +import java.util.Objects; + +public class PgpDecryptionGenerator { + + static { + if (Objects.isNull(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME))) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + private final char[] passCode; + private final PGPSecretKeyRingCollection pgpSecretKeyRingCollection; + + public PgpDecryptionGenerator(InputStream privateKeyIn, byte[] passCode) throws IOException, PGPException { + this.passCode = new String(passCode, StandardCharsets.UTF_8).toCharArray(); + this.pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(privateKeyIn) + , new JcaKeyFingerprintCalculator()); + } + + private PGPPrivateKey findSecretKey(long keyID) throws PGPException { + PGPSecretKey pgpSecretKey = pgpSecretKeyRingCollection.getSecretKey(keyID); + return pgpSecretKey == null ? null : pgpSecretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passCode)); + } + + public void decryptStream(InputStream encryptedIn, OutputStream clearOut) + throws PGPException, IOException { + // Remove armour and return the underlying binary encrypted stream + encryptedIn = PGPUtil.getDecoderStream(encryptedIn); + JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(encryptedIn); + + Object obj = pgpObjectFactory.nextObject(); + // Verify the marker packet + PGPEncryptedDataList pgpEncryptedDataList = (obj instanceof PGPEncryptedDataList) + ? (PGPEncryptedDataList) obj : (PGPEncryptedDataList) pgpObjectFactory.nextObject(); + + PGPPrivateKey pgpPrivateKey = null; + PGPPublicKeyEncryptedData publicKeyEncryptedData = null; + + Iterator encryptedDataItr = pgpEncryptedDataList.getEncryptedDataObjects(); + while (pgpPrivateKey == null && encryptedDataItr.hasNext()) { + publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedDataItr.next(); + pgpPrivateKey = findSecretKey(publicKeyEncryptedData.getKeyID()); + } + + if (Objects.isNull(publicKeyEncryptedData)) { + throw new PGPException("Could not generate PGPPublicKeyEncryptedData object"); + } + + if (pgpPrivateKey == null) { + throw new PGPException("Could Not Extract private key"); + } + decrypt(clearOut, pgpPrivateKey, publicKeyEncryptedData); + } + + public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { + ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); + ByteArrayOutputStream clearOut = new ByteArrayOutputStream(); + decryptStream(encryptedIn, clearOut); + return ValueCreator.createArrayValue(clearOut.toByteArray()); + } + + static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, + PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { + PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(pgpPrivateKey); + InputStream decryptedCompressedIn = publicKeyEncryptedData.getDataStream(decryptorFactory); + + JcaPGPObjectFactory decCompObjFac = new JcaPGPObjectFactory(decryptedCompressedIn); + PGPCompressedData pgpCompressedData = (PGPCompressedData) decCompObjFac.nextObject(); + + InputStream compressedDataStream = new BufferedInputStream(pgpCompressedData.getDataStream()); + JcaPGPObjectFactory pgpCompObjFac = new JcaPGPObjectFactory(compressedDataStream); + + Object message = pgpCompObjFac.nextObject(); + + if (message instanceof PGPLiteralData pgpLiteralData) { + InputStream decDataStream = pgpLiteralData.getInputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = decDataStream.read(buffer)) != -1) { + clearOut.write(buffer, 0, bytesRead); + } + clearOut.close(); + } else if (message instanceof PGPOnePassSignatureList) { + throw new PGPException("Encrypted message contains a signed message not literal data"); + } else { + throw new PGPException("Message is not a simple encrypted file - Type Unknown"); + } + // Perform the integrity check + if (publicKeyEncryptedData.isIntegrityProtected()) { + if (!publicKeyEncryptedData.verify()) { + throw new PGPException("Message failed integrity check"); + } + } + } + +} diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java new file mode 100644 index 0000000..110430b --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -0,0 +1,145 @@ +/* + * 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. + */ +package io.ballerina.stdlib.crypto; + +import io.ballerina.runtime.api.creators.ValueCreator; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.security.Security; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; + +public class PgpEncryptionGenerator { + + static { + if (Objects.isNull(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME))) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + private final int compressionAlgorithm; + private final int symmetricKeyAlgorithm; + private final boolean armor; + private final boolean withIntegrityCheck; + private static final int BUFFER_SIZE = 8192; + + public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorithm, boolean armor, + boolean withIntegrityCheck) { + this.compressionAlgorithm = compressionAlgorithm; + this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + this.armor = armor; + this.withIntegrityCheck = withIntegrityCheck; + } + + public void encryptStream(OutputStream encryptOut, InputStream clearIn, long length, InputStream publicKeyIn) + throws IOException, PGPException { + PGPCompressedDataGenerator compressedDataGenerator = + new PGPCompressedDataGenerator(compressionAlgorithm); + PGPEncryptedDataGenerator pgpEncryptedDataGenerator = new PGPEncryptedDataGenerator( + // Configure the encrypted data generator + new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) + .setWithIntegrityPacket(withIntegrityCheck) + .setSecureRandom(new SecureRandom()) + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + ); + // Add public key + pgpEncryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator( + getPublicKey(publicKeyIn))); + if (armor) { + encryptOut = new ArmoredOutputStream(encryptOut); + } + OutputStream cipherOutStream = pgpEncryptedDataGenerator.open(encryptOut, new byte[BUFFER_SIZE]); + copyAsLiteralData(compressedDataGenerator.open(cipherOutStream), clearIn, length); + compressedDataGenerator.close(); + cipherOutStream.close(); + encryptOut.close(); + } + + public Object encrypt(byte[] clearData, InputStream publicKeyIn) throws PGPException, IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(clearData); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + encryptStream(outputStream, inputStream, clearData.length, publicKeyIn); + return ValueCreator.createArrayValue(outputStream.toByteArray()); + } + + static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, PGPException { + PGPPublicKeyRingCollection pgpPublicKeyRings = new PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(keyInputStream), new JcaKeyFingerprintCalculator()); + Iterator keyRingIterator = pgpPublicKeyRings.getKeyRings(); + while (keyRingIterator.hasNext()) { + PGPPublicKeyRing pgpPublicKeyRing = keyRingIterator.next(); + Optional pgpPublicKey = extractPGPKeyFromRing(pgpPublicKeyRing); + if (pgpPublicKey.isPresent()) { + return pgpPublicKey.get(); + } + } + throw new PGPException("Invalid public key"); + } + + static void copyAsLiteralData(OutputStream outputStream, InputStream in, long length) + throws IOException { + PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); + OutputStream pOut = lData.open(outputStream, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, + Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)), new byte[PgpEncryptionGenerator.BUFFER_SIZE]); + byte[] buff = new byte[PgpEncryptionGenerator.BUFFER_SIZE]; + try (in) { + int len; + long totalBytesWritten = 0L; + while (totalBytesWritten <= length && (len = in.read(buff)) > 0) { + pOut.write(buff, 0, len); + totalBytesWritten += len; + } + pOut.close(); + } finally { + Arrays.fill(buff, (byte) 0); + } + } + + private static Optional extractPGPKeyFromRing(PGPPublicKeyRing pgpPublicKeyRing) { + for (PGPPublicKey publicKey : pgpPublicKeyRing) { + if (publicKey.isEncryptionKey()) { + return Optional.of(publicKey); + } + } + return Optional.empty(); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java index 2632ede..521c671 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java @@ -22,7 +22,12 @@ import io.ballerina.runtime.api.values.BMap; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; +import io.ballerina.stdlib.crypto.PgpDecryptionGenerator; +import org.bouncycastle.openpgp.PGPException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; @@ -74,4 +79,21 @@ public static Object decryptRsaEcb(BArray inputValue, Object keys, Object paddin return CryptoUtils.rsaEncryptDecrypt(CryptoUtils.CipherMode.DECRYPT, Constants.ECB, padding.toString(), key, input, null, -1); } + + public static Object decryptPgp(BArray inputValue, BArray keyValue, BArray passphrase) { + byte[] input = inputValue.getBytes(); + byte[] key = keyValue.getBytes(); + byte[] passphraseInBytes = passphrase.getBytes(); + InputStream keyStream = new ByteArrayInputStream(key); + + try { + PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator( + keyStream, + passphraseInBytes + ); + return pgpDecryptionGenerator.decrypt(input); + } catch (IOException | PGPException e) { + return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); + } + } } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java index c36e6c0..ff3b2aa 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java @@ -18,11 +18,21 @@ package io.ballerina.stdlib.crypto.nativeimpl; +import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BValue; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; +import io.ballerina.stdlib.crypto.PgpEncryptionGenerator; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; @@ -34,6 +44,11 @@ */ public class Encrypt { + public static final String COMPRESSION_ALGORITHM = "compressionAlgorithm"; + public static final String SYMMETRIC_KEY_ALGORITHM = "symmetricKeyAlgorithm"; + public static final String ARMOR = "armor"; + public static final String WITH_INTEGRITY_CHECK = "withIntegrityCheck"; + private Encrypt() {} public static Object encryptAesCbc(BArray inputValue, BArray keyValue, BArray ivValue, Object padding) { @@ -80,4 +95,23 @@ public static Object encryptRsaEcb(BArray inputValue, Object keys, Object paddin return CryptoUtils.rsaEncryptDecrypt(CryptoUtils.CipherMode.ENCRYPT, Constants.ECB, padding.toString(), key, input, null, -1); } + + public static Object encryptPgp(BArray inputValue, BArray keyValue, BMap options) { + byte[] input = inputValue.getBytes(); + byte[] key = keyValue.getBytes(); + InputStream keyStream = new ByteArrayInputStream(key); + + PgpEncryptionGenerator pgpEncryptionGenerator = new PgpEncryptionGenerator( + Integer.parseInt(options.get(StringUtils.fromString(COMPRESSION_ALGORITHM)).toString()), + Integer.parseInt(options.get(StringUtils.fromString(SYMMETRIC_KEY_ALGORITHM)).toString()), + Boolean.parseBoolean(options.get(StringUtils.fromString(ARMOR)).toString()), + Boolean.parseBoolean(options.get(StringUtils.fromString(WITH_INTEGRITY_CHECK)).toString()) + ); + + try { + return pgpEncryptionGenerator.encrypt(input, keyStream); + } catch (PGPException | IOException e) { + return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); + } + } } diff --git a/native/src/main/java/module-info.java b/native/src/main/java/module-info.java index d539fcf..6fca0bb 100644 --- a/native/src/main/java/module-info.java +++ b/native/src/main/java/module-info.java @@ -22,6 +22,7 @@ requires org.bouncycastle.pkix; requires org.graalvm.sdk; requires org.graalvm.nativeimage.builder; + requires org.bouncycastle.pg; exports io.ballerina.stdlib.crypto.nativeimpl; exports io.ballerina.stdlib.crypto.svm; } From 04f59c4f21016df7f425d50c5d4ae23b8961749c Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Thu, 9 May 2024 18:27:07 +0530 Subject: [PATCH 05/12] Update pgp method params --- ballerina/encrypt_decrypt.bal | 14 +++++++------- ballerina/pgp_utils.bal | 2 +- ballerina/tests/encrypt_decrypt_pgp_test.bal | 4 ++-- .../io/ballerina/stdlib/crypto/CryptoUtils.java | 1 - .../stdlib/crypto/nativeimpl/Decrypt.java | 10 +++++----- .../stdlib/crypto/nativeimpl/Encrypt.java | 14 +++++--------- 6 files changed, 20 insertions(+), 25 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 895f694..c458e16 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -251,17 +251,17 @@ public isolated function decryptAesGcm(byte[] input, byte[] key, byte[] iv, AesP # byte[] cipherText = check crypto:encryptPgp(message, publicKey); # ``` # -# + input - The content to be encrypted -# + key - Public key +# + 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[] input, byte[] key, *PgpEncryptionOptions options) +public isolated function encryptPgp(byte[] plainText, byte[] publicKey, *PgpEncryptionOptions options) returns byte[]|Error = @java:Method { name: "encryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; -# Returns the PGP-decrypted value for the given PGP-encrypted data. +# 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 @@ -272,11 +272,11 @@ public isolated function encryptPgp(byte[] input, byte[] key, *PgpEncryptionOpti # byte[] decryptedMessage = check crypto:decryptPgp(cipherText, privateKey, passphrase); # ``` # -# + input - The encrypted content to be decrypted -# + key - Private key +# + 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[] input, byte[] key, byte[] passphrase) +public isolated function decryptPgp(byte[] cipherText, byte[] privateKey, byte[] passphrase) returns byte[]|Error = @java:Method { name: "decryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" diff --git a/ballerina/pgp_utils.bal b/ballerina/pgp_utils.bal index b787806..161a2ef 100644 --- a/ballerina/pgp_utils.bal +++ b/ballerina/pgp_utils.bal @@ -43,4 +43,4 @@ public enum SymmetricKeyAlgorithmTags { CAMELLIA_128 = "11", CAMELLIA_192 = "12", CAMELLIA_256 = "13" -} \ No newline at end of file +} diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 3644fb0..4c4096a 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -1,6 +1,6 @@ -// Copyright (c) 2020 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). // -// WSO2 Inc. licenses this file to you under the Apache License, +// 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 diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java index d0376c3..f34e024 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java @@ -448,5 +448,4 @@ private static Digest selectHash(String algorithm) { } throw CryptoUtils.createError("Unsupported algorithm: " + algorithm); } - } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java index 521c671..36b4733 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java @@ -80,18 +80,18 @@ public static Object decryptRsaEcb(BArray inputValue, Object keys, Object paddin input, null, -1); } - public static Object decryptPgp(BArray inputValue, BArray keyValue, BArray passphrase) { - byte[] input = inputValue.getBytes(); - byte[] key = keyValue.getBytes(); + public static Object decryptPgp(BArray cipherTextValue, BArray privateKeyValue, BArray passphrase) { + byte[] cipherText = cipherTextValue.getBytes(); + byte[] privateKey = privateKeyValue.getBytes(); byte[] passphraseInBytes = passphrase.getBytes(); - InputStream keyStream = new ByteArrayInputStream(key); + InputStream keyStream = new ByteArrayInputStream(privateKey); try { PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator( keyStream, passphraseInBytes ); - return pgpDecryptionGenerator.decrypt(input); + return pgpDecryptionGenerator.decrypt(cipherText); } catch (IOException | PGPException e) { return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java index ff3b2aa..3930817 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java @@ -21,13 +21,9 @@ import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BMap; -import io.ballerina.runtime.api.values.BString; -import io.ballerina.runtime.api.values.BValue; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; import io.ballerina.stdlib.crypto.PgpEncryptionGenerator; -import org.bouncycastle.bcpg.CompressionAlgorithmTags; -import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.openpgp.PGPException; import java.io.ByteArrayInputStream; @@ -96,10 +92,10 @@ public static Object encryptRsaEcb(BArray inputValue, Object keys, Object paddin input, null, -1); } - public static Object encryptPgp(BArray inputValue, BArray keyValue, BMap options) { - byte[] input = inputValue.getBytes(); - byte[] key = keyValue.getBytes(); - InputStream keyStream = new ByteArrayInputStream(key); + public static Object encryptPgp(BArray plainTextValue, BArray publicKeyValue, BMap options) { + byte[] plainText = plainTextValue.getBytes(); + byte[] publicKey = publicKeyValue.getBytes(); + InputStream publicKeyStream = new ByteArrayInputStream(publicKey); PgpEncryptionGenerator pgpEncryptionGenerator = new PgpEncryptionGenerator( Integer.parseInt(options.get(StringUtils.fromString(COMPRESSION_ALGORITHM)).toString()), @@ -109,7 +105,7 @@ public static Object encryptPgp(BArray inputValue, BArray keyValue, BMap options ); try { - return pgpEncryptionGenerator.encrypt(input, keyStream); + return pgpEncryptionGenerator.encrypt(plainText, publicKeyStream); } catch (PGPException | IOException e) { return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); } From a3bfb6d5ae1c388b01d29e6e6bc9c686c308a857 Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Thu, 9 May 2024 20:16:04 +0530 Subject: [PATCH 06/12] Fix resource leak bug in PGP encryption --- .../stdlib/crypto/PgpEncryptionGenerator.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java index 110430b..021ba23 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -118,17 +118,17 @@ static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, static void copyAsLiteralData(OutputStream outputStream, InputStream in, long length) throws IOException { PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); - OutputStream pOut = lData.open(outputStream, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, - Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)), new byte[PgpEncryptionGenerator.BUFFER_SIZE]); byte[] buff = new byte[PgpEncryptionGenerator.BUFFER_SIZE]; - try (in) { + try (OutputStream pOut = lData.open(outputStream, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, + Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)), new byte[PgpEncryptionGenerator.BUFFER_SIZE]); + InputStream inputStream = in) { + int len; long totalBytesWritten = 0L; - while (totalBytesWritten <= length && (len = in.read(buff)) > 0) { + while (totalBytesWritten <= length && (len = inputStream.read(buff)) > 0) { pOut.write(buff, 0, len); totalBytesWritten += len; } - pOut.close(); } finally { Arrays.fill(buff, (byte) 0); } From d89771b83252945bf9c10b21b9c39640a031fbd3 Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Thu, 9 May 2024 21:17:46 +0530 Subject: [PATCH 07/12] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 6 +++--- ballerina/Dependencies.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index f80bfcd..9c442d1 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -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" @@ -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" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index a76bef6..313a332 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -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 = "io"}, {org = "ballerina", name = "jballerina.java"}, From dc1d31ef3f543517f5922e745a9c8e47b6648cff Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Thu, 9 May 2024 22:54:36 +0530 Subject: [PATCH 08/12] Add API documentation for public records for PGP --- ballerina/pgp_utils.bal | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ballerina/pgp_utils.bal b/ballerina/pgp_utils.bal index 161a2ef..cb09bf3 100644 --- a/ballerina/pgp_utils.bal +++ b/ballerina/pgp_utils.bal @@ -14,6 +14,12 @@ // 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 PgpEncryptionOptions record {| CompressionAlgorithmTags compressionAlgorithm = ZIP; SymmetricKeyAlgorithmTags symmetricKeyAlgorithm = AES_256; @@ -21,6 +27,12 @@ public type PgpEncryptionOptions record {| boolean withIntegrityCheck = true; |}; +# Represents the compressions 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", @@ -28,6 +40,22 @@ public enum CompressionAlgorithmTags { 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", From 247f52a52f4c51f26623cdf49a47bb7813222f97 Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Fri, 10 May 2024 15:04:55 +0530 Subject: [PATCH 09/12] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 313a332..575531e 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -12,7 +12,6 @@ org = "ballerina" name = "crypto" version = "2.7.1" dependencies = [ - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "test"}, @@ -22,19 +21,6 @@ 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" @@ -81,15 +67,6 @@ 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" From 78e3fb4fbeab147e5c59d3a51cfec372b405651c Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Fri, 10 May 2024 15:07:30 +0530 Subject: [PATCH 10/12] Address suggestions on pgp APIs --- ballerina/encrypt_decrypt.bal | 19 ++-- ballerina/pgp_utils.bal | 6 +- ballerina/tests/encrypt_decrypt_pgp_test.bal | 44 ++++++-- .../tests/resources/invalid_private_key.asc | 99 +++++++++++++++++ .../resources/pgp_private_key_passphrase.txt | 1 - ballerina/tests/test_utils.bal | 1 + .../stdlib/crypto/PgpDecryptionGenerator.java | 103 +++++++++++------- .../stdlib/crypto/PgpEncryptionGenerator.java | 35 +++--- .../stdlib/crypto/nativeimpl/Decrypt.java | 20 ++-- .../stdlib/crypto/nativeimpl/Encrypt.java | 38 ++++--- 10 files changed, 261 insertions(+), 105 deletions(-) create mode 100644 ballerina/tests/resources/invalid_private_key.asc delete mode 100644 ballerina/tests/resources/pgp_private_key_passphrase.txt diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index c458e16..4b516c5 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -247,15 +247,14 @@ public isolated function decryptAesGcm(byte[] input, byte[] key, byte[] iv, AesP # 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); +# byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); # ``` # # + plainText - The content to be encrypted -# + publicKey - Public key +# + 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, byte[] publicKey, *PgpEncryptionOptions options) +public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *PgpEncryptionOptions options) returns byte[]|Error = @java:Method { name: "encryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" @@ -264,19 +263,17 @@ public isolated function encryptPgp(byte[] plainText, byte[] publicKey, *PgpEncr # 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[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); # -# 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); +# 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 -# + privateKey - Private key +# + 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, byte[] privateKey, byte[] passphrase) +public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, byte[] passphrase) returns byte[]|Error = @java:Method { name: "decryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" diff --git a/ballerina/pgp_utils.bal b/ballerina/pgp_utils.bal index cb09bf3..9ff156a 100644 --- a/ballerina/pgp_utils.bal +++ b/ballerina/pgp_utils.bal @@ -14,7 +14,7 @@ // specific language governing permissions and limitations // under the License. -# 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 @@ -27,7 +27,7 @@ public type PgpEncryptionOptions record {| boolean withIntegrityCheck = true; |}; -# Represents the compressions algorithms available in PGP +# Represents the compression algorithms available in PGP. # # + UNCOMPRESSED - No compression # + ZIP - Uses (RFC 1951) compression @@ -40,7 +40,7 @@ public enum CompressionAlgorithmTags { BZIP2= "3" } -# Represent the symmetric key algorithms available in PGP +# Represent the symmetric key algorithms available in PGP. # # + NULL - No encryption # + IDEA - IDEA symmetric key algorithm diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 4c4096a..c09dfe2 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -15,26 +15,48 @@ // 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); + 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 = 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); + 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()); } + +@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:assertTrue(false, "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:assertTrue(false, "Should return a crypto Error"); + } +} diff --git a/ballerina/tests/resources/invalid_private_key.asc b/ballerina/tests/resources/invalid_private_key.asc new file mode 100644 index 0000000..416a529 --- /dev/null +++ b/ballerina/tests/resources/invalid_private_key.asc @@ -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----- diff --git a/ballerina/tests/resources/pgp_private_key_passphrase.txt b/ballerina/tests/resources/pgp_private_key_passphrase.txt deleted file mode 100644 index 9b60eb8..0000000 --- a/ballerina/tests/resources/pgp_private_key_passphrase.txt +++ /dev/null @@ -1 +0,0 @@ -qCr3bv@5mj5n4eY \ No newline at end of file diff --git a/ballerina/tests/test_utils.bal b/ballerina/tests/test_utils.bal index 59a1e2b..e12598c 100644 --- a/ballerina/tests/test_utils.bal +++ b/ballerina/tests/test_utils.bal @@ -36,4 +36,5 @@ const string INVALID_PUBLIC_CERT_PATH = "tests/resources/cert/public.crt.invalid const string PGP_PUBLIC_KEY_PATH = "tests/resources/public_key.asc"; const string PGP_PRIVATE_KEY_PATH = "tests/resources/private_key.asc"; +const string PGP_INVALID_PRIVATE_KEY_PATH = "tests/resources/invalid_private_key.asc"; const string PGP_PRIVATE_KEY_PASSPHRASE_PATH = "tests/resources/pgp_private_key_passphrase.txt"; diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java index 28590d8..3b18829 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -46,7 +46,13 @@ import java.security.Security; import java.util.Iterator; import java.util.Objects; +import java.util.Optional; +/** + * Provides functionality for PGP decryption operations. + * + * @since 2.7.0 + */ public class PgpDecryptionGenerator { static { @@ -58,19 +64,27 @@ public class PgpDecryptionGenerator { private final char[] passCode; private final PGPSecretKeyRingCollection pgpSecretKeyRingCollection; + // The constructor of the PGP decryption generator. public PgpDecryptionGenerator(InputStream privateKeyIn, byte[] passCode) throws IOException, PGPException { this.passCode = new String(passCode, StandardCharsets.UTF_8).toCharArray(); this.pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(privateKeyIn) , new JcaKeyFingerprintCalculator()); } - private PGPPrivateKey findSecretKey(long keyID) throws PGPException { - PGPSecretKey pgpSecretKey = pgpSecretKeyRingCollection.getSecretKey(keyID); - return pgpSecretKey == null ? null : pgpSecretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() - .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passCode)); + private Optional findSecretKey(long keyID) throws PGPException { + Optional pgpSecretKey = Optional.ofNullable(pgpSecretKeyRingCollection.getSecretKey(keyID)); + if (pgpSecretKey.isPresent()) { + PGPPrivateKey privateKey = pgpSecretKey.get().extractPrivateKey( + new JcePBESecretKeyDecryptorBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build(passCode)); + return Optional.of(privateKey); + } else { + return Optional.empty(); + } } - public void decryptStream(InputStream encryptedIn, OutputStream clearOut) + private void decryptStream(InputStream encryptedIn, OutputStream clearOut) throws PGPException, IOException { // Remove armour and return the underlying binary encrypted stream encryptedIn = PGPUtil.getDecoderStream(encryptedIn); @@ -81,11 +95,11 @@ public void decryptStream(InputStream encryptedIn, OutputStream clearOut) PGPEncryptedDataList pgpEncryptedDataList = (obj instanceof PGPEncryptedDataList) ? (PGPEncryptedDataList) obj : (PGPEncryptedDataList) pgpObjectFactory.nextObject(); - PGPPrivateKey pgpPrivateKey = null; + Optional pgpPrivateKey = Optional.empty(); PGPPublicKeyEncryptedData publicKeyEncryptedData = null; Iterator encryptedDataItr = pgpEncryptedDataList.getEncryptedDataObjects(); - while (pgpPrivateKey == null && encryptedDataItr.hasNext()) { + while (pgpPrivateKey.isEmpty() && encryptedDataItr.hasNext()) { publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedDataItr.next(); pgpPrivateKey = findSecretKey(publicKeyEncryptedData.getKeyID()); } @@ -94,52 +108,57 @@ public void decryptStream(InputStream encryptedIn, OutputStream clearOut) throw new PGPException("Could not generate PGPPublicKeyEncryptedData object"); } - if (pgpPrivateKey == null) { + if (pgpPrivateKey.isEmpty()) { throw new PGPException("Could Not Extract private key"); } decrypt(clearOut, pgpPrivateKey, publicKeyEncryptedData); } + // Decrypts the given byte array of encrypted data using PGP decryption. public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { - ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); - ByteArrayOutputStream clearOut = new ByteArrayOutputStream(); - decryptStream(encryptedIn, clearOut); - return ValueCreator.createArrayValue(clearOut.toByteArray()); + try (ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); + ByteArrayOutputStream clearOut = new ByteArrayOutputStream()) { + decryptStream(encryptedIn, clearOut); + return ValueCreator.createArrayValue(clearOut.toByteArray()); + } } - static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, + private static void decrypt(OutputStream clearOut, Optional pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { - PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(pgpPrivateKey); - InputStream decryptedCompressedIn = publicKeyEncryptedData.getDataStream(decryptorFactory); - - JcaPGPObjectFactory decCompObjFac = new JcaPGPObjectFactory(decryptedCompressedIn); - PGPCompressedData pgpCompressedData = (PGPCompressedData) decCompObjFac.nextObject(); - - InputStream compressedDataStream = new BufferedInputStream(pgpCompressedData.getDataStream()); - JcaPGPObjectFactory pgpCompObjFac = new JcaPGPObjectFactory(compressedDataStream); - - Object message = pgpCompObjFac.nextObject(); - - if (message instanceof PGPLiteralData pgpLiteralData) { - InputStream decDataStream = pgpLiteralData.getInputStream(); - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = decDataStream.read(buffer)) != -1) { - clearOut.write(buffer, 0, bytesRead); + if (pgpPrivateKey.isPresent()) { + PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(pgpPrivateKey.get()); + try (InputStream decryptedCompressedIn = publicKeyEncryptedData.getDataStream(decryptorFactory)) { + + JcaPGPObjectFactory decCompObjFac = new JcaPGPObjectFactory(decryptedCompressedIn); + PGPCompressedData pgpCompressedData = (PGPCompressedData) decCompObjFac.nextObject(); + + try (InputStream compressedDataStream = new BufferedInputStream(pgpCompressedData.getDataStream())) { + JcaPGPObjectFactory pgpCompObjFac = new JcaPGPObjectFactory(compressedDataStream); + + Object message = pgpCompObjFac.nextObject(); + + if (message instanceof PGPLiteralData pgpLiteralData) { + try (InputStream decDataStream = pgpLiteralData.getInputStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = decDataStream.read(buffer)) != -1) { + clearOut.write(buffer, 0, bytesRead); + } + } + } else if (message instanceof PGPOnePassSignatureList) { + throw new PGPException("Encrypted message contains a signed message not literal data"); + } else { + throw new PGPException("Unknown message type encountered during decryption"); + } + } } - clearOut.close(); - } else if (message instanceof PGPOnePassSignatureList) { - throw new PGPException("Encrypted message contains a signed message not literal data"); - } else { - throw new PGPException("Message is not a simple encrypted file - Type Unknown"); - } - // Perform the integrity check - if (publicKeyEncryptedData.isIntegrityProtected()) { - if (!publicKeyEncryptedData.verify()) { - throw new PGPException("Message failed integrity check"); + // Perform the integrity check + if (publicKeyEncryptedData.isIntegrityProtected()) { + if (!publicKeyEncryptedData.verify()) { + throw new PGPException("Message failed integrity check"); + } } } } - } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java index 021ba23..abfced9 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -48,6 +48,11 @@ import java.util.Objects; import java.util.Optional; +/** + * Provides functionality for PGP encryption operations. + * + * @since 2.7.0 + */ public class PgpEncryptionGenerator { static { @@ -62,6 +67,7 @@ public class PgpEncryptionGenerator { private final boolean withIntegrityCheck; private static final int BUFFER_SIZE = 8192; + // The constructor of the PGP encryption generator. public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorithm, boolean armor, boolean withIntegrityCheck) { this.compressionAlgorithm = compressionAlgorithm; @@ -70,7 +76,7 @@ public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorith this.withIntegrityCheck = withIntegrityCheck; } - public void encryptStream(OutputStream encryptOut, InputStream clearIn, long length, InputStream publicKeyIn) + private void encryptStream(OutputStream encryptOut, InputStream clearIn, long length, InputStream publicKeyIn) throws IOException, PGPException { PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm); @@ -87,27 +93,30 @@ public void encryptStream(OutputStream encryptOut, InputStream clearIn, long len if (armor) { encryptOut = new ArmoredOutputStream(encryptOut); } - OutputStream cipherOutStream = pgpEncryptedDataGenerator.open(encryptOut, new byte[BUFFER_SIZE]); - copyAsLiteralData(compressedDataGenerator.open(cipherOutStream), clearIn, length); - compressedDataGenerator.close(); - cipherOutStream.close(); + + try (OutputStream cipherOutStream = pgpEncryptedDataGenerator.open(encryptOut, new byte[BUFFER_SIZE])) { + copyAsLiteralData(compressedDataGenerator.open(cipherOutStream), clearIn, length); + compressedDataGenerator.close(); + } encryptOut.close(); } + // Encrypts the given byte array of plain text data using PGP encryption. public Object encrypt(byte[] clearData, InputStream publicKeyIn) throws PGPException, IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(clearData); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - encryptStream(outputStream, inputStream, clearData.length, publicKeyIn); - return ValueCreator.createArrayValue(outputStream.toByteArray()); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(clearData); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + encryptStream(outputStream, inputStream, clearData.length, publicKeyIn); + return ValueCreator.createArrayValue(outputStream.toByteArray()); + } } - static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, PGPException { + private static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, PGPException { PGPPublicKeyRingCollection pgpPublicKeyRings = new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream(keyInputStream), new JcaKeyFingerprintCalculator()); Iterator keyRingIterator = pgpPublicKeyRings.getKeyRings(); while (keyRingIterator.hasNext()) { PGPPublicKeyRing pgpPublicKeyRing = keyRingIterator.next(); - Optional pgpPublicKey = extractPGPKeyFromRing(pgpPublicKeyRing); + Optional pgpPublicKey = extractPgpKeyFromRing(pgpPublicKeyRing); if (pgpPublicKey.isPresent()) { return pgpPublicKey.get(); } @@ -115,7 +124,7 @@ static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, throw new PGPException("Invalid public key"); } - static void copyAsLiteralData(OutputStream outputStream, InputStream in, long length) + private static void copyAsLiteralData(OutputStream outputStream, InputStream in, long length) throws IOException { PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); byte[] buff = new byte[PgpEncryptionGenerator.BUFFER_SIZE]; @@ -134,7 +143,7 @@ static void copyAsLiteralData(OutputStream outputStream, InputStream in, long le } } - private static Optional extractPGPKeyFromRing(PGPPublicKeyRing pgpPublicKeyRing) { + private static Optional extractPgpKeyFromRing(PGPPublicKeyRing pgpPublicKeyRing) { for (PGPPublicKey publicKey : pgpPublicKeyRing) { if (publicKey.isEncryptionKey()) { return Optional.of(publicKey); diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java index 36b4733..ffa4adc 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java @@ -20,6 +20,7 @@ import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; import io.ballerina.stdlib.crypto.PgpDecryptionGenerator; @@ -28,6 +29,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; @@ -80,17 +83,18 @@ public static Object decryptRsaEcb(BArray inputValue, Object keys, Object paddin input, null, -1); } - public static Object decryptPgp(BArray cipherTextValue, BArray privateKeyValue, BArray passphrase) { + public static Object decryptPgp(BArray cipherTextValue, BString privateKeyPath, BArray passphrase) { byte[] cipherText = cipherTextValue.getBytes(); - byte[] privateKey = privateKeyValue.getBytes(); byte[] passphraseInBytes = passphrase.getBytes(); - InputStream keyStream = new ByteArrayInputStream(privateKey); - + byte[] privateKey; try { - PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator( - keyStream, - passphraseInBytes - ); + privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); + } catch (IOException e) { + return CryptoUtils.createError("Error occurred while reading public key: " + e.getMessage()); + } + + try (InputStream keyStream = new ByteArrayInputStream(privateKey)) { + PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); return pgpDecryptionGenerator.decrypt(cipherText); } catch (IOException | PGPException e) { return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java index 3930817..eb75baa 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java @@ -21,6 +21,7 @@ import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; import io.ballerina.stdlib.crypto.PgpEncryptionGenerator; @@ -29,6 +30,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; @@ -40,10 +43,10 @@ */ public class Encrypt { - public static final String COMPRESSION_ALGORITHM = "compressionAlgorithm"; - public static final String SYMMETRIC_KEY_ALGORITHM = "symmetricKeyAlgorithm"; - public static final String ARMOR = "armor"; - public static final String WITH_INTEGRITY_CHECK = "withIntegrityCheck"; + private static final BString COMPRESSION_ALGORITHM = StringUtils.fromString("compressionAlgorithm"); + private static final BString SYMMETRIC_KEY_ALGORITHM = StringUtils.fromString("symmetricKeyAlgorithm"); + private static final BString ARMOR = StringUtils.fromString("armor"); + private static final BString WITH_INTEGRITY_CHECK = StringUtils.fromString("withIntegrityCheck"); private Encrypt() {} @@ -92,21 +95,24 @@ public static Object encryptRsaEcb(BArray inputValue, Object keys, Object paddin input, null, -1); } - public static Object encryptPgp(BArray plainTextValue, BArray publicKeyValue, BMap options) { + public static Object encryptPgp(BArray plainTextValue, BString publicKeyPath, BMap options) { byte[] plainText = plainTextValue.getBytes(); - byte[] publicKey = publicKeyValue.getBytes(); - InputStream publicKeyStream = new ByteArrayInputStream(publicKey); - - PgpEncryptionGenerator pgpEncryptionGenerator = new PgpEncryptionGenerator( - Integer.parseInt(options.get(StringUtils.fromString(COMPRESSION_ALGORITHM)).toString()), - Integer.parseInt(options.get(StringUtils.fromString(SYMMETRIC_KEY_ALGORITHM)).toString()), - Boolean.parseBoolean(options.get(StringUtils.fromString(ARMOR)).toString()), - Boolean.parseBoolean(options.get(StringUtils.fromString(WITH_INTEGRITY_CHECK)).toString()) - ); - + byte[] publicKey; try { + publicKey = Files.readAllBytes(Path.of(publicKeyPath.toString())); + } catch (IOException e) { + return CryptoUtils.createError("Error occurred while reading public key: " + e.getMessage()); + } + + try (InputStream publicKeyStream = new ByteArrayInputStream(publicKey)) { + PgpEncryptionGenerator pgpEncryptionGenerator = new PgpEncryptionGenerator( + Integer.parseInt(options.get(COMPRESSION_ALGORITHM).toString()), + Integer.parseInt(options.get(SYMMETRIC_KEY_ALGORITHM).toString()), + Boolean.parseBoolean(options.get(ARMOR).toString()), + Boolean.parseBoolean(options.get(WITH_INTEGRITY_CHECK).toString()) + ); return pgpEncryptionGenerator.encrypt(plainText, publicKeyStream); - } catch (PGPException | IOException e) { + } catch (IOException | PGPException e) { return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); } } From 557f72b5647d4a2c64b3fba9af0312fa47475703 Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Sat, 11 May 2024 11:48:49 +0530 Subject: [PATCH 11/12] Change PgpEncryptionOptions to Options --- ballerina/encrypt_decrypt.bal | 2 +- ballerina/pgp_utils.bal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 4b516c5..0002f4f 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -254,7 +254,7 @@ public isolated function decryptAesGcm(byte[] input, byte[] key, byte[] iv, AesP # + 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, *PgpEncryptionOptions options) +public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Options options) returns byte[]|Error = @java:Method { name: "encryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" diff --git a/ballerina/pgp_utils.bal b/ballerina/pgp_utils.bal index 9ff156a..a60d351 100644 --- a/ballerina/pgp_utils.bal +++ b/ballerina/pgp_utils.bal @@ -20,7 +20,7 @@ # + 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 {| +public type Options record {| CompressionAlgorithmTags compressionAlgorithm = ZIP; SymmetricKeyAlgorithmTags symmetricKeyAlgorithm = AES_256; boolean armor = true; From f24bd9ac4eb6e33ca69d87d19842b1aeeef391db Mon Sep 17 00:00:00 2001 From: Dinuka Amarasinghe Date: Mon, 13 May 2024 08:51:15 +0530 Subject: [PATCH 12/12] Address minor comments on PGP testing --- ballerina/tests/encrypt_decrypt_pgp_test.bal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index c09dfe2..2451177 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -43,7 +43,7 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPrivateKey() return if plainText is Error { test:assertEquals(plainText.message(), "Error occurred while PGP decrypt: Could Not Extract private key"); } else { - test:assertTrue(false, "Should return a crypto Error"); + test:assertFail("Should return a crypto Error"); } } @@ -57,6 +57,6 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() return test:assertEquals(plainText.message(), "Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes"); } else { - test:assertTrue(false, "Should return a crypto Error"); + test:assertFail("Should return a crypto Error"); } }