From 0c230e218c5f2cbc0dde1f6d83deca05fe19f045 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 11:55:10 +0530 Subject: [PATCH 01/44] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 0ea61d07..74a87bac 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "crypto" -version = "2.7.2" +version = "2.7.3" 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.2" -path = "../native/build/libs/crypto-native-2.7.2.jar" +version = "2.7.3" +path = "../native/build/libs/crypto-native-2.7.3-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" From 85c3173ce3200d2c6ff5c17dd19e93b5096691c9 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 11:58:35 +0530 Subject: [PATCH 02/44] [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 78f2c216..271dd6ac 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.2" +version = "2.7.3" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.array"}, From 3ac65fde4ec9172f9834471b14fa6599d38a77eb Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 14:38:11 +0530 Subject: [PATCH 03/44] Add PGP encrypt/decrypt functions with files --- ballerina/encrypt_decrypt.bal | 31 +++++++++++++++++++ .../stdlib/crypto/PgpDecryptionGenerator.java | 8 +++++ .../stdlib/crypto/PgpEncryptionGenerator.java | 21 ++++++++----- .../stdlib/crypto/nativeimpl/Decrypt.java | 20 ++++++++++++ .../stdlib/crypto/nativeimpl/Encrypt.java | 25 +++++++++++++++ 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 0002f4ff..adff5754 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -260,6 +260,21 @@ public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Opt 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; +# Writes the PGP-encrypted value of the given data to a file specified by the output file path. +# ```ballerina +# check crypto:encryptPgpAsFile("input.txt", "public_key.asc", "output.txt"); +# ``` +# +# + inputFilePath - Path to the input file +# + publicKeyPath - Path to the public key +# + outputFilePath - Path to the output file +# + options - PGP encryption options +# + return - A `crypto:Error` will be returned if the process fails +public isolated function encryptPgpAsFile(string inputFilePath, string publicKeyPath, string outputFilePath, + *Options options) returns Error? = @java:Method { + '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(); @@ -278,3 +293,19 @@ public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, by name: "decryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; + +# Writes the PGP-decrypted value of the given data to a file specified by the output file path. +# ```ballerina +# byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); +# check crypto:decryptPgpAsFile("input.txt", "private_key.asc", passphrase, "output.txt"); +# ``` +# +# + inputFilePath - Path to the input file +# + privateKeyPath - Path to the private key +# + passphrase - passphrase of the private key +# + outputFilePath - Path to the output file +# + return - A `crypto:Error` will be returned if the process fails +public isolated function decryptPgpAsFile(string inputFilePath, string privateKeyPath, byte[] passphrase, + string outputFilePath) returns Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" +} external; 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 3b188291..1cba6a8b 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -43,6 +43,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.Security; import java.util.Iterator; import java.util.Objects; @@ -123,6 +125,12 @@ public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { } } + public void decrypt(InputStream encryptedIn, String outputPath) throws PGPException, IOException { + try (OutputStream outputStream = Files.newOutputStream(Path.of(outputPath))) { + decryptStream(encryptedIn, outputStream); + } + } + private static void decrypt(OutputStream clearOut, Optional pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { if (pgpPrivateKey.isPresent()) { 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 abfced90..6774966c 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -38,6 +38,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.SecureRandom; import java.security.Security; import java.time.LocalDateTime; @@ -76,7 +78,7 @@ public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorith this.withIntegrityCheck = withIntegrityCheck; } - private void encryptStream(OutputStream encryptOut, InputStream clearIn, long length, InputStream publicKeyIn) + private void encryptStream(OutputStream encryptOut, InputStream clearIn, InputStream publicKeyIn) throws IOException, PGPException { PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm); @@ -95,7 +97,7 @@ private void encryptStream(OutputStream encryptOut, InputStream clearIn, long le } try (OutputStream cipherOutStream = pgpEncryptedDataGenerator.open(encryptOut, new byte[BUFFER_SIZE])) { - copyAsLiteralData(compressedDataGenerator.open(cipherOutStream), clearIn, length); + copyAsLiteralData(compressedDataGenerator.open(cipherOutStream), clearIn); compressedDataGenerator.close(); } encryptOut.close(); @@ -105,11 +107,18 @@ private void encryptStream(OutputStream encryptOut, InputStream clearIn, long le public Object encrypt(byte[] clearData, InputStream publicKeyIn) throws PGPException, IOException { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(clearData); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - encryptStream(outputStream, inputStream, clearData.length, publicKeyIn); + encryptStream(outputStream, inputStream, publicKeyIn); return ValueCreator.createArrayValue(outputStream.toByteArray()); } } + public void encrypt(InputStream inputStream, InputStream publicKeyIn, String outputPath) + throws PGPException, IOException { + try (OutputStream outputStream = Files.newOutputStream(Path.of(outputPath))) { + encryptStream(outputStream, inputStream, publicKeyIn); + } + } + private static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, PGPException { PGPPublicKeyRingCollection pgpPublicKeyRings = new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream(keyInputStream), new JcaKeyFingerprintCalculator()); @@ -124,7 +133,7 @@ private static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOEx throw new PGPException("Invalid public key"); } - private static void copyAsLiteralData(OutputStream outputStream, InputStream in, long length) + private static void copyAsLiteralData(OutputStream outputStream, InputStream in) throws IOException { PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); byte[] buff = new byte[PgpEncryptionGenerator.BUFFER_SIZE]; @@ -133,10 +142,8 @@ private static void copyAsLiteralData(OutputStream outputStream, InputStream in, InputStream inputStream = in) { int len; - long totalBytesWritten = 0L; - while (totalBytesWritten <= length && (len = inputStream.read(buff)) > 0) { + while ((len = inputStream.read(buff)) > 0) { pOut.write(buff, 0, len); - totalBytesWritten += len; } } finally { Arrays.fill(buff, (byte) 0); 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 ffa4adc5..5aa7cc3c 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 @@ -100,4 +100,24 @@ public static Object decryptPgp(BArray cipherTextValue, BString privateKeyPath, return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); } } + + public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyPath, BArray passphrase, + BString outputFilePath) { + byte[] passphraseInBytes = passphrase.getBytes(); + byte[] privateKey; + try { + 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); + InputStream cipherTextStream = Files.newInputStream(Path.of(inputFilePath.toString()))) { + PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); + pgpDecryptionGenerator.decrypt(cipherTextStream, outputFilePath.getValue()); + return null; + } 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 eb75baad..2f4dc482 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 @@ -116,4 +116,29 @@ public static Object encryptPgp(BArray plainTextValue, BString publicKeyPath, BM return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); } } + + public static Object encryptPgpAsFile(BString inputFilePath, BString publicKeyPath, BString outputFilePath, + BMap options) { + 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); + InputStream inputStream = Files.newInputStream(Path.of(inputFilePath.toString())) + ) { + 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()) + ); + pgpEncryptionGenerator.encrypt(inputStream, publicKeyStream, outputFilePath.getValue()); + return null; + } catch (IOException | PGPException e) { + return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); + } + } } From 0fbae6bd6b8e121ce926c6de12767663beac88c9 Mon Sep 17 00:00:00 2001 From: Krishnananthalingam Tharmigan <63336800+TharmiganK@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:20:47 +0530 Subject: [PATCH 04/44] Apply suggestions from code review Co-authored-by: DimuthuMadushan <35717653+DimuthuMadushan@users.noreply.github.com> --- .../java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 5aa7cc3c..82914fe0 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 @@ -90,7 +90,7 @@ public static Object decryptPgp(BArray cipherTextValue, BString privateKeyPath, try { privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading public key: " + e.getMessage()); + return CryptoUtils.createError("Error occurred while reading private key: " + e.getMessage()); } try (InputStream keyStream = new ByteArrayInputStream(privateKey)) { @@ -108,11 +108,11 @@ public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyP try { privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading public key: " + e.getMessage()); + return CryptoUtils.createError("Error occurred while reading private key: " + e.getMessage()); } try (InputStream keyStream = new ByteArrayInputStream(privateKey); - InputStream cipherTextStream = Files.newInputStream(Path.of(inputFilePath.toString()))) { + InputStream cipherTextStream = Files.newInputStream(Path.of(inputFilePath.toString()))) { PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); pgpDecryptionGenerator.decrypt(cipherTextStream, outputFilePath.getValue()); return null; From ae7af8c03207e2ec0bd87339642403d000f6c814 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 15:53:46 +0530 Subject: [PATCH 05/44] [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 271dd6ac..52cab4f6 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -12,6 +12,7 @@ org = "ballerina" name = "crypto" version = "2.7.3" 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.1" +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 3deeba87128a87c7bff8dafe7f26f334365d78d5 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:06:07 +0530 Subject: [PATCH 06/44] Add test cases --- ballerina/encrypt_decrypt.bal | 2 + ballerina/private_public_key.bal | 4 +- ballerina/tests/encrypt_decrypt_pgp_test.bal | 58 +++++++++++++++++++- ballerina/tests/resources/sample.txt | 12 ++++ ballerina/tests/test_utils.bal | 4 ++ 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 ballerina/tests/resources/sample.txt diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index adff5754..f2f70587 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -276,6 +276,7 @@ public isolated function encryptPgpAsFile(string inputFilePath, string publicKey } external; # Returns the PGP-decrypted value of the given PGP-encrypted data. +# If the output file already exists, it will be overwritten. # ```ballerina # byte[] message = "Hello Ballerina!".toBytes(); # byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); @@ -295,6 +296,7 @@ public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, by } external; # Writes the PGP-decrypted value of the given data to a file specified by the output file path. +# If the output file already exists, it will be overwritten. # ```ballerina # byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); # check crypto:decryptPgpAsFile("input.txt", "private_key.asc", passphrase, "output.txt"); diff --git a/ballerina/private_public_key.bal b/ballerina/private_public_key.bal index 3a963ede..48d59005 100644 --- a/ballerina/private_public_key.bal +++ b/ballerina/private_public_key.bal @@ -176,7 +176,7 @@ public isolated function decodeRsaPrivateKeyFromKeyFile(string keyFile, string? # crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromContent(keyFileContent, "keyPassword"); # ``` # -# + keyFile - Private key content as a byte array +# + content - Private key content as a byte array # + keyPassword - Password of the private key if it is encrypted # + return - Reference to the private key or else a `crypto:Error` if the private key was unreadable public isolated function decodeRsaPrivateKeyFromContent(byte[] content, string? keyPassword = ()) returns PrivateKey|Error = @java:Method { @@ -311,7 +311,7 @@ public isolated function decodeRsaPublicKeyFromCertFile(string certFile) returns # crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromContent(certContent); # ``` # -# + certFile - The certificate content as a byte array +# + content - The certificate content as a byte array # + return - Reference to the public key or else a `crypto:Error` if the public key was unreadable public isolated function decodeRsaPublicKeyFromContent(byte[] content) returns PublicKey|Error = @java:Method { 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decode" diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 2451177c..686dd541 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -14,6 +14,7 @@ // specific language governing permissions and limitations // under the License. +import ballerina/io; import ballerina/test; @test:Config {} @@ -55,8 +56,63 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() return 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"); + "Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes"); } else { test:assertFail("Should return a crypto Error"); } } + +@test:Config { + serialExecution: true +} +isolated function testEncryptAndDecryptFileWithPgp() returns error? { + byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); + check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); + check decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); + test:assertTrue(check isSameFileContent(SAMPLE_TEXT, TARGET_DECRYPTION_OUTPUT)); +} + +@test:Config { + serialExecution: true +} +isolated function testEncryptAndDecryptFileWithPgpWithOptions() returns error? { + byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); + check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT, symmetricKeyAlgorithm = AES_128, armor = false); + check decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); + test:assertTrue(check isSameFileContent(SAMPLE_TEXT, TARGET_DECRYPTION_OUTPUT)); +} + +@test:Config { + serialExecution: true +} +isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPrivateKey() returns error? { + byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); + check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); + error? err = decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_INVALID_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); + if err is Error { + test:assertEquals(err.message(), "Error occurred while PGP decrypt: Could Not Extract private key"); + } else { + test:assertFail("Should return a crypto Error"); + } +} + +@test:Config { + serialExecution: true +} +isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPassphrase() returns error? { + byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); + check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); + error? err = decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); + if err is Error { + test:assertEquals(err.message(), + "Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes"); + } else { + test:assertFail("Should return a crypto Error"); + } +} + +isolated function isSameFileContent(string inputFilePath, string outputFilePath) returns boolean|error { + byte[] input = check io:fileReadBytes(inputFilePath); + byte[] output = check io:fileReadBytes(outputFilePath); + return input.toBase64() == output.toBase64(); +} diff --git a/ballerina/tests/resources/sample.txt b/ballerina/tests/resources/sample.txt new file mode 100644 index 00000000..4556189a --- /dev/null +++ b/ballerina/tests/resources/sample.txt @@ -0,0 +1,12 @@ + +Ballerina is an open-source programming language designed for cloud-native application development. It combines features for integration, service orchestration, and network interaction, with a focus on ease of use for building APIs, managing data, and deploying in distributed environments. Ballerina's syntax and built-in concurrency support make it well-suited for creating robust, scalable, and secure services. + +Ballerina adopts a developer-friendly approach by incorporating modern programming constructs, such as structural typing, flexible JSON handling, and a familiar C-style syntax, which reduces the learning curve for developers. The language has first-class support for network primitives, allowing developers to directly work with network protocols like HTTP, WebSockets, and gRPC without the need for additional libraries. This direct handling of network interactions makes Ballerina ideal for writing microservices and integrating with other systems effortlessly. + +Ballerina also features built-in support for distributed transactions, reliable messaging, and data transformations, making it suitable for integration-heavy applications. Its built-in observability tools, including metrics, logs, and distributed tracing, help developers monitor and debug applications efficiently. Ballerina is inherently cloud-native, with easy containerization and Kubernetes deployment support, simplifying the process of deploying services in modern cloud environments. + +The concurrency model in Ballerina is based on the concept of "strands," which are lightweight threads managed by the language runtime. This model allows developers to write concurrent code using simple constructs, such as asynchronous functions and workers, without worrying about low-level threading concerns. This makes it easier to develop applications that are responsive and scalable, capable of handling high loads and concurrent user interactions. + +Ballerina’s ecosystem includes various tools, such as the Ballerina Central registry, which provides a platform for sharing and discovering packages. The language’s visual representation of code through sequence diagrams is another unique feature, enabling both developers and non-developers to better understand program behavior, especially for integration logic. Ballerina's compiler can generate these diagrams automatically, which is beneficial for documentation and analysis of workflows. + +Furthermore, Ballerina's support for data-oriented programming makes it easy to transform and manipulate structured data formats like JSON, XML, and SQL. This, along with the language’s built-in type system that directly represents these data types, reduces the need for complex data mapping and serialization tasks. With support for RESTful APIs, GraphQL, and multiple database connectors, Ballerina is designed to provide seamless integration capabilities, making it an excellent choice for businesses looking to modernize their IT landscape with cloud-native services. diff --git a/ballerina/tests/test_utils.bal b/ballerina/tests/test_utils.bal index e12598c8..53655aeb 100644 --- a/ballerina/tests/test_utils.bal +++ b/ballerina/tests/test_utils.bal @@ -38,3 +38,7 @@ 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"; + +const string SAMPLE_TEXT = "tests/resources/sample.txt"; +const string TARGET_ENCRYPTION_OUTPUT = "target/encrypted_output.txt"; +const string TARGET_DECRYPTION_OUTPUT = "target/decrypted_output.txt"; From c930edd6c33ee0a9bb253cf16c750d07f3b367ef Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:08:49 +0530 Subject: [PATCH 07/44] Update change log --- changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/changelog.md b/changelog.md index f4a09171..57b2a5e9 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- [Introduce new APIs to support PGP encryption and decryption with files](https://github.com/ballerina-platform/ballerina-library/issues/7064) + +## [2.7.2] - 2024-05-30 + ### Added - [Implement the support for reading private/public keys from the content](https://github.com/ballerina-platform/ballerina-library/issues/6513) From 04ba4ca83b46d98104d56ae8e6ec1a850b35aae7 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:22:03 +0530 Subject: [PATCH 08/44] Update spec --- docs/spec/spec.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 7c5850ae..8a1f6b72 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -51,18 +51,19 @@ The conforming implementation of the specification is released and included in t * 4.17. [Decode ML-KEM-768 Private key using Private key and Password](#417-decode-ml-kem-768-private-key-using-private-key-and-password) * 4.18. [Decode ML-KEM-768 Public key from PKCS12 file](#418-decode-ml-kem-768-public-key-from-pkcs12-file) * 4.19. [Decode ML-KEM-768 Public key from the certificate file](#419-decode-ml-kem-768-public-key-from-the-certificate-file) - 5. [Encrypt-Decrypt](#5-encrypt-decrypt) * 5.1. [Encryption](#51-encryption) * 5.1.1. [RSA](#511-rsa) * 5.1.2. [AES-CBC](#512-aes-cbc) * 5.1.3. [AES-ECB](#513-aes-ecb) * 5.1.4. [AES-GCM](#514-aes-gcm) + * 5.1.5. [PGP](#515-pgp) * 5.2. [Decryption](#52-decryption) * 5.2.1. [RSA-ECB](#521-rsa-ecb) * 5.2.2. [AES-CBC](#522-aes-cbc) * 5.2.3. [AES-ECB](#523-aes-ecb) * 5.2.4. [AES-GCM](#524-aes-gcm) + * 5.2.5. [PGP](#525-pgp) 6. [Sign and Verify](#6-sign-and-verify) * 6.1. [Sign messages](#61-sign-messages) * 6.1.1. [RSA-MD5](#611-rsa-md5) @@ -502,6 +503,46 @@ foreach int i in 0...15 { byte[] cipherText = check crypto:encryptAesGcm(data, key, initialVector); ``` +#### 5.1.5. [PGP](#515-pgp) + +This API can be used to create the PGP-encrypted value for the given data. + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +string publicKeyPath = "/path/to/publickey.asc"; + +byte[] cipherText = check crypto:encryptPgp(data, publicKeyPath); +``` + +The following encryption options can be configured in the PGP encryption. + +| Option | Description | Default Value | +|-----------------------|-------------------------------------------------------------------|---------------| +| compressionAlgorithm | Specifies the compression algorithm used for PGP encryption | ZIP | +| symmetricKeyAlgorithm | Specifies the symmetric key algorithm used for encryption | AES_256 | +| armor | Indicates whether ASCII armor is enabled for the encrypted output | true | +| withIntegrityCheck | Indicates whether integrity check is included in the encryption | true | + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +string publicKeyPath = "/path/to/publickey.asc"; + +byte[] cipherText = check crypto:encryptPgp(data, publicKeyPath, armor = false); +``` + +In addition to the above, the following API can be used to read a content from a file, encrypt it using the PGP public +key and write the encrypted content to the file specified. + +```ballerina +string inputFilePath = "/path/to/input.txt"; +string outputFilePath = "/path/to/output.txt"; +string publicKeyPath = "/path/to/publickey.asc"; + +check crypto:encryptPgpAsFile(inputFilePath, publicKeyPath, outputFilePath); +``` + ### 5.2. [Decryption](#52-decryption) #### 5.2.1. [RSA-ECB](#521-rsa-ecb) @@ -574,6 +615,33 @@ byte[] cipherText = check crypto:encryptAesGcm(data, key, initialVector); byte[] plainText = check crypto:decryptAesGcm(cipherText, key, initialVector); ``` +#### 5.2.5. [PGP](#525-pgp) + +This API can be used to create the PGP-decrypted value for the given PGP-encrypted data. + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +string publicKeyPath = "/path/to/publickey.asc"; +string privateKeyPath = "/path/to/privatekey.asc"; +string passPhrase = "passphrase"; + +byte[] cipherText = check crypto:encryptPgp(data, publicKeyPath); +byte[] plainText = check crypto:decryptPgp(cipherText, privateKeyPath, passPhrase.toBytes()); +``` + +In addition to the above, the following API can be used to read an encrypted content from a file, decrypt it using the +PGP private key and passphrase and write the decrypted content to the file specified. + +```ballerina +string inputFilePath = "/path/to/input.txt"; +string outputFilePath = "/path/to/output.txt"; +string privateKeyPath = "/path/to/privatekey.asc"; +string passPhrase = "passphrase"; + +check crypto:decryptPgpAsFile(inputFilePath, privateKeyPath, passPhrase.toBytes(), outputFilePath); +``` + ## 6. [Sign and Verify](#6-sign-and-verify) The `crypto` library supports signing data using the RSA private key and verification of the signature using the RSA public key. This supports MD5, SHA1, SHA256, SHA384, and SHA512 digesting algorithms, and ML-DSA-65 post-quantum signature algorithm as well. From 365414329bd41d86b44312c717f4c4d3cecd0cc5 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:23:31 +0530 Subject: [PATCH 09/44] Update to next minor version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ac5ea3e8..66ab4c5d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.7.3-SNAPSHOT +version=2.8.0-SNAPSHOT puppycrawlCheckstyleVersion=10.12.0 bouncycastleVersion=1.78 githubSpotbugsVersion=5.0.14 From 1fa055274a5410bf41250c5437da43bd2554dee4 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:23:55 +0530 Subject: [PATCH 10/44] [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 74a87bac..1ce6ae2a 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "crypto" -version = "2.7.3" +version = "2.8.0" 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.3" -path = "../native/build/libs/crypto-native-2.7.3-SNAPSHOT.jar" +version = "2.8.0" +path = "../native/build/libs/crypto-native-2.8.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 52cab4f6..3cc7bc46 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.3" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, From 95f34c14ff9a9aed77d3bacf790ad68be888de4a Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:31:26 +0530 Subject: [PATCH 11/44] [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 3cc7bc46..0d00631e 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -107,7 +107,7 @@ modules = [ [[package]] org = "ballerina" name = "time" -version = "2.4.0" +version = "2.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] From 957f623f15f96c143aa17ceb119ce2c4c7720bda Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:36:37 +0530 Subject: [PATCH 12/44] Add IO test dependency --- build.gradle | 2 ++ gradle.properties | 1 + 2 files changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 60573602..be8428ca 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ ext.puppycrawlCheckstyleVersion = project.puppycrawlCheckstyleVersion ext.bouncycastleVersion = project.bouncycastleVersion ext.ballerinaLangVersion = project.ballerinaLangVersion ext.stdlibTimeVersion = project.stdlibTimeVersion +ext.stdlibIoVersion = project.stdlibIoVersion allprojects { group = project.group @@ -68,6 +69,7 @@ subprojects { dependencies { /* Standard libraries */ ballerinaStdLibs "io.ballerina.stdlib:time-ballerina:${stdlibTimeVersion}" + ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}" } } diff --git a/gradle.properties b/gradle.properties index 66ab4c5d..b8f845a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,4 @@ nativeImageVersion=22.2.0 ballerinaLangVersion=2201.9.0 stdlibTimeVersion=2.4.0 +stdlibIoVersion=1.6.1 From 5ba88d7d21748a54d11ba4f7eda224128b2b288c Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:37:15 +0530 Subject: [PATCH 13/44] Add suggestions from review --- ballerina/encrypt_decrypt.bal | 6 +-- ballerina/tests/encrypt_decrypt_pgp_test.bal | 2 +- ballerina/tests/test_utils.bal | 50 +++++++++---------- .../stdlib/crypto/nativeimpl/Decrypt.java | 3 +- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index f2f70587..26b04dba 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -260,7 +260,8 @@ public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Opt 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; -# Writes the PGP-encrypted value of the given data to a file specified by the output file path. +# Writes the PGP-encrypted value of the content given in the input file to a file specified by the output file path. +# If the output file already exists, it will be overwritten. # ```ballerina # check crypto:encryptPgpAsFile("input.txt", "public_key.asc", "output.txt"); # ``` @@ -276,7 +277,6 @@ public isolated function encryptPgpAsFile(string inputFilePath, string publicKey } external; # Returns the PGP-decrypted value of the given PGP-encrypted data. -# If the output file already exists, it will be overwritten. # ```ballerina # byte[] message = "Hello Ballerina!".toBytes(); # byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); @@ -295,7 +295,7 @@ public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, by 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; -# Writes the PGP-decrypted value of the given data to a file specified by the output file path. +# Writes the PGP-decrypted value of the content given in the input file to a file specified by the output file path. # If the output file already exists, it will be overwritten. # ```ballerina # byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 686dd541..885de768 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -114,5 +114,5 @@ isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPassphrase() re isolated function isSameFileContent(string inputFilePath, string outputFilePath) returns boolean|error { byte[] input = check io:fileReadBytes(inputFilePath); byte[] output = check io:fileReadBytes(outputFilePath); - return input.toBase64() == output.toBase64(); + return input == output; } diff --git a/ballerina/tests/test_utils.bal b/ballerina/tests/test_utils.bal index 53655aeb..8f1a9a0f 100644 --- a/ballerina/tests/test_utils.bal +++ b/ballerina/tests/test_utils.bal @@ -14,31 +14,31 @@ // specific language governing permissions and limitations // under the License. -const string KEYSTORE_PATH = "tests/resources/keyStore.p12"; -const string EC_KEYSTORE_PATH = "tests/resources/ec-keystore.pkcs12"; -const string MLDSA_KEYSTORE_PATH = "tests/resources/mldsa-keystore.pkcs12"; -const string MLKEM_KEYSTORE_PATH = "tests/resources/mlkem-keystore.pkcs12"; -const string ENCRYPTED_KEY_PAIR_PATH = "tests/resources/encryptedKeyPair.pem"; -const string KEY_PAIR_PATH = "tests/resources/keyPair.pem"; -const string ENCRYPTED_PRIVATE_KEY_PATH = "tests/resources/encryptedPrivate.key"; -const string PRIVATE_KEY_PATH = "tests/resources/private.key"; -const string X509_PUBLIC_CERT_PATH = "tests/resources/public.crt"; -const string EC_CERT_PATH = "tests/resources/ec-cert.crt"; -const string EC_PRIVATE_KEY_PATH = "tests/resources/ec-key.pem"; -const string MLDSA_CERT_PATH = "tests/resources/mldsa-cert.crt"; -const string MLDSA_PRIVATE_KEY_PATH = "tests/resources/mldsa-key.pem"; -const string MLKEM_CERT_PATH = "tests/resources/mlkem-cert.crt"; -const string MLKEM_PRIVATE_KEY_PATH = "tests/resources/mlkem-key.pem"; +const KEYSTORE_PATH = "tests/resources/keyStore.p12"; +const EC_KEYSTORE_PATH = "tests/resources/ec-keystore.pkcs12"; +const MLDSA_KEYSTORE_PATH = "tests/resources/mldsa-keystore.pkcs12"; +const MLKEM_KEYSTORE_PATH = "tests/resources/mlkem-keystore.pkcs12"; +const ENCRYPTED_KEY_PAIR_PATH = "tests/resources/encryptedKeyPair.pem"; +const KEY_PAIR_PATH = "tests/resources/keyPair.pem"; +const ENCRYPTED_PRIVATE_KEY_PATH = "tests/resources/encryptedPrivate.key"; +const PRIVATE_KEY_PATH = "tests/resources/private.key"; +const X509_PUBLIC_CERT_PATH = "tests/resources/public.crt"; +const EC_CERT_PATH = "tests/resources/ec-cert.crt"; +const EC_PRIVATE_KEY_PATH = "tests/resources/ec-key.pem"; +const MLDSA_CERT_PATH = "tests/resources/mldsa-cert.crt"; +const MLDSA_PRIVATE_KEY_PATH = "tests/resources/mldsa-key.pem"; +const MLKEM_CERT_PATH = "tests/resources/mlkem-cert.crt"; +const 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 INVALID_KEYSTORE_PATH = "tests/resources/cert/keyStore.p12.invalid"; +const INVALID_PRIVATE_KEY_PATH = "tests/resources/cert/private.key.invalid"; +const 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"; +const PGP_PUBLIC_KEY_PATH = "tests/resources/public_key.asc"; +const PGP_PRIVATE_KEY_PATH = "tests/resources/private_key.asc"; +const PGP_INVALID_PRIVATE_KEY_PATH = "tests/resources/invalid_private_key.asc"; +const PGP_PRIVATE_KEY_PASSPHRASE_PATH = "tests/resources/pgp_private_key_passphrase.txt"; -const string SAMPLE_TEXT = "tests/resources/sample.txt"; -const string TARGET_ENCRYPTION_OUTPUT = "target/encrypted_output.txt"; -const string TARGET_DECRYPTION_OUTPUT = "target/decrypted_output.txt"; +const SAMPLE_TEXT = "tests/resources/sample.txt"; +const TARGET_ENCRYPTION_OUTPUT = "target/encrypted_output.txt"; +const TARGET_DECRYPTION_OUTPUT = "target/decrypted_output.txt"; 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 82914fe0..21373c1a 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 @@ -112,7 +112,8 @@ public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyP } try (InputStream keyStream = new ByteArrayInputStream(privateKey); - InputStream cipherTextStream = Files.newInputStream(Path.of(inputFilePath.toString()))) { + InputStream cipherTextStream = Files.newInputStream(Path.of(inputFilePath.toString())) + ) { PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); pgpDecryptionGenerator.decrypt(cipherTextStream, outputFilePath.getValue()); return null; From e207f957cb284d4252637b175a508fda6e2e9406 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:39:27 +0530 Subject: [PATCH 14/44] Change error message --- ballerina/tests/encrypt_decrypt_pgp_test.bal | 4 ++-- .../io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 885de768..562ebe1c 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -42,7 +42,7 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPrivateKey() return 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"); + test:assertEquals(plainText.message(), "Error occurred while PGP decrypt: Could not Extract private key"); } else { test:assertFail("Should return a crypto Error"); } @@ -90,7 +90,7 @@ isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPrivateKey() re check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); error? err = decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_INVALID_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); if err is Error { - test:assertEquals(err.message(), "Error occurred while PGP decrypt: Could Not Extract private key"); + test:assertEquals(err.message(), "Error occurred while PGP decrypt: Could not Extract private key"); } else { test:assertFail("Should return a crypto Error"); } 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 1cba6a8b..286f2a4a 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -111,7 +111,7 @@ private void decryptStream(InputStream encryptedIn, OutputStream clearOut) } if (pgpPrivateKey.isEmpty()) { - throw new PGPException("Could Not Extract private key"); + throw new PGPException("Could not Extract private key"); } decrypt(clearOut, pgpPrivateKey, publicKeyEncryptedData); } From 18ff4cc0aa5ac399aa04b48c794bc01ca6f7da57 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 2 Oct 2024 16:40:54 +0530 Subject: [PATCH 15/44] Update time version --- ballerina/Dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 0d00631e..3cc7bc46 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -107,7 +107,7 @@ modules = [ [[package]] org = "ballerina" name = "time" -version = "2.5.0" +version = "2.4.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] From 95c6beb2aee1f55006892ff830f885d6146e4413 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 7 Oct 2024 12:55:49 +0530 Subject: [PATCH 16/44] Add stream pgp decrypt support --- ballerina/encrypt_decrypt.bal | 24 +++- ballerina/stream_iterator.bal | 33 +++++ .../stdlib/crypto/BallerinaInputStream.java | 120 ++++++++++++++++++ .../io/ballerina/stdlib/crypto/Constants.java | 4 + .../stdlib/crypto/PgpDecryptionGenerator.java | 118 +++++++++++++---- .../stdlib/crypto/nativeimpl/Decrypt.java | 31 +++++ .../stdlib/crypto/nativeimpl/StreamUtils.java | 84 ++++++++++++ spotbugs-exclude.xml | 5 +- 8 files changed, 388 insertions(+), 31 deletions(-) create mode 100644 ballerina/stream_iterator.bal create mode 100644 native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java create mode 100644 native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 26b04dba..6e1e4463 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -111,7 +111,7 @@ public isolated function encryptAesCbc(byte[] input, byte[] key, byte[] iv, AesP # + padding - The padding algorithm # + return - Encrypted data or else a `crypto:Error` if the key is invalid public isolated function encryptAesEcb(byte[] input, byte[] key, AesPadding padding = PKCS5) - returns byte[]|Error = @java:Method { + returns byte[]|Error = @java:Method { name: "encryptAesEcb", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; @@ -189,7 +189,7 @@ public isolated function decryptRsaEcb(byte[] input, PrivateKey|PublicKey key, R # + padding - The padding algorithm # + return - Decrypted data or else a `crypto:Error` if the key is invalid public isolated function decryptAesCbc(byte[] input, byte[] key, byte[] iv, AesPadding padding = PKCS5) - returns byte[]|Error = @java:Method { + returns byte[]|Error = @java:Method { name: "decryptAesCbc", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; @@ -255,7 +255,7 @@ public isolated function decryptAesGcm(byte[] input, byte[] key, byte[] iv, AesP # + options - PGP encryption options # + return - Encrypted data or else a `crypto:Error` if the key is invalid public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Options options) - returns byte[]|Error = @java:Method { + returns byte[]|Error = @java:Method { name: "encryptPgp", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; @@ -280,7 +280,7 @@ public isolated function encryptPgpAsFile(string inputFilePath, string publicKey # ```ballerina # byte[] message = "Hello Ballerina!".toBytes(); # byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); -# +# # byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); # byte[] decryptedMessage = check crypto:decryptPgp(cipherText, "private_key.asc", passphrase); # ``` @@ -311,3 +311,19 @@ public isolated function decryptPgpAsFile(string inputFilePath, string privateKe string outputFilePath) returns Error? = @java:Method { 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; + +# Returns the PGP-decrypted stream of the content given in the input stream. +# ```ballerina +# byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); +# stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); +# stream|Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); +# ``` +# +# + inputStream - The encrypted content as a stream +# + privateKeyPath - Path to the private key +# + passphrase - passphrase of the private key +# + return - Decrypted stream or else a `crypto:Error` if the key or passphrase is invalid +public isolated function decryptStreamPgp(stream inputStream, string privateKeyPath, + byte[] passphrase) returns stream|Error = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" +} external; diff --git a/ballerina/stream_iterator.bal b/ballerina/stream_iterator.bal new file mode 100644 index 00000000..2ca2cf99 --- /dev/null +++ b/ballerina/stream_iterator.bal @@ -0,0 +1,33 @@ +import ballerina/jballerina.java; + +class StreamIterator { + boolean isClosed = false; + + isolated function next() returns record {|byte[] value;|}|Error? { + byte[]|Error? bytes = self.read(); + if bytes is byte[] { + return {value: bytes}; + } else { + return bytes; + } + } + + isolated function close() returns Error? { + if !self.isClosed { + var closeResult = self.closeStream(); + if closeResult is () { + self.isClosed = true; + } + return closeResult; + } + return; + } + + isolated function read() returns byte[]|Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" + } external; + + isolated function closeStream() returns Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" + } external; +} diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java new file mode 100644 index 00000000..368b72e5 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java @@ -0,0 +1,120 @@ +/* + * 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.Environment; +import io.ballerina.runtime.api.async.Callback; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BStream; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; + +/** + * Represents a Ballerina stream as an {@link InputStream}. + * + * @since 2.8.0 + */ +public class BallerinaInputStream extends InputStream { + private final Environment environment; + private final BStream ballerinaStream; + private ByteBuffer buffer = null; + + public BallerinaInputStream(Environment environment, BStream ballerinaStream) { + this.ballerinaStream = ballerinaStream; + this.environment = environment; + } + + @Override + public int read() throws IOException { + if (Objects.isNull(buffer) || !buffer.hasRemaining()) { + Object nextElement = getNext(); + if (nextElement instanceof BError) { + throw new IOException(((BError) nextElement).getMessage()); + } + if (Objects.isNull(nextElement)) { + return -1; + } + if (nextElement instanceof BMap nextValue) { + Object nextBytes = nextValue.get(StringUtils.fromString("value")); + if (nextBytes instanceof BArray) { + buffer = ByteBuffer.wrap(((BArray) nextBytes).getBytes()); + } else { + throw new IOException("Error occurred while reading the next element from the stream: " + + "unexpected value type"); + } + } else { + throw new IOException("Error occurred while reading the next element from the stream: " + + "unexpected value type"); + } + } + return buffer.get() & 0xFF; + } + + @Override + public void close() { + Object result = callBallerinaFunction("close", "Error occurred while closing the stream"); + if (result instanceof BError) { + throw new RuntimeException(((BError) result).getMessage()); + } + } + + public Object getNext() { + return callBallerinaFunction("next", "Error occurred while reading the next element from the stream"); + } + + private Object callBallerinaFunction(String functionName, String message) { + final Object[] nextResult = new Object[1]; + CountDownLatch countDownLatch = new CountDownLatch(1); + Callback returnCallback = new StreamCallback(message, nextResult, countDownLatch); + + environment.getRuntime().invokeMethodAsyncSequentially(ballerinaStream.getIteratorObj(), functionName, null, + null, returnCallback, null, null); + try { + countDownLatch.await(); + } catch (InterruptedException exception) { + return CryptoUtils.createError("Error occurred while reading the next element from the stream: " + + "interrupted exception"); + } + return nextResult[0]; + } + + private record StreamCallback(String message, Object[] nextResult, + CountDownLatch countDownLatch) implements Callback { + + @Override + public void notifySuccess(Object result) { + nextResult[0] = result; + countDownLatch.countDown(); + } + + @Override + public void notifyFailure(BError bError) { + BError error = CryptoUtils.createError(String.format("%s: %s", message, bError.getMessage())); + nextResult[0] = error; + countDownLatch.countDown(); + } + } +} + diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java index 52ffec48..b084fcaf 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java @@ -124,4 +124,8 @@ private Constants() {} public static final String GCM = "GCM"; public static final String AES = "AES"; public static final String RSA = "RSA"; + + public static final String DECRYPTED_STREAM = "INPUT_STREAM"; + public static final String COMPRESSED_PGP_STREAM = "COMPRESSED_PGP_STREAM"; + public static final String COMPRESSED_STREAM = "COMPRESSED_STREAM"; } 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 286f2a4a..a4fe3c93 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -18,6 +18,7 @@ package io.ballerina.stdlib.crypto; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.values.BObject; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -50,6 +51,10 @@ import java.util.Objects; import java.util.Optional; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_PGP_STREAM; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_STREAM; +import static io.ballerina.stdlib.crypto.Constants.DECRYPTED_STREAM; + /** * Provides functionality for PGP decryption operations. * @@ -113,7 +118,36 @@ private void decryptStream(InputStream encryptedIn, OutputStream clearOut) if (pgpPrivateKey.isEmpty()) { throw new PGPException("Could not Extract private key"); } - decrypt(clearOut, pgpPrivateKey, publicKeyEncryptedData); + decrypt(clearOut, pgpPrivateKey.get(), publicKeyEncryptedData); + } + + private void decryptStream(InputStream encryptedIn, BObject iteratorObj) 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(); + + Optional pgpPrivateKey = Optional.empty(); + PGPPublicKeyEncryptedData publicKeyEncryptedData = null; + + Iterator encryptedDataItr = pgpEncryptedDataList.getEncryptedDataObjects(); + while (pgpPrivateKey.isEmpty() && encryptedDataItr.hasNext()) { + publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedDataItr.next(); + pgpPrivateKey = findSecretKey(publicKeyEncryptedData.getKeyID()); + } + + if (Objects.isNull(publicKeyEncryptedData)) { + throw new PGPException("Could not generate PGPPublicKeyEncryptedData object"); + } + + if (pgpPrivateKey.isEmpty()) { + throw new PGPException("Could not Extract private key"); + } + decrypt(pgpPrivateKey.get(), publicKeyEncryptedData, iteratorObj); } // Decrypts the given byte array of encrypted data using PGP decryption. @@ -125,48 +159,80 @@ public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { } } + public void decrypt(InputStream encryptedIn, BObject iteratorObj) throws PGPException, IOException { + decryptStream(encryptedIn, iteratorObj); + } + public void decrypt(InputStream encryptedIn, String outputPath) throws PGPException, IOException { try (OutputStream outputStream = Files.newOutputStream(Path.of(outputPath))) { decryptStream(encryptedIn, outputStream); } } - private static void decrypt(OutputStream clearOut, Optional pgpPrivateKey, + private static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { - 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); - } + PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(pgpPrivateKey); + 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"); } + } 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"); } } + } + // Perform the integrity check + if (publicKeyEncryptedData.isIntegrityProtected()) { + if (!publicKeyEncryptedData.verify()) { + throw new PGPException("Message failed integrity check"); + } + } + } + + private static void decrypt(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData, + BObject iteratorObj) 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) { // Perform the integrity check if (publicKeyEncryptedData.isIntegrityProtected()) { if (!publicKeyEncryptedData.verify()) { throw new PGPException("Message failed integrity check"); } } + iteratorObj.addNativeData(DECRYPTED_STREAM, pgpLiteralData.getDataStream()); + iteratorObj.addNativeData(COMPRESSED_PGP_STREAM, compressedDataStream); + iteratorObj.addNativeData(COMPRESSED_STREAM, decryptedCompressedIn); + } 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"); } } } 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 21373c1a..03baebb3 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 @@ -18,9 +18,17 @@ package io.ballerina.stdlib.crypto.nativeimpl; +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; import io.ballerina.runtime.api.values.BString; +import io.ballerina.stdlib.crypto.BallerinaInputStream; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; import io.ballerina.stdlib.crypto.PgpDecryptionGenerator; @@ -121,4 +129,27 @@ public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyP return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); } } + + public static Object decryptStreamPgp(Environment environment, BStream inputStream, BString privateKeyPath, + BArray passphrase) { + byte[] passphraseInBytes = passphrase.getBytes(); + byte[] privateKey; + try { + privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); + } catch (IOException e) { + return CryptoUtils.createError("Error occurred while reading private key: " + e.getMessage()); + } + + try (InputStream keyStream = new ByteArrayInputStream(privateKey)) { + InputStream cipherTextStream = new BallerinaInputStream(environment, inputStream); + PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); + BObject iteratorObj = ValueCreator.createObjectValue(ModuleUtils.getModule(), "StreamIterator"); + pgpDecryptionGenerator.decrypt(cipherTextStream, iteratorObj); + Type constrainedType = TypeCreator.createArrayType(PredefinedTypes.TYPE_BYTE); + return ValueCreator.createStreamValue(TypeCreator.createStreamType(constrainedType), + iteratorObj); + } 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/StreamUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java new file mode 100644 index 00000000..0949048e --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java @@ -0,0 +1,84 @@ +/* + * 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.nativeimpl; + +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.stdlib.crypto.CryptoUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_PGP_STREAM; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_STREAM; +import static io.ballerina.stdlib.crypto.Constants.DECRYPTED_STREAM; + +/** + * Provides functionality for stream operations. + * + * @since 2.8.0 + */ +public final class StreamUtils { + + private StreamUtils() { + } + + public static Object read(BObject iterator) { + Object stream = iterator.getNativeData(DECRYPTED_STREAM); + if (Objects.isNull(stream) || !(stream instanceof InputStream inputStream)) { + return CryptoUtils.createError("Stream is not available"); + } + try { + byte[] buffer = new byte[4096]; + int in = inputStream.read(buffer); + if (in == -1) { + return null; + } + if (in < buffer.length) { + byte[] temp = new byte[in]; + System.arraycopy(buffer, 0, temp, 0, in); + return ValueCreator.createArrayValue(temp); + } + return ValueCreator.createArrayValue(buffer); + } catch (IOException e) { + return CryptoUtils.createError("Error occurred while reading from the stream: " + e.getMessage()); + } + } + + public static Object closeStream(BObject iterator) { + Object result = closeNativeStream(iterator, DECRYPTED_STREAM); + // Ignore the errors occurred while closing the compressed streams. + closeNativeStream(iterator, COMPRESSED_PGP_STREAM); + closeNativeStream(iterator, COMPRESSED_STREAM); + return result; + } + + public static Object closeNativeStream(BObject iterator, String streamName) { + Object stream = iterator.getNativeData(streamName); + if (Objects.isNull(stream) || !(stream instanceof InputStream inputStream)) { + return CryptoUtils.createError("Stream is not available"); + } + try { + inputStream.close(); + } catch (IOException e) { + return CryptoUtils.createError("Error occurred while closing the stream: " + e.getMessage()); + } + return null; + } +} diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index e72d98dd..8cf90acd 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -16,5 +16,8 @@ ~ under the License. --> - + + + + From 7c584bb8a0af41131019deacf13f76446d18ac14 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 7 Oct 2024 13:28:37 +0530 Subject: [PATCH 17/44] Fix casting issue --- ballerina/stream_iterator.bal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ballerina/stream_iterator.bal b/ballerina/stream_iterator.bal index 2ca2cf99..cd36c28e 100644 --- a/ballerina/stream_iterator.bal +++ b/ballerina/stream_iterator.bal @@ -3,7 +3,7 @@ import ballerina/jballerina.java; class StreamIterator { boolean isClosed = false; - isolated function next() returns record {|byte[] value;|}|Error? { + public isolated function next() returns record {|byte[] value;|}|Error? { byte[]|Error? bytes = self.read(); if bytes is byte[] { return {value: bytes}; @@ -12,7 +12,7 @@ class StreamIterator { } } - isolated function close() returns Error? { + public isolated function close() returns Error? { if !self.isClosed { var closeResult = self.closeStream(); if closeResult is () { From d09e23548dc67fc52a07cf2baea6ba39263911c8 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 8 Oct 2024 11:21:47 +0530 Subject: [PATCH 18/44] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 1ce6ae2a..c0f9b2ba 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -21,23 +21,23 @@ path = "../native/build/libs/crypto-native-2.8.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcpkix-jdk18on" -version = "1.78" -path = "./lib/bcpkix-jdk18on-1.78.jar" +version = "1.78.1" +path = "./lib/bcpkix-jdk18on-1.78.1.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcprov-jdk18on" -version = "1.78" -path = "./lib/bcprov-jdk18on-1.78.jar" +version = "1.78.1" +path = "./lib/bcprov-jdk18on-1.78.1.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcutil-jdk18on" -version = "1.78" -path = "./lib/bcutil-jdk18on-1.78.jar" +version = "1.78.1" +path = "./lib/bcutil-jdk18on-1.78.1.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcpg-jdk18on" -version = "1.78" -path = "./lib/bcpg-jdk18on-1.78.jar" +version = "1.78.1" +path = "./lib/bcpg-jdk18on-1.78.1.jar" From 1126113556dd560c863ae70c99fa0f9cc626ff58 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 8 Oct 2024 11:24:05 +0530 Subject: [PATCH 19/44] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index c0f9b2ba..1ce6ae2a 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -21,23 +21,23 @@ path = "../native/build/libs/crypto-native-2.8.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcpkix-jdk18on" -version = "1.78.1" -path = "./lib/bcpkix-jdk18on-1.78.1.jar" +version = "1.78" +path = "./lib/bcpkix-jdk18on-1.78.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcprov-jdk18on" -version = "1.78.1" -path = "./lib/bcprov-jdk18on-1.78.1.jar" +version = "1.78" +path = "./lib/bcprov-jdk18on-1.78.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcutil-jdk18on" -version = "1.78.1" -path = "./lib/bcutil-jdk18on-1.78.1.jar" +version = "1.78" +path = "./lib/bcutil-jdk18on-1.78.jar" [[platform.java17.dependency]] groupId = "org.bouncycastle" artifactId = "bcpg-jdk18on" -version = "1.78.1" -path = "./lib/bcpg-jdk18on-1.78.1.jar" +version = "1.78" +path = "./lib/bcpg-jdk18on-1.78.jar" From ea708afda1558d38bf8f9e58217844c19c28abdc Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 09:39:10 +0530 Subject: [PATCH 20/44] Add stream pgp encrypt support --- ballerina/encrypt_decrypt.bal | 14 ++ ballerina/stream_iterator.bal | 33 ---- ballerina/stream_iterators.bal | 65 +++++++ .../stdlib/crypto/BallerinaInputStream.java | 92 +++++++--- .../io/ballerina/stdlib/crypto/Constants.java | 12 +- .../stdlib/crypto/PgpDecryptionGenerator.java | 18 +- .../stdlib/crypto/PgpEncryptionGenerator.java | 57 +++++- .../stdlib/crypto/nativeimpl/Decrypt.java | 29 ++-- .../stdlib/crypto/nativeimpl/Encrypt.java | 49 +++++- .../stdlib/crypto/nativeimpl/StreamUtils.java | 163 +++++++++++++++--- 10 files changed, 418 insertions(+), 114 deletions(-) delete mode 100644 ballerina/stream_iterator.bal create mode 100644 ballerina/stream_iterators.bal diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 6e1e4463..fa190967 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -276,6 +276,20 @@ public isolated function encryptPgpAsFile(string inputFilePath, string publicKey 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; +# Returns the PGP-encrypted stream of the content given in the input stream. +# ```ballerina +# stream inputStream = check io:fileReadBlocksAsStream("input.txt"); +# stream|Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); +# ``` +# +# + inputStream - The content to be encrypted as a stream +# + privateKeyPath - Path to the private key +# + return - Encrypted stream or else a `crypto:Error` if the key is invalid +public isolated function encryptStreamPgp(stream inputStream, string publicKeyPath, + *Options options) returns stream|Error = @java:Method { + '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(); diff --git a/ballerina/stream_iterator.bal b/ballerina/stream_iterator.bal deleted file mode 100644 index cd36c28e..00000000 --- a/ballerina/stream_iterator.bal +++ /dev/null @@ -1,33 +0,0 @@ -import ballerina/jballerina.java; - -class StreamIterator { - boolean isClosed = false; - - public isolated function next() returns record {|byte[] value;|}|Error? { - byte[]|Error? bytes = self.read(); - if bytes is byte[] { - return {value: bytes}; - } else { - return bytes; - } - } - - public isolated function close() returns Error? { - if !self.isClosed { - var closeResult = self.closeStream(); - if closeResult is () { - self.isClosed = true; - } - return closeResult; - } - return; - } - - isolated function read() returns byte[]|Error? = @java:Method { - 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" - } external; - - isolated function closeStream() returns Error? = @java:Method { - 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" - } external; -} diff --git a/ballerina/stream_iterators.bal b/ballerina/stream_iterators.bal new file mode 100644 index 00000000..df047117 --- /dev/null +++ b/ballerina/stream_iterators.bal @@ -0,0 +1,65 @@ +import ballerina/jballerina.java; + +class DecryptedStreamIterator { + boolean isClosed = false; + + public isolated function next() returns record {|byte[] value;|}|Error? { + byte[]|Error? bytes = self.readDecryptedStream(); + if bytes is byte[] { + return {value: bytes}; + } else { + return bytes; + } + } + + public isolated function close() returns Error? { + if !self.isClosed { + var closeResult = self.closeDecryptedStream(); + if closeResult is () { + self.isClosed = true; + } + return closeResult; + } + return; + } + + isolated function readDecryptedStream() returns byte[]|Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" + } external; + + isolated function closeDecryptedStream() returns Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" + } external; +} + +class EncryptedStreamIterator { + boolean isClosed = false; + + public isolated function next() returns record {|byte[] value;|}|Error? { + byte[]|Error? bytes = self.readEncryptedStream(); + if bytes is byte[] { + return {value: bytes}; + } else { + return bytes; + } + } + + public isolated function close() returns Error? { + if !self.isClosed { + var closeResult = self.closeEncryptedStream(); + if closeResult is () { + self.isClosed = true; + } + return closeResult; + } + return; + } + + isolated function readEncryptedStream() returns byte[]|Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" + } external; + + isolated function closeEncryptedStream() returns Error? = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.StreamUtils" + } external; +} diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java index 368b72e5..335f79bc 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java @@ -37,9 +37,23 @@ * @since 2.8.0 */ public class BallerinaInputStream extends InputStream { + public static final String BAL_STREAM_CLOSE = "close"; + public static final String STREAM_VALUE = "value"; + public static final String BAL_STREAM_NEXT = "next"; + + public static final String ERROR_OCCURRED_WHILE_CLOSING_THE_STREAM = "Error occurred while closing the stream"; + public static final String ERROR_OCCURRED_WHILE_READING_THE_STREAM = "Error occurred while reading the next " + + "element from the stream"; + public static final String INTERRUPTED_ERROR_WHILE_READING_THE_STREAM = ERROR_OCCURRED_WHILE_READING_THE_STREAM + + ": interrupted exception"; + public static final String ERR_MSG_FORMAT = "%s: %s"; + public static final String UNEXPECTED_TYPE_ERROR = ERROR_OCCURRED_WHILE_READING_THE_STREAM + + ": unexpected value type"; + private final Environment environment; private final BStream ballerinaStream; private ByteBuffer buffer = null; + private boolean endOfStream = false; public BallerinaInputStream(Environment environment, BStream ballerinaStream) { this.ballerinaStream = ballerinaStream; @@ -48,40 +62,29 @@ public BallerinaInputStream(Environment environment, BStream ballerinaStream) { @Override public int read() throws IOException { + if (endOfStream) { + return -1; + } if (Objects.isNull(buffer) || !buffer.hasRemaining()) { - Object nextElement = getNext(); - if (nextElement instanceof BError) { - throw new IOException(((BError) nextElement).getMessage()); - } - if (Objects.isNull(nextElement)) { + boolean result = pollNext(); + if (!result) { + endOfStream = true; return -1; } - if (nextElement instanceof BMap nextValue) { - Object nextBytes = nextValue.get(StringUtils.fromString("value")); - if (nextBytes instanceof BArray) { - buffer = ByteBuffer.wrap(((BArray) nextBytes).getBytes()); - } else { - throw new IOException("Error occurred while reading the next element from the stream: " + - "unexpected value type"); - } - } else { - throw new IOException("Error occurred while reading the next element from the stream: " + - "unexpected value type"); - } } return buffer.get() & 0xFF; } @Override - public void close() { - Object result = callBallerinaFunction("close", "Error occurred while closing the stream"); - if (result instanceof BError) { - throw new RuntimeException(((BError) result).getMessage()); + public void close() throws IOException { + Object result = callBallerinaFunction(BAL_STREAM_CLOSE, ERROR_OCCURRED_WHILE_CLOSING_THE_STREAM); + if (result instanceof BError bError) { + throw new IOException((bError).getMessage()); } } public Object getNext() { - return callBallerinaFunction("next", "Error occurred while reading the next element from the stream"); + return callBallerinaFunction(BAL_STREAM_NEXT, ERROR_OCCURRED_WHILE_READING_THE_STREAM); } private Object callBallerinaFunction(String functionName, String message) { @@ -94,8 +97,8 @@ private Object callBallerinaFunction(String functionName, String message) { try { countDownLatch.await(); } catch (InterruptedException exception) { - return CryptoUtils.createError("Error occurred while reading the next element from the stream: " + - "interrupted exception"); + Thread.currentThread().interrupt(); + return CryptoUtils.createError(INTERRUPTED_ERROR_WHILE_READING_THE_STREAM); } return nextResult[0]; } @@ -111,10 +114,49 @@ public void notifySuccess(Object result) { @Override public void notifyFailure(BError bError) { - BError error = CryptoUtils.createError(String.format("%s: %s", message, bError.getMessage())); + BError error = CryptoUtils.createError(String.format(ERR_MSG_FORMAT, message, bError.getMessage())); nextResult[0] = error; countDownLatch.countDown(); } } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (endOfStream) { + return -1; + } + if (Objects.isNull(buffer) || !buffer.hasRemaining()) { + boolean result = pollNext(); + if (!result) { + endOfStream = true; + return -1; + } + } + int remaining = buffer.remaining(); + int readLength = Math.min(remaining, len); + buffer.get(b, off, readLength); + return readLength; + } + + private boolean pollNext() throws IOException { + Object nextElement = getNext(); + if (nextElement instanceof BError bError) { + throw new IllegalStateException((bError).getMessage()); + } + if (Objects.isNull(nextElement)) { + return false; + } + if (nextElement instanceof BMap nextValue) { + Object nextBytes = nextValue.get(StringUtils.fromString(STREAM_VALUE)); + if (nextBytes instanceof BArray nextBytesArray) { + buffer = ByteBuffer.wrap((nextBytesArray).getBytes()); + } else { + throw new IOException(UNEXPECTED_TYPE_ERROR); + } + } else { + throw new IOException(UNEXPECTED_TYPE_ERROR); + } + return true; + } } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java index b084fcaf..749ed207 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java @@ -125,7 +125,13 @@ private Constants() {} public static final String AES = "AES"; public static final String RSA = "RSA"; - public static final String DECRYPTED_STREAM = "INPUT_STREAM"; - public static final String COMPRESSED_PGP_STREAM = "COMPRESSED_PGP_STREAM"; - public static final String COMPRESSED_STREAM = "COMPRESSED_STREAM"; + public static final String COMPRESSED_DATA_STREAM = "COMPRESSED_DATA_STREAM"; + public static final String DATA_STREAM = "DATA_STREAM"; + public static final String TARGET_STREAM = "TARGET_STREAM"; + public static final String ENCRYPTED_OUTPUT_STREAM = "ENCRYPTED_OUTPUT_STREAM"; + public static final String INPUT_STREAM_TO_ENCRYPT = "INPUT_STREAM_TO_ENCRYPT"; + public static final String PIPED_INPUT_STREAM = "PIPED_INPUT_STREAM"; + public static final String PIPED_OUTPUT_STREAM = "PIPED_OUTPUT_STREAM"; + public static final String ENCRYPTION_STARTED = "ENCRYPTION_STARTED"; + public static final String COMPRESSED_DATA_GENERATOR = "COMPRESSED_DATA_GENERATOR"; } 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 a4fe3c93..5bd939a6 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -51,9 +51,9 @@ import java.util.Objects; import java.util.Optional; -import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_PGP_STREAM; -import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_STREAM; -import static io.ballerina.stdlib.crypto.Constants.DECRYPTED_STREAM; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_STREAM; +import static io.ballerina.stdlib.crypto.Constants.DATA_STREAM; +import static io.ballerina.stdlib.crypto.Constants.TARGET_STREAM; /** * Provides functionality for PGP decryption operations. @@ -121,7 +121,7 @@ private void decryptStream(InputStream encryptedIn, OutputStream clearOut) decrypt(clearOut, pgpPrivateKey.get(), publicKeyEncryptedData); } - private void decryptStream(InputStream encryptedIn, BObject iteratorObj) throws PGPException, IOException { + public void decryptStream(InputStream encryptedIn, BObject iteratorObj) throws PGPException, IOException { // Remove armour and return the underlying binary encrypted stream encryptedIn = PGPUtil.getDecoderStream(encryptedIn); JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(encryptedIn); @@ -159,10 +159,6 @@ public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { } } - public void decrypt(InputStream encryptedIn, BObject iteratorObj) throws PGPException, IOException { - decryptStream(encryptedIn, iteratorObj); - } - public void decrypt(InputStream encryptedIn, String outputPath) throws PGPException, IOException { try (OutputStream outputStream = Files.newOutputStream(Path.of(outputPath))) { decryptStream(encryptedIn, outputStream); @@ -226,9 +222,9 @@ private static void decrypt(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedDa throw new PGPException("Message failed integrity check"); } } - iteratorObj.addNativeData(DECRYPTED_STREAM, pgpLiteralData.getDataStream()); - iteratorObj.addNativeData(COMPRESSED_PGP_STREAM, compressedDataStream); - iteratorObj.addNativeData(COMPRESSED_STREAM, decryptedCompressedIn); + iteratorObj.addNativeData(TARGET_STREAM, pgpLiteralData.getDataStream()); + iteratorObj.addNativeData(COMPRESSED_DATA_STREAM, compressedDataStream); + iteratorObj.addNativeData(DATA_STREAM, decryptedCompressedIn); } else if (message instanceof PGPOnePassSignatureList) { throw new PGPException("Encrypted message contains a signed message not literal data"); } else { 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 6774966c..99bfa05e 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -18,6 +18,7 @@ package io.ballerina.stdlib.crypto; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.values.BObject; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; @@ -38,6 +39,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.SecureRandom; @@ -50,6 +53,13 @@ import java.util.Objects; import java.util.Optional; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_GENERATOR; +import static io.ballerina.stdlib.crypto.Constants.DATA_STREAM; +import static io.ballerina.stdlib.crypto.Constants.ENCRYPTED_OUTPUT_STREAM; +import static io.ballerina.stdlib.crypto.Constants.PIPED_INPUT_STREAM; +import static io.ballerina.stdlib.crypto.Constants.PIPED_OUTPUT_STREAM; +import static io.ballerina.stdlib.crypto.Constants.TARGET_STREAM; + /** * Provides functionality for PGP encryption operations. * @@ -67,7 +77,7 @@ public class PgpEncryptionGenerator { private final int symmetricKeyAlgorithm; private final boolean armor; private final boolean withIntegrityCheck; - private static final int BUFFER_SIZE = 8192; + public static final int BUFFER_SIZE = 8192; // The constructor of the PGP encryption generator. public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorithm, boolean armor, @@ -80,8 +90,7 @@ public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorith private void encryptStream(OutputStream encryptOut, InputStream clearIn, InputStream publicKeyIn) throws IOException, PGPException { - PGPCompressedDataGenerator compressedDataGenerator = - new PGPCompressedDataGenerator(compressionAlgorithm); + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm); PGPEncryptedDataGenerator pgpEncryptedDataGenerator = new PGPEncryptedDataGenerator( // Configure the encrypted data generator new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) @@ -103,6 +112,36 @@ private void encryptStream(OutputStream encryptOut, InputStream clearIn, InputSt encryptOut.close(); } + public void encryptStream(InputStream publicKeyIn, BObject iteratorObj) + throws IOException, PGPException { + OutputStream encryptOut = new PipedOutputStream(); + iteratorObj.addNativeData(PIPED_OUTPUT_STREAM, encryptOut); + PipedInputStream pipedInputStream = new PipedInputStream((PipedOutputStream) encryptOut, + PgpEncryptionGenerator.BUFFER_SIZE); + iteratorObj.addNativeData(PIPED_INPUT_STREAM, pipedInputStream); + 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); + } + + iteratorObj.addNativeData(ENCRYPTED_OUTPUT_STREAM, encryptOut); + OutputStream cipherOutStream = pgpEncryptedDataGenerator.open(encryptOut, new byte[BUFFER_SIZE]); + OutputStream compressedOutStream = compressedDataGenerator.open(cipherOutStream); + iteratorObj.addNativeData(DATA_STREAM, cipherOutStream); + iteratorObj.addNativeData(COMPRESSED_DATA_GENERATOR, compressedDataGenerator); + copyAsLiteralData(compressedOutStream, iteratorObj); + } + // Encrypts the given byte array of plain text data using PGP encryption. public Object encrypt(byte[] clearData, InputStream publicKeyIn) throws PGPException, IOException { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(clearData); @@ -136,9 +175,9 @@ private static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOEx private static void copyAsLiteralData(OutputStream outputStream, InputStream in) throws IOException { PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); - byte[] buff = new byte[PgpEncryptionGenerator.BUFFER_SIZE]; + byte[] buff = new byte[BUFFER_SIZE]; try (OutputStream pOut = lData.open(outputStream, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, - Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)), new byte[PgpEncryptionGenerator.BUFFER_SIZE]); + Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)), new byte[BUFFER_SIZE]); InputStream inputStream = in) { int len; @@ -150,6 +189,14 @@ private static void copyAsLiteralData(OutputStream outputStream, InputStream in) } } + private static void copyAsLiteralData(OutputStream outputStream, BObject iteratorObj) + throws IOException { + PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); + OutputStream pOut = lData.open(outputStream, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, + Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)), new byte[BUFFER_SIZE]); + iteratorObj.addNativeData(TARGET_STREAM, pOut); + } + private static Optional extractPgpKeyFromRing(PGPPublicKeyRing pgpPublicKeyRing) { for (PGPPublicKey publicKey : pgpPublicKeyRing) { if (publicKey.isEncryptionKey()) { 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 03baebb3..b433372c 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 @@ -50,6 +50,10 @@ */ public class Decrypt { + public static final String ERROR_OCCURRED_WHILE_PGP_DECRYPT = "Error occurred while PGP decrypt: "; + public static final String ERROR_OCCURRED_WHILE_READING_PRIVATE_KEY = "Error occurred while reading private key: "; + public static final String UNINITIALIZED_PRIVATE_PUBLIC_KEY = "Uninitialized private/public key."; + private Decrypt() {} public static Object decryptAesCbc(BArray inputValue, BArray keyValue, BArray ivValue, Object padding) { @@ -85,7 +89,7 @@ public static Object decryptRsaEcb(BArray inputValue, Object keys, Object paddin } else if (keyMap.getNativeData(Constants.NATIVE_DATA_PUBLIC_KEY) != null) { key = (PublicKey) keyMap.getNativeData(Constants.NATIVE_DATA_PUBLIC_KEY); } else { - return CryptoUtils.createError("Uninitialized private/public key."); + return CryptoUtils.createError(UNINITIALIZED_PRIVATE_PUBLIC_KEY); } return CryptoUtils.rsaEncryptDecrypt(CryptoUtils.CipherMode.DECRYPT, Constants.ECB, padding.toString(), key, input, null, -1); @@ -98,14 +102,14 @@ public static Object decryptPgp(BArray cipherTextValue, BString privateKeyPath, try { privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading private key: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_READING_PRIVATE_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()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_DECRYPT + e.getMessage()); } } @@ -116,7 +120,7 @@ public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyP try { privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading private key: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_READING_PRIVATE_KEY + e.getMessage()); } try (InputStream keyStream = new ByteArrayInputStream(privateKey); @@ -126,30 +130,29 @@ public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyP pgpDecryptionGenerator.decrypt(cipherTextStream, outputFilePath.getValue()); return null; } catch (IOException | PGPException e) { - return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_DECRYPT + e.getMessage()); } } - public static Object decryptStreamPgp(Environment environment, BStream inputStream, BString privateKeyPath, + public static Object decryptStreamPgp(Environment environment, BStream inputBalStream, BString privateKeyPath, BArray passphrase) { byte[] passphraseInBytes = passphrase.getBytes(); byte[] privateKey; try { privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading private key: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_READING_PRIVATE_KEY + e.getMessage()); } try (InputStream keyStream = new ByteArrayInputStream(privateKey)) { - InputStream cipherTextStream = new BallerinaInputStream(environment, inputStream); + InputStream cipherTextStream = new BallerinaInputStream(environment, inputBalStream); PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); - BObject iteratorObj = ValueCreator.createObjectValue(ModuleUtils.getModule(), "StreamIterator"); - pgpDecryptionGenerator.decrypt(cipherTextStream, iteratorObj); + BObject iteratorObj = ValueCreator.createObjectValue(ModuleUtils.getModule(), "DecryptedStreamIterator"); + pgpDecryptionGenerator.decryptStream(cipherTextStream, iteratorObj); Type constrainedType = TypeCreator.createArrayType(PredefinedTypes.TYPE_BYTE); - return ValueCreator.createStreamValue(TypeCreator.createStreamType(constrainedType), - iteratorObj); + return ValueCreator.createStreamValue(TypeCreator.createStreamType(constrainedType), iteratorObj); } catch (IOException | PGPException e) { - return CryptoUtils.createError("Error occurred while PGP decrypt: " + e.getMessage()); + 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 2f4dc482..0ded0eab 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,10 +18,18 @@ package io.ballerina.stdlib.crypto.nativeimpl; +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.Type; 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.BObject; +import io.ballerina.runtime.api.values.BStream; import io.ballerina.runtime.api.values.BString; +import io.ballerina.stdlib.crypto.BallerinaInputStream; import io.ballerina.stdlib.crypto.Constants; import io.ballerina.stdlib.crypto.CryptoUtils; import io.ballerina.stdlib.crypto.PgpEncryptionGenerator; @@ -36,6 +44,9 @@ import java.security.PrivateKey; import java.security.PublicKey; +import static io.ballerina.stdlib.crypto.Constants.ENCRYPTION_STARTED; +import static io.ballerina.stdlib.crypto.Constants.INPUT_STREAM_TO_ENCRYPT; + /** * Extern functions ballerina encrypt algorithms. * @@ -47,6 +58,8 @@ public class Encrypt { 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"); + public static final String ERROR_OCCURRED_WHILE_READING_PUBLIC_KEY = "Error occurred while reading public key: "; + public static final String ERROR_OCCURRED_WHILE_PGP_ENCRYPT = "Error occurred while PGP encrypt: "; private Encrypt() {} @@ -101,7 +114,7 @@ public static Object encryptPgp(BArray plainTextValue, BString publicKeyPath, BM try { publicKey = Files.readAllBytes(Path.of(publicKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading public key: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_READING_PUBLIC_KEY + e.getMessage()); } try (InputStream publicKeyStream = new ByteArrayInputStream(publicKey)) { @@ -113,7 +126,7 @@ public static Object encryptPgp(BArray plainTextValue, BString publicKeyPath, BM ); return pgpEncryptionGenerator.encrypt(plainText, publicKeyStream); } catch (IOException | PGPException e) { - return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_ENCRYPT + e.getMessage()); } } @@ -123,7 +136,7 @@ public static Object encryptPgpAsFile(BString inputFilePath, BString publicKeyPa try { publicKey = Files.readAllBytes(Path.of(publicKeyPath.toString())); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading public key: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_READING_PUBLIC_KEY + e.getMessage()); } try (InputStream publicKeyStream = new ByteArrayInputStream(publicKey); @@ -138,7 +151,35 @@ public static Object encryptPgpAsFile(BString inputFilePath, BString publicKeyPa pgpEncryptionGenerator.encrypt(inputStream, publicKeyStream, outputFilePath.getValue()); return null; } catch (IOException | PGPException e) { - return CryptoUtils.createError("Error occurred while PGP encrypt: " + e.getMessage()); + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_ENCRYPT + e.getMessage()); + } + } + + public static Object encryptStreamPgp(Environment environment, BStream inputBalStream, BString publicKeyPath, + BMap options) { + 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)) { + InputStream inputStream = new BallerinaInputStream(environment, inputBalStream); + 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()) + ); + BObject iteratorObj = ValueCreator.createObjectValue(ModuleUtils.getModule(), "EncryptedStreamIterator"); + iteratorObj.addNativeData(ENCRYPTION_STARTED, false); + iteratorObj.addNativeData(INPUT_STREAM_TO_ENCRYPT, inputStream); + pgpEncryptionGenerator.encryptStream(publicKeyStream, iteratorObj); + Type constrainedType = TypeCreator.createArrayType(PredefinedTypes.TYPE_BYTE); + return ValueCreator.createStreamValue(TypeCreator.createStreamType(constrainedType), iteratorObj); + } catch (IOException | PGPException e) { + return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_ENCRYPT + e.getMessage()); } } } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java index 0949048e..4282f986 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java @@ -18,16 +18,29 @@ package io.ballerina.stdlib.crypto.nativeimpl; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; import io.ballerina.runtime.api.values.BObject; import io.ballerina.stdlib.crypto.CryptoUtils; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; import java.util.Objects; -import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_PGP_STREAM; -import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_STREAM; -import static io.ballerina.stdlib.crypto.Constants.DECRYPTED_STREAM; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_GENERATOR; +import static io.ballerina.stdlib.crypto.Constants.ENCRYPTED_OUTPUT_STREAM; +import static io.ballerina.stdlib.crypto.Constants.ENCRYPTION_STARTED; +import static io.ballerina.stdlib.crypto.Constants.INPUT_STREAM_TO_ENCRYPT; +import static io.ballerina.stdlib.crypto.Constants.PIPED_INPUT_STREAM; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_STREAM; +import static io.ballerina.stdlib.crypto.Constants.DATA_STREAM; +import static io.ballerina.stdlib.crypto.Constants.PIPED_OUTPUT_STREAM; +import static io.ballerina.stdlib.crypto.Constants.TARGET_STREAM; +import static io.ballerina.stdlib.crypto.PgpEncryptionGenerator.BUFFER_SIZE; /** * Provides functionality for stream operations. @@ -36,16 +49,24 @@ */ public final class StreamUtils { + public static final String STREAM_NOT_AVAILABLE = "Stream is not available"; + public static final String ERROR_OCCURRED_WHILE_CLOSING_THE_STREAM = "Error occurred while closing the stream: %s"; + public static final String ERROR_OCCURRED_WHILE_CLOSING_THE_GENERATOR = "Error occurred while closing the " + + "generator: %s"; + public static final String ERROR_OCCURRED_WHILE_READING_THE_STREAM = "Error occurred while reading from the " + + "stream: %s"; + public static final String NATIVE_DATA_NOT_AVAILABLE_ERROR = "%s is not available"; + private StreamUtils() { } - public static Object read(BObject iterator) { - Object stream = iterator.getNativeData(DECRYPTED_STREAM); + public static Object readDecryptedStream(BObject iterator) { + Object stream = iterator.getNativeData(TARGET_STREAM); if (Objects.isNull(stream) || !(stream instanceof InputStream inputStream)) { - return CryptoUtils.createError("Stream is not available"); + return CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, TARGET_STREAM)); } try { - byte[] buffer = new byte[4096]; + byte[] buffer = new byte[BUFFER_SIZE]; int in = inputStream.read(buffer); if (in == -1) { return null; @@ -61,24 +82,126 @@ public static Object read(BObject iterator) { } } - public static Object closeStream(BObject iterator) { - Object result = closeNativeStream(iterator, DECRYPTED_STREAM); - // Ignore the errors occurred while closing the compressed streams. - closeNativeStream(iterator, COMPRESSED_PGP_STREAM); - closeNativeStream(iterator, COMPRESSED_STREAM); - return result; + public static Object readEncryptedStream(BObject iterator) { + NativeData nativeData = getNativeData(iterator); + + try { + if (Boolean.FALSE.equals(nativeData.encryptionStarted())) { + iterator.addNativeData(ENCRYPTION_STARTED, true); + Thread writer = new Thread(() -> { + try { + writeToOutStream(iterator, nativeData.inputStream(), nativeData.outputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + writer.start(); + } + return readFromPipedStream(nativeData.pipedInStream()); + } catch (IOException e) { + return CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_READING_THE_STREAM, e.getMessage())); + } catch (BError e) { + return e; + } } - public static Object closeNativeStream(BObject iterator, String streamName) { - Object stream = iterator.getNativeData(streamName); - if (Objects.isNull(stream) || !(stream instanceof InputStream inputStream)) { - return CryptoUtils.createError("Stream is not available"); + private static NativeData getNativeData(BObject iterator) { + Object inputStreamToEncrypt = iterator.getNativeData(INPUT_STREAM_TO_ENCRYPT); + if (Objects.isNull(inputStreamToEncrypt) || !(inputStreamToEncrypt instanceof InputStream inputStream)) { + throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, INPUT_STREAM_TO_ENCRYPT)); + } + + Object targetStream = iterator.getNativeData(TARGET_STREAM); + if (Objects.isNull(targetStream) || !(targetStream instanceof OutputStream outputStream)) { + throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, TARGET_STREAM)); + } + + Object pipelinedInputStream = iterator.getNativeData(PIPED_INPUT_STREAM); + if (Objects.isNull(pipelinedInputStream) || + !(pipelinedInputStream instanceof PipedInputStream pipedInStream)) { + throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, PIPED_INPUT_STREAM)); + } + + Object encryptionStartedObj = iterator.getNativeData(ENCRYPTION_STARTED); + if (Objects.isNull(encryptionStartedObj) || !(encryptionStartedObj instanceof Boolean encryptionStarted)) { + throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, ENCRYPTION_STARTED)); + } + return new NativeData(inputStream, outputStream, pipedInStream, encryptionStarted); + } + + private record NativeData(InputStream inputStream, OutputStream outputStream, PipedInputStream pipedInStream, + Boolean encryptionStarted) { + } + + private static BArray readFromPipedStream(PipedInputStream pipedInStream) throws IOException { + byte[] pipelinedBuffer = new byte[BUFFER_SIZE]; + int pipelinedIn = pipedInStream.read(pipelinedBuffer); + if (pipelinedIn == -1) { + return null; + } + if (pipelinedIn < pipelinedBuffer.length) { + byte[] temp = new byte[pipelinedIn]; + System.arraycopy(pipelinedBuffer, 0, temp, 0, pipelinedIn); + return ValueCreator.createArrayValue(temp); + } + return ValueCreator.createArrayValue(pipelinedBuffer); + } + + private static void writeToOutStream(BObject iterator, InputStream inputStream, OutputStream outputStream) + throws IOException { + byte[] inputBuffer = new byte[BUFFER_SIZE]; + int len; + while ((len = inputStream.read(inputBuffer)) > 0) { + outputStream.write(inputBuffer, 0, len); + } + closeEncryptedSourceStreams(iterator); + } + + public static void closeDecryptedStream(BObject iterator) throws BError { + closeNativeStream(iterator, TARGET_STREAM); + closeNativeStream(iterator, COMPRESSED_DATA_STREAM); + closeNativeStream(iterator, DATA_STREAM); + } + + public static void closeEncryptedStream(BObject iterator) throws BError { + closeNativeStream(iterator, TARGET_STREAM); + closeDataGenerator(iterator); + closeNativeStream(iterator, DATA_STREAM); + closeNativeStream(iterator, ENCRYPTED_OUTPUT_STREAM); + closeNativeStream(iterator, PIPED_OUTPUT_STREAM); + closeNativeStream(iterator, PIPED_INPUT_STREAM); + } + + public static void closeEncryptedSourceStreams(BObject iterator) throws BError { + closeNativeStream(iterator, INPUT_STREAM_TO_ENCRYPT); + closeNativeStream(iterator, TARGET_STREAM); + closeDataGenerator(iterator); + closeNativeStream(iterator, DATA_STREAM); + closeNativeStream(iterator, ENCRYPTED_OUTPUT_STREAM); + closeNativeStream(iterator, PIPED_OUTPUT_STREAM); + } + + public static void closeNativeStream(BObject iterator, String streamName) throws BError { + Object streamObj = iterator.getNativeData(streamName); + if (Objects.isNull(streamObj) || !(streamObj instanceof Closeable stream)) { + throw CryptoUtils.createError(STREAM_NOT_AVAILABLE); + } + try { + stream.close(); + } catch (IOException e) { + throw CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_CLOSING_THE_STREAM, e.getMessage())); + } + } + + public static void closeDataGenerator(BObject iterator) throws BError { + Object generatorObj = iterator.getNativeData(COMPRESSED_DATA_GENERATOR); + if (Objects.isNull(generatorObj) || !(generatorObj instanceof PGPCompressedDataGenerator generator)) { + throw CryptoUtils.createError(STREAM_NOT_AVAILABLE); } try { - inputStream.close(); + generator.close(); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while closing the stream: " + e.getMessage()); + throw CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_CLOSING_THE_GENERATOR, e.getMessage())); } - return null; } } From 7094e49ebf4679e1d68b05612bb329be681a24fb Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 11:41:15 +0530 Subject: [PATCH 21/44] Replace PipedStreams with a custom implementation --- .../io/ballerina/stdlib/crypto/Constants.java | 2 +- .../stdlib/crypto/PgpEncryptionGenerator.java | 8 +-- .../stdlib/crypto/SequentialBufferedPipe.java | 72 +++++++++++++++++++ .../stdlib/crypto/nativeimpl/Encrypt.java | 4 +- .../stdlib/crypto/nativeimpl/StreamUtils.java | 43 +++++------ 5 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java index 749ed207..279bff87 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java @@ -132,6 +132,6 @@ private Constants() {} public static final String INPUT_STREAM_TO_ENCRYPT = "INPUT_STREAM_TO_ENCRYPT"; public static final String PIPED_INPUT_STREAM = "PIPED_INPUT_STREAM"; public static final String PIPED_OUTPUT_STREAM = "PIPED_OUTPUT_STREAM"; - public static final String ENCRYPTION_STARTED = "ENCRYPTION_STARTED"; + public static final String END_OF_INPUT_STREAM = "END_OF_INPUT_STREAM"; public static final String COMPRESSED_DATA_GENERATOR = "COMPRESSED_DATA_GENERATOR"; } 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 99bfa05e..94db6961 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -39,8 +39,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.SecureRandom; @@ -114,10 +112,10 @@ private void encryptStream(OutputStream encryptOut, InputStream clearIn, InputSt public void encryptStream(InputStream publicKeyIn, BObject iteratorObj) throws IOException, PGPException { - OutputStream encryptOut = new PipedOutputStream(); + SequentialBufferedPipe pipe = new SequentialBufferedPipe(); + OutputStream encryptOut = pipe.getOutputStream(); iteratorObj.addNativeData(PIPED_OUTPUT_STREAM, encryptOut); - PipedInputStream pipedInputStream = new PipedInputStream((PipedOutputStream) encryptOut, - PgpEncryptionGenerator.BUFFER_SIZE); + InputStream pipedInputStream = pipe.getInputStream(); iteratorObj.addNativeData(PIPED_INPUT_STREAM, pipedInputStream); PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm); PGPEncryptedDataGenerator pgpEncryptedDataGenerator = new PGPEncryptedDataGenerator( diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java b/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java new file mode 100644 index 00000000..c07d9675 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java @@ -0,0 +1,72 @@ +package io.ballerina.stdlib.crypto; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.Queue; + +/** + * Represents a pipe that can be used to connect an output stream to an input stream. + * This Pipe implementation assumes the output stream write and input stream read operations are done + * sequentially in the same thread and the output stream should be closed after writing is done. + * + * @since 2.8.0 + */ +public class SequentialBufferedPipe { + + Queue buffer = new LinkedList<>(); + boolean outputClosed = false; + + public InputStream getInputStream() { + return new InputStream() { + @Override + public int read() { + if (buffer.isEmpty()) { + if (outputClosed) { + return -1; + } + // This should not be reached with respect to the assumption + return 0; + } + return buffer.poll() & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) { + if (buffer.isEmpty()) { + if (outputClosed) { + return -1; + } + return 0; + } + int i = 0; + while (i < len && !buffer.isEmpty()) { + b[off + i] = buffer.poll(); + i++; + } + return i; + } + }; + } + + public OutputStream getOutputStream() { + return new OutputStream() { + @Override + public void write(int b) { + buffer.add((byte) b); + } + + @Override + public void write(byte[] b, int off, int len) { + for (int i = off; i < off + len; i++) { + buffer.add(b[i]); + } + } + + @Override + public void close() { + outputClosed = true; + } + }; + } +} 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 0ded0eab..63a05384 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 @@ -44,7 +44,7 @@ import java.security.PrivateKey; import java.security.PublicKey; -import static io.ballerina.stdlib.crypto.Constants.ENCRYPTION_STARTED; +import static io.ballerina.stdlib.crypto.Constants.END_OF_INPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.INPUT_STREAM_TO_ENCRYPT; /** @@ -173,7 +173,7 @@ public static Object encryptStreamPgp(Environment environment, BStream inputBalS Boolean.parseBoolean(options.get(WITH_INTEGRITY_CHECK).toString()) ); BObject iteratorObj = ValueCreator.createObjectValue(ModuleUtils.getModule(), "EncryptedStreamIterator"); - iteratorObj.addNativeData(ENCRYPTION_STARTED, false); + iteratorObj.addNativeData(END_OF_INPUT_STREAM, false); iteratorObj.addNativeData(INPUT_STREAM_TO_ENCRYPT, inputStream); pgpEncryptionGenerator.encryptStream(publicKeyStream, iteratorObj); Type constrainedType = TypeCreator.createArrayType(PredefinedTypes.TYPE_BYTE); diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java index 4282f986..ff9c0cbd 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java @@ -28,12 +28,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PipedInputStream; import java.util.Objects; import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_GENERATOR; import static io.ballerina.stdlib.crypto.Constants.ENCRYPTED_OUTPUT_STREAM; -import static io.ballerina.stdlib.crypto.Constants.ENCRYPTION_STARTED; +import static io.ballerina.stdlib.crypto.Constants.END_OF_INPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.INPUT_STREAM_TO_ENCRYPT; import static io.ballerina.stdlib.crypto.Constants.PIPED_INPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_STREAM; @@ -86,16 +85,8 @@ public static Object readEncryptedStream(BObject iterator) { NativeData nativeData = getNativeData(iterator); try { - if (Boolean.FALSE.equals(nativeData.encryptionStarted())) { - iterator.addNativeData(ENCRYPTION_STARTED, true); - Thread writer = new Thread(() -> { - try { - writeToOutStream(iterator, nativeData.inputStream(), nativeData.outputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - writer.start(); + if (Boolean.FALSE.equals(nativeData.endOfStream())) { + writeToOutStream(iterator, nativeData.inputStream(), nativeData.outputStream()); } return readFromPipedStream(nativeData.pipedInStream()); } catch (IOException e) { @@ -117,23 +108,22 @@ private static NativeData getNativeData(BObject iterator) { } Object pipelinedInputStream = iterator.getNativeData(PIPED_INPUT_STREAM); - if (Objects.isNull(pipelinedInputStream) || - !(pipelinedInputStream instanceof PipedInputStream pipedInStream)) { + if (Objects.isNull(pipelinedInputStream) || !(pipelinedInputStream instanceof InputStream pipedInStream)) { throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, PIPED_INPUT_STREAM)); } - Object encryptionStartedObj = iterator.getNativeData(ENCRYPTION_STARTED); - if (Objects.isNull(encryptionStartedObj) || !(encryptionStartedObj instanceof Boolean encryptionStarted)) { - throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, ENCRYPTION_STARTED)); + Object endOfInputStream = iterator.getNativeData(END_OF_INPUT_STREAM); + if (Objects.isNull(endOfInputStream) || !(endOfInputStream instanceof Boolean endOfStream)) { + throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, END_OF_INPUT_STREAM)); } - return new NativeData(inputStream, outputStream, pipedInStream, encryptionStarted); + return new NativeData(inputStream, outputStream, pipedInStream, endOfStream); } - private record NativeData(InputStream inputStream, OutputStream outputStream, PipedInputStream pipedInStream, - Boolean encryptionStarted) { + private record NativeData(InputStream inputStream, OutputStream outputStream, InputStream pipedInStream, + Boolean endOfStream) { } - private static BArray readFromPipedStream(PipedInputStream pipedInStream) throws IOException { + private static BArray readFromPipedStream(InputStream pipedInStream) throws IOException { byte[] pipelinedBuffer = new byte[BUFFER_SIZE]; int pipelinedIn = pipedInStream.read(pipelinedBuffer); if (pipelinedIn == -1) { @@ -150,11 +140,14 @@ private static BArray readFromPipedStream(PipedInputStream pipedInStream) throws private static void writeToOutStream(BObject iterator, InputStream inputStream, OutputStream outputStream) throws IOException { byte[] inputBuffer = new byte[BUFFER_SIZE]; - int len; - while ((len = inputStream.read(inputBuffer)) > 0) { - outputStream.write(inputBuffer, 0, len); + int result = inputStream.read(inputBuffer); + if (result == -1) { + iterator.addNativeData(END_OF_INPUT_STREAM, true); + closeEncryptedSourceStreams(iterator); + } + if (result > 0) { + outputStream.write(inputBuffer, 0, result); } - closeEncryptedSourceStreams(iterator); } public static void closeDecryptedStream(BObject iterator) throws BError { From b635c453c2bd59c2959be48717570d63217b4fc5 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 12:07:55 +0530 Subject: [PATCH 22/44] Remove PGP file APIs --- ballerina/encrypt_decrypt.bal | 33 ------------------- .../stdlib/crypto/PgpDecryptionGenerator.java | 8 ----- .../stdlib/crypto/PgpEncryptionGenerator.java | 9 ----- .../stdlib/crypto/nativeimpl/Decrypt.java | 21 ------------ .../stdlib/crypto/nativeimpl/Encrypt.java | 25 -------------- 5 files changed, 96 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index fa190967..ba52052f 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -260,22 +260,6 @@ public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Opt 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; -# Writes the PGP-encrypted value of the content given in the input file to a file specified by the output file path. -# If the output file already exists, it will be overwritten. -# ```ballerina -# check crypto:encryptPgpAsFile("input.txt", "public_key.asc", "output.txt"); -# ``` -# -# + inputFilePath - Path to the input file -# + publicKeyPath - Path to the public key -# + outputFilePath - Path to the output file -# + options - PGP encryption options -# + return - A `crypto:Error` will be returned if the process fails -public isolated function encryptPgpAsFile(string inputFilePath, string publicKeyPath, string outputFilePath, - *Options options) returns Error? = @java:Method { - 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" -} external; - # Returns the PGP-encrypted stream of the content given in the input stream. # ```ballerina # stream inputStream = check io:fileReadBlocksAsStream("input.txt"); @@ -309,23 +293,6 @@ public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, by 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; -# Writes the PGP-decrypted value of the content given in the input file to a file specified by the output file path. -# If the output file already exists, it will be overwritten. -# ```ballerina -# byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); -# check crypto:decryptPgpAsFile("input.txt", "private_key.asc", passphrase, "output.txt"); -# ``` -# -# + inputFilePath - Path to the input file -# + privateKeyPath - Path to the private key -# + passphrase - passphrase of the private key -# + outputFilePath - Path to the output file -# + return - A `crypto:Error` will be returned if the process fails -public isolated function decryptPgpAsFile(string inputFilePath, string privateKeyPath, byte[] passphrase, - string outputFilePath) returns Error? = @java:Method { - 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" -} external; - # Returns the PGP-decrypted stream of the content given in the input stream. # ```ballerina # byte[] passphrase = check io:fileReadBytes("pass_phrase.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 5bd939a6..0613d1fc 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -44,8 +44,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.Security; import java.util.Iterator; import java.util.Objects; @@ -159,12 +157,6 @@ public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { } } - public void decrypt(InputStream encryptedIn, String outputPath) throws PGPException, IOException { - try (OutputStream outputStream = Files.newOutputStream(Path.of(outputPath))) { - decryptStream(encryptedIn, outputStream); - } - } - private static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() 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 94db6961..f2145c75 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -39,8 +39,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.SecureRandom; import java.security.Security; import java.time.LocalDateTime; @@ -149,13 +147,6 @@ public Object encrypt(byte[] clearData, InputStream publicKeyIn) throws PGPExcep } } - public void encrypt(InputStream inputStream, InputStream publicKeyIn, String outputPath) - throws PGPException, IOException { - try (OutputStream outputStream = Files.newOutputStream(Path.of(outputPath))) { - encryptStream(outputStream, inputStream, publicKeyIn); - } - } - private static PGPPublicKey getPublicKey(InputStream keyInputStream) throws IOException, PGPException { PGPPublicKeyRingCollection pgpPublicKeyRings = new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream(keyInputStream), new JcaKeyFingerprintCalculator()); 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 b433372c..428cda38 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 @@ -113,27 +113,6 @@ public static Object decryptPgp(BArray cipherTextValue, BString privateKeyPath, } } - public static Object decryptPgpAsFile(BString inputFilePath, BString privateKeyPath, BArray passphrase, - BString outputFilePath) { - byte[] passphraseInBytes = passphrase.getBytes(); - byte[] privateKey; - try { - privateKey = Files.readAllBytes(Path.of(privateKeyPath.toString())); - } catch (IOException e) { - return CryptoUtils.createError(ERROR_OCCURRED_WHILE_READING_PRIVATE_KEY + e.getMessage()); - } - - try (InputStream keyStream = new ByteArrayInputStream(privateKey); - InputStream cipherTextStream = Files.newInputStream(Path.of(inputFilePath.toString())) - ) { - PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); - pgpDecryptionGenerator.decrypt(cipherTextStream, outputFilePath.getValue()); - return null; - } catch (IOException | PGPException e) { - return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_DECRYPT + e.getMessage()); - } - } - public static Object decryptStreamPgp(Environment environment, BStream inputBalStream, BString privateKeyPath, BArray passphrase) { byte[] passphraseInBytes = passphrase.getBytes(); 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 63a05384..aa2870a8 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 @@ -130,31 +130,6 @@ public static Object encryptPgp(BArray plainTextValue, BString publicKeyPath, BM } } - public static Object encryptPgpAsFile(BString inputFilePath, BString publicKeyPath, BString outputFilePath, - BMap options) { - 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); - InputStream inputStream = Files.newInputStream(Path.of(inputFilePath.toString())) - ) { - 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()) - ); - pgpEncryptionGenerator.encrypt(inputStream, publicKeyStream, outputFilePath.getValue()); - return null; - } catch (IOException | PGPException e) { - return CryptoUtils.createError(ERROR_OCCURRED_WHILE_PGP_ENCRYPT + e.getMessage()); - } - } - public static Object encryptStreamPgp(Environment environment, BStream inputBalStream, BString publicKeyPath, BMap options) { byte[] publicKey; From 9cdd933b58afaf3d2fe6cc511b7fe2bc1e65dd19 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 12:08:05 +0530 Subject: [PATCH 23/44] Fix test cases --- ballerina/tests/encrypt_decrypt_pgp_test.bal | 62 +++++++++++++------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 562ebe1c..8e79ce53 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -67,9 +67,17 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() return } isolated function testEncryptAndDecryptFileWithPgp() returns error? { byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); - check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); - check decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); - test:assertTrue(check isSameFileContent(SAMPLE_TEXT, TARGET_DECRYPTION_OUTPUT)); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH); + stream decryptedStream = check decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + + byte[] expected = check io:fileReadBytes(SAMPLE_TEXT); + byte[] actual = []; + check from byte[] bytes in decryptedStream + do { + actual.push(...bytes); + }; + test:assertEquals(actual, expected); } @test:Config { @@ -77,9 +85,17 @@ isolated function testEncryptAndDecryptFileWithPgp() returns error? { } isolated function testEncryptAndDecryptFileWithPgpWithOptions() returns error? { byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); - check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT, symmetricKeyAlgorithm = AES_128, armor = false); - check decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); - test:assertTrue(check isSameFileContent(SAMPLE_TEXT, TARGET_DECRYPTION_OUTPUT)); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); + stream decryptedStream = check decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + + byte[] expected = check io:fileReadBytes(SAMPLE_TEXT); + byte[] actual = []; + check from byte[] bytes in decryptedStream + do { + actual.push(...bytes); + }; + test:assertEquals(actual, expected); } @test:Config { @@ -87,11 +103,17 @@ isolated function testEncryptAndDecryptFileWithPgpWithOptions() returns error? { } isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPrivateKey() returns error? { byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); - check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); - error? err = decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_INVALID_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); - if err is Error { - test:assertEquals(err.message(), "Error occurred while PGP decrypt: Could not Extract private key"); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); + stream|Error result = check decryptStreamPgp(encryptedStream, PGP_INVALID_PRIVATE_KEY_PATH, passphrase); + if result is Error { + check encryptedStream.close(); + check inputStream.close(); + test:assertEquals(result.message(), "Error occurred while PGP decrypt: Could not Extract private key"); } else { + check encryptedStream.close(); + check inputStream.close(); + check result.close(); test:assertFail("Should return a crypto Error"); } } @@ -101,18 +123,18 @@ isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPrivateKey() re } isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPassphrase() returns error? { byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); - check encryptPgpAsFile(SAMPLE_TEXT, PGP_PUBLIC_KEY_PATH, TARGET_ENCRYPTION_OUTPUT); - error? err = decryptPgpAsFile(TARGET_ENCRYPTION_OUTPUT, PGP_PRIVATE_KEY_PATH, passphrase, TARGET_DECRYPTION_OUTPUT); - if err is Error { - test:assertEquals(err.message(), + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); + stream|Error result = check decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + if result is Error { + check encryptedStream.close(); + check inputStream.close(); + test:assertEquals(result.message(), "Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes"); } else { + check encryptedStream.close(); + check inputStream.close(); + check result.close(); test:assertFail("Should return a crypto Error"); } } - -isolated function isSameFileContent(string inputFilePath, string outputFilePath) returns boolean|error { - byte[] input = check io:fileReadBytes(inputFilePath); - byte[] output = check io:fileReadBytes(outputFilePath); - return input == output; -} From 29333dc112c3e78477d433d55537e0893e5de8ff Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 12:09:44 +0530 Subject: [PATCH 24/44] Update change log --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 57b2a5e9..1abac4da 100644 --- a/changelog.md +++ b/changelog.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- [Introduce new APIs to support PGP encryption and decryption with files](https://github.com/ballerina-platform/ballerina-library/issues/7064) +- [Introduce new APIs to support PGP encryption and decryption with streams](https://github.com/ballerina-platform/ballerina-library/issues/7064) ## [2.7.2] - 2024-05-30 From f71ebb7f62cda810436911d650038e3a6053c5ff Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 12:12:17 +0530 Subject: [PATCH 25/44] Update spec --- docs/spec/spec.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 8a1f6b72..16da3c60 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -532,15 +532,12 @@ string publicKeyPath = "/path/to/publickey.asc"; byte[] cipherText = check crypto:encryptPgp(data, publicKeyPath, armor = false); ``` -In addition to the above, the following API can be used to read a content from a file, encrypt it using the PGP public -key and write the encrypted content to the file specified. +In addition to the above, the following API can be used to read a content from a stream, encrypt it using the PGP public +key and return an encrypted stream ```ballerina -string inputFilePath = "/path/to/input.txt"; -string outputFilePath = "/path/to/output.txt"; -string publicKeyPath = "/path/to/publickey.asc"; - -check crypto:encryptPgpAsFile(inputFilePath, publicKeyPath, outputFilePath); +stream inputStream = check io:fileReadBlocksAsStream("input.txt"); +stream|Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); ``` ### 5.2. [Decryption](#52-decryption) @@ -630,16 +627,12 @@ byte[] cipherText = check crypto:encryptPgp(data, publicKeyPath); byte[] plainText = check crypto:decryptPgp(cipherText, privateKeyPath, passPhrase.toBytes()); ``` -In addition to the above, the following API can be used to read an encrypted content from a file, decrypt it using the -PGP private key and passphrase and write the decrypted content to the file specified. +In addition to the above, the following API can be used to read an encrypted content from a stream, decrypt it using the +PGP private key and passphrase and return a decrypted stream. ```ballerina -string inputFilePath = "/path/to/input.txt"; -string outputFilePath = "/path/to/output.txt"; -string privateKeyPath = "/path/to/privatekey.asc"; -string passPhrase = "passphrase"; - -check crypto:decryptPgpAsFile(inputFilePath, privateKeyPath, passPhrase.toBytes(), outputFilePath); +stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); +stream|Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); ``` ## 6. [Sign and Verify](#6-sign-and-verify) From 91697210486e3ef0892cb0466311cd2a09b58547 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 13:14:19 +0530 Subject: [PATCH 26/44] Update ballerina invoke call --- gradle.properties | 2 +- .../stdlib/crypto/BallerinaInputStream.java | 39 ++----------------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/gradle.properties b/gradle.properties index b8f845a8..d94ed202 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,6 @@ researchgateReleaseVersion=2.8.0 ballerinaGradlePluginVersion=2.0.1 nativeImageVersion=22.2.0 -ballerinaLangVersion=2201.9.0 +ballerinaLangVersion=2201.10.0-20240926-231800-8a5a4343 stdlibTimeVersion=2.4.0 stdlibIoVersion=1.6.1 diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java index 335f79bc..909367eb 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java @@ -18,7 +18,6 @@ package io.ballerina.stdlib.crypto; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.async.Callback; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BError; @@ -29,7 +28,6 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Objects; -import java.util.concurrent.CountDownLatch; /** * Represents a Ballerina stream as an {@link InputStream}. @@ -77,47 +75,18 @@ public int read() throws IOException { @Override public void close() throws IOException { - Object result = callBallerinaFunction(BAL_STREAM_CLOSE, ERROR_OCCURRED_WHILE_CLOSING_THE_STREAM); + Object result = callBalStreamMethod(BAL_STREAM_CLOSE); if (result instanceof BError bError) { throw new IOException((bError).getMessage()); } } public Object getNext() { - return callBallerinaFunction(BAL_STREAM_NEXT, ERROR_OCCURRED_WHILE_READING_THE_STREAM); + return callBalStreamMethod(BAL_STREAM_NEXT); } - private Object callBallerinaFunction(String functionName, String message) { - final Object[] nextResult = new Object[1]; - CountDownLatch countDownLatch = new CountDownLatch(1); - Callback returnCallback = new StreamCallback(message, nextResult, countDownLatch); - - environment.getRuntime().invokeMethodAsyncSequentially(ballerinaStream.getIteratorObj(), functionName, null, - null, returnCallback, null, null); - try { - countDownLatch.await(); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - return CryptoUtils.createError(INTERRUPTED_ERROR_WHILE_READING_THE_STREAM); - } - return nextResult[0]; - } - - private record StreamCallback(String message, Object[] nextResult, - CountDownLatch countDownLatch) implements Callback { - - @Override - public void notifySuccess(Object result) { - nextResult[0] = result; - countDownLatch.countDown(); - } - - @Override - public void notifyFailure(BError bError) { - BError error = CryptoUtils.createError(String.format(ERR_MSG_FORMAT, message, bError.getMessage())); - nextResult[0] = error; - countDownLatch.countDown(); - } + private Object callBalStreamMethod(String functionName) { + return environment.getRuntime().call(ballerinaStream.getIteratorObj(), functionName); } @Override From 1ba983dc7bf0229338f503b0f9989ac010e2f95d Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 13:14:48 +0530 Subject: [PATCH 27/44] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 1ce6ae2a..93cdd313 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -9,34 +9,34 @@ icon = "icon.png" license = ["Apache-2.0"] distribution = "2201.9.0" -[platform.java17] +[platform.java21] graalvmCompatible = true -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "crypto-native" version = "2.8.0" path = "../native/build/libs/crypto-native-2.8.0-SNAPSHOT.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcpkix-jdk18on" version = "1.78" path = "./lib/bcpkix-jdk18on-1.78.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcprov-jdk18on" version = "1.78" path = "./lib/bcprov-jdk18on-1.78.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcutil-jdk18on" version = "1.78" path = "./lib/bcutil-jdk18on-1.78.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcpg-jdk18on" version = "1.78" From 132cd3154ae2712fd31dd25fd3fa9bf045719398 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 13:15:29 +0530 Subject: [PATCH 28/44] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 117 ------------------------------------ 1 file changed, 117 deletions(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 3cc7bc46..e69de29b 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -1,117 +0,0 @@ -# AUTO-GENERATED FILE. DO NOT MODIFY. - -# This file is auto-generated by Ballerina for managing dependency versions. -# It should not be modified by hand. - -[ballerina] -dependencies-toml-version = "2" -distribution-version = "2201.9.0" - -[[package]] -org = "ballerina" -name = "crypto" -version = "2.8.0" -dependencies = [ - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.array"}, - {org = "ballerina", name = "test"}, - {org = "ballerina", name = "time"} -] -modules = [ - {org = "ballerina", packageName = "crypto", moduleName = "crypto"} -] - -[[package]] -org = "ballerina" -name = "io" -version = "1.6.1" -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" -version = "0.0.0" -modules = [ - {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "lang.__internal" -version = "0.0.0" -scope = "testOnly" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.object"} -] - -[[package]] -org = "ballerina" -name = "lang.array" -version = "0.0.0" -scope = "testOnly" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.__internal"} -] -modules = [ - {org = "ballerina", packageName = "lang.array", moduleName = "lang.array"} -] - -[[package]] -org = "ballerina" -name = "lang.error" -version = "0.0.0" -scope = "testOnly" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -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" -version = "0.0.0" -scope = "testOnly" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.array"}, - {org = "ballerina", name = "lang.error"} -] -modules = [ - {org = "ballerina", packageName = "test", moduleName = "test"} -] - -[[package]] -org = "ballerina" -name = "time" -version = "2.4.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] -modules = [ - {org = "ballerina", packageName = "time", moduleName = "time"} -] - From a51aa97ea8287b32006bfa15740355bea6732020 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 13:20:06 +0530 Subject: [PATCH 29/44] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 117 ++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index e69de29b..088071ec 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -0,0 +1,117 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.10.0-20240926-231800-8a5a4343" + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "crypto", moduleName = "crypto"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.2" +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" +version = "0.0.0" +modules = [ + {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] +modules = [ + {org = "ballerina", packageName = "lang.array", moduleName = "lang.array"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +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" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.error"} +] +modules = [ + {org = "ballerina", packageName = "test", moduleName = "test"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.5.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] + From 7139227d6d59d0a3bd17ae5ad06dfb29df5d138d Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 14:08:24 +0530 Subject: [PATCH 30/44] Update dependency versions supported with Java 21 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index cadf707c..7a2f95cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ ballerinaGradlePluginVersion=2.0.1 nativeImageVersion=22.2.0 ballerinaLangVersion=2201.10.0-20240926-231800-8a5a4343 -stdlibTimeVersion=2.4.0 -stdlibIoVersion=1.6.1 +stdlibTimeVersion=2.5.1-20240930-120200-e59222b +stdlibIoVersion=1.6.2-20240928-084100-656404f From 43b390354407e693315bccf7d6975dab6b0350ab Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 14:08:37 +0530 Subject: [PATCH 31/44] Rename test functions --- ballerina/tests/encrypt_decrypt_pgp_test.bal | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 8e79ce53..c9188d7f 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -65,7 +65,7 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() return @test:Config { serialExecution: true } -isolated function testEncryptAndDecryptFileWithPgp() returns error? { +isolated function testEncryptAndDecryptStreamWithPgp() returns error? { byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH); @@ -83,7 +83,7 @@ isolated function testEncryptAndDecryptFileWithPgp() returns error? { @test:Config { serialExecution: true } -isolated function testEncryptAndDecryptFileWithPgpWithOptions() returns error? { +isolated function testEncryptAndDecryptStreamWithPgpWithOptions() returns error? { byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); @@ -101,11 +101,11 @@ isolated function testEncryptAndDecryptFileWithPgpWithOptions() returns error? { @test:Config { serialExecution: true } -isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPrivateKey() returns error? { +isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPrivateKey() returns error? { byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); - stream|Error result = check decryptStreamPgp(encryptedStream, PGP_INVALID_PRIVATE_KEY_PATH, passphrase); + stream|Error result = decryptStreamPgp(encryptedStream, PGP_INVALID_PRIVATE_KEY_PATH, passphrase); if result is Error { check encryptedStream.close(); check inputStream.close(); @@ -121,11 +121,11 @@ isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPrivateKey() re @test:Config { serialExecution: true } -isolated function testNegativeEncryptAndDecryptFileWithPgpInvalidPassphrase() returns error? { +isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPassphrase() returns error? { byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); - stream|Error result = check decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + stream|Error result = decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); if result is Error { check encryptedStream.close(); check inputStream.close(); From 7c429ca3980fa09128ad974c8bbabe2abc8c2128 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 14:37:19 +0530 Subject: [PATCH 32/44] Add license header --- ballerina/stream_iterators.bal | 16 ++++++++++++++++ .../stdlib/crypto/SequentialBufferedPipe.java | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/ballerina/stream_iterators.bal b/ballerina/stream_iterators.bal index df047117..b936b6e1 100644 --- a/ballerina/stream_iterators.bal +++ b/ballerina/stream_iterators.bal @@ -1,3 +1,19 @@ +// 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/jballerina.java; class DecryptedStreamIterator { diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java b/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java index c07d9675..4da7162f 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java @@ -1,3 +1,20 @@ +/* + * 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 java.io.InputStream; From 9d529a43bf263b7a4930720e8faf93d0cb4e8d23 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 14:46:50 +0530 Subject: [PATCH 33/44] Add module prefix --- docs/spec/spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 16da3c60..ccbdee1e 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -537,7 +537,7 @@ key and return an encrypted stream ```ballerina stream inputStream = check io:fileReadBlocksAsStream("input.txt"); -stream|Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); +stream|io:Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); ``` ### 5.2. [Decryption](#52-decryption) @@ -632,7 +632,7 @@ PGP private key and passphrase and return a decrypted stream. ```ballerina stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); -stream|Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); +stream|io:Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); ``` ## 6. [Sign and Verify](#6-sign-and-verify) From 6902dbec2ff4d6bd458f4d29014942447c341ad2 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 14:51:35 +0530 Subject: [PATCH 34/44] Address sonar cloud issues --- .../stdlib/crypto/PgpDecryptionGenerator.java | 48 ++++++------------- 1 file changed, 15 insertions(+), 33 deletions(-) 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 ee72713a..e622738c 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -91,6 +91,11 @@ private Optional findSecretKey(long keyID) throws PGPException { private void decryptStream(InputStream encryptedIn, OutputStream clearOut) throws PGPException, IOException { + KeyEncryptedResult keyEncryptedResult = getKeyEncryptedResult(encryptedIn); + decrypt(clearOut, keyEncryptedResult.pgpPrivateKey(), keyEncryptedResult.publicKeyEncryptedData()); + } + + private KeyEncryptedResult getKeyEncryptedResult(InputStream encryptedIn) throws IOException, PGPException { // Remove armour and return the underlying binary encrypted stream encryptedIn = PGPUtil.getDecoderStream(encryptedIn); JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(encryptedIn); @@ -116,36 +121,15 @@ private void decryptStream(InputStream encryptedIn, OutputStream clearOut) if (pgpPrivateKey.isEmpty()) { throw new PGPException("Could not Extract private key"); } - decrypt(clearOut, pgpPrivateKey.get(), publicKeyEncryptedData); + return new KeyEncryptedResult(pgpPrivateKey.get(), publicKeyEncryptedData); } - public void decryptStream(InputStream encryptedIn, BObject iteratorObj) 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(); - - Optional pgpPrivateKey = Optional.empty(); - PGPPublicKeyEncryptedData publicKeyEncryptedData = null; - - Iterator encryptedDataItr = pgpEncryptedDataList.getEncryptedDataObjects(); - while (pgpPrivateKey.isEmpty() && encryptedDataItr.hasNext()) { - publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedDataItr.next(); - pgpPrivateKey = findSecretKey(publicKeyEncryptedData.getKeyID()); - } - - if (Objects.isNull(publicKeyEncryptedData)) { - throw new PGPException("Could not generate PGPPublicKeyEncryptedData object"); - } + private record KeyEncryptedResult(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData) { + } - if (pgpPrivateKey.isEmpty()) { - throw new PGPException("Could not Extract private key"); - } - decrypt(pgpPrivateKey.get(), publicKeyEncryptedData, iteratorObj); + public void decryptStream(InputStream encryptedIn, BObject iteratorObj) throws PGPException, IOException { + KeyEncryptedResult keyEncryptedResult = getKeyEncryptedResult(encryptedIn); + decrypt(keyEncryptedResult.pgpPrivateKey, keyEncryptedResult.publicKeyEncryptedData, iteratorObj); } // Decrypts the given byte array of encrypted data using PGP decryption. @@ -187,11 +171,10 @@ private static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, } } // Perform the integrity check - if (publicKeyEncryptedData.isIntegrityProtected()) { - if (!publicKeyEncryptedData.verify()) { + if (publicKeyEncryptedData.isIntegrityProtected() && !publicKeyEncryptedData.verify()) { throw new PGPException("Message failed integrity check"); } - } + } private static void decrypt(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData, @@ -209,11 +192,10 @@ private static void decrypt(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedDa if (message instanceof PGPLiteralData pgpLiteralData) { // Perform the integrity check - if (publicKeyEncryptedData.isIntegrityProtected()) { - if (!publicKeyEncryptedData.verify()) { + if (publicKeyEncryptedData.isIntegrityProtected() && !publicKeyEncryptedData.verify()) { throw new PGPException("Message failed integrity check"); } - } + iteratorObj.addNativeData(TARGET_STREAM, pgpLiteralData.getDataStream()); iteratorObj.addNativeData(COMPRESSED_DATA_STREAM, compressedDataStream); iteratorObj.addNativeData(DATA_STREAM, decryptedCompressedIn); From 534c5a0d7303781ff2c7a725252335585bf2f0d2 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Wed, 9 Oct 2024 16:28:21 +0530 Subject: [PATCH 35/44] Fix integrity check --- .../io/ballerina/stdlib/crypto/Constants.java | 1 + .../stdlib/crypto/PgpDecryptionGenerator.java | 18 ++++------ .../stdlib/crypto/PgpEncryptionGenerator.java | 4 ++- .../stdlib/crypto/nativeimpl/StreamUtils.java | 35 +++++++++++++++++-- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java index 279bff87..faf301a5 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/Constants.java @@ -134,4 +134,5 @@ private Constants() {} public static final String PIPED_OUTPUT_STREAM = "PIPED_OUTPUT_STREAM"; public static final String END_OF_INPUT_STREAM = "END_OF_INPUT_STREAM"; public static final String COMPRESSED_DATA_GENERATOR = "COMPRESSED_DATA_GENERATOR"; + public static final String KEY_ENCRYPTED_DATA = "KEY_ENCRYPTED_DATA"; } 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 e622738c..810d0470 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpDecryptionGenerator.java @@ -51,6 +51,7 @@ import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_STREAM; import static io.ballerina.stdlib.crypto.Constants.DATA_STREAM; +import static io.ballerina.stdlib.crypto.Constants.KEY_ENCRYPTED_DATA; import static io.ballerina.stdlib.crypto.Constants.TARGET_STREAM; /** @@ -102,8 +103,8 @@ private KeyEncryptedResult getKeyEncryptedResult(InputStream encryptedIn) throws Object obj = pgpObjectFactory.nextObject(); // Verify the marker packet - PGPEncryptedDataList pgpEncryptedDataList = (obj instanceof PGPEncryptedDataList) - ? (PGPEncryptedDataList) obj : (PGPEncryptedDataList) pgpObjectFactory.nextObject(); + PGPEncryptedDataList pgpEncryptedDataList = (obj instanceof PGPEncryptedDataList pgpEncryptedData) + ? pgpEncryptedData : (PGPEncryptedDataList) pgpObjectFactory.nextObject(); Optional pgpPrivateKey = Optional.empty(); PGPPublicKeyEncryptedData publicKeyEncryptedData = null; @@ -142,7 +143,7 @@ public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { } private static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, - PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { + PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(pgpPrivateKey); try (InputStream decryptedCompressedIn = publicKeyEncryptedData.getDataStream(decryptorFactory)) { @@ -172,9 +173,8 @@ private static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, } // Perform the integrity check if (publicKeyEncryptedData.isIntegrityProtected() && !publicKeyEncryptedData.verify()) { - throw new PGPException("Message failed integrity check"); - } - + throw new PGPException("Message failed integrity check"); + } } private static void decrypt(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData, @@ -191,11 +191,7 @@ private static void decrypt(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedDa Object message = pgpCompObjFac.nextObject(); if (message instanceof PGPLiteralData pgpLiteralData) { - // Perform the integrity check - if (publicKeyEncryptedData.isIntegrityProtected() && !publicKeyEncryptedData.verify()) { - throw new PGPException("Message failed integrity check"); - } - + iteratorObj.addNativeData(KEY_ENCRYPTED_DATA, publicKeyEncryptedData); iteratorObj.addNativeData(TARGET_STREAM, pgpLiteralData.getDataStream()); iteratorObj.addNativeData(COMPRESSED_DATA_STREAM, compressedDataStream); iteratorObj.addNativeData(DATA_STREAM, decryptedCompressedIn); 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 f2145c75..e9ccff50 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/PgpEncryptionGenerator.java @@ -50,6 +50,7 @@ import java.util.Optional; import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_GENERATOR; +import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_STREAM; import static io.ballerina.stdlib.crypto.Constants.DATA_STREAM; import static io.ballerina.stdlib.crypto.Constants.ENCRYPTED_OUTPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.PIPED_INPUT_STREAM; @@ -77,7 +78,7 @@ public class PgpEncryptionGenerator { // The constructor of the PGP encryption generator. public PgpEncryptionGenerator(int compressionAlgorithm, int symmetricKeyAlgorithm, boolean armor, - boolean withIntegrityCheck) { + boolean withIntegrityCheck) { this.compressionAlgorithm = compressionAlgorithm; this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; this.armor = armor; @@ -134,6 +135,7 @@ public void encryptStream(InputStream publicKeyIn, BObject iteratorObj) OutputStream cipherOutStream = pgpEncryptedDataGenerator.open(encryptOut, new byte[BUFFER_SIZE]); OutputStream compressedOutStream = compressedDataGenerator.open(cipherOutStream); iteratorObj.addNativeData(DATA_STREAM, cipherOutStream); + iteratorObj.addNativeData(COMPRESSED_DATA_STREAM, compressedOutStream); iteratorObj.addNativeData(COMPRESSED_DATA_GENERATOR, compressedDataGenerator); copyAsLiteralData(compressedOutStream, iteratorObj); } diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java index ff9c0cbd..a2cbf3ee 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java @@ -23,6 +23,8 @@ import io.ballerina.runtime.api.values.BObject; import io.ballerina.stdlib.crypto.CryptoUtils; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import java.io.Closeable; import java.io.IOException; @@ -34,6 +36,7 @@ import static io.ballerina.stdlib.crypto.Constants.ENCRYPTED_OUTPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.END_OF_INPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.INPUT_STREAM_TO_ENCRYPT; +import static io.ballerina.stdlib.crypto.Constants.KEY_ENCRYPTED_DATA; import static io.ballerina.stdlib.crypto.Constants.PIPED_INPUT_STREAM; import static io.ballerina.stdlib.crypto.Constants.COMPRESSED_DATA_STREAM; import static io.ballerina.stdlib.crypto.Constants.DATA_STREAM; @@ -55,6 +58,11 @@ public final class StreamUtils { public static final String ERROR_OCCURRED_WHILE_READING_THE_STREAM = "Error occurred while reading from the " + "stream: %s"; public static final String NATIVE_DATA_NOT_AVAILABLE_ERROR = "%s is not available"; + public static final String MESSAGE_FAILED_INTEGRITY_CHECK = "Message failed integrity check"; + public static final String ERROR_OCCURRED_WHILE_VERIFYING_THE_INTEGRITY = "Error occurred while verifying the" + + " integrity: %s"; + public static final String ERROR_OCCURRED_WHILE_READING_FROM_THE_STREAM = "Error occurred while reading from " + + "the stream: %s"; private StreamUtils() { } @@ -68,6 +76,8 @@ public static Object readDecryptedStream(BObject iterator) { byte[] buffer = new byte[BUFFER_SIZE]; int in = inputStream.read(buffer); if (in == -1) { + closeNativeStream(iterator, TARGET_STREAM); + performIntegrityCheck(iterator); return null; } if (in < buffer.length) { @@ -77,7 +87,23 @@ public static Object readDecryptedStream(BObject iterator) { } return ValueCreator.createArrayValue(buffer); } catch (IOException e) { - return CryptoUtils.createError("Error occurred while reading from the stream: " + e.getMessage()); + return CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_READING_FROM_THE_STREAM, + e.getMessage())); + } + } + + private static void performIntegrityCheck(BObject iterator) throws IOException { + Object publicKeyEncryptedDataObj = iterator.getNativeData(KEY_ENCRYPTED_DATA); + if (Objects.isNull(publicKeyEncryptedDataObj) || !(publicKeyEncryptedDataObj instanceof + PGPPublicKeyEncryptedData publicKeyEncryptedData)) { + throw CryptoUtils.createError(STREAM_NOT_AVAILABLE); + } + try { + if (publicKeyEncryptedData.isIntegrityProtected() && !publicKeyEncryptedData.verify()) { + throw CryptoUtils.createError(MESSAGE_FAILED_INTEGRITY_CHECK); + } + } catch (PGPException e) { + throw CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_VERIFYING_THE_INTEGRITY, e.getMessage())); } } @@ -88,7 +114,7 @@ public static Object readEncryptedStream(BObject iterator) { if (Boolean.FALSE.equals(nativeData.endOfStream())) { writeToOutStream(iterator, nativeData.inputStream(), nativeData.outputStream()); } - return readFromPipedStream(nativeData.pipedInStream()); + return readFromPipedStream(iterator, nativeData.pipedInStream()); } catch (IOException e) { return CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_READING_THE_STREAM, e.getMessage())); } catch (BError e) { @@ -123,10 +149,11 @@ private record NativeData(InputStream inputStream, OutputStream outputStream, In Boolean endOfStream) { } - private static BArray readFromPipedStream(InputStream pipedInStream) throws IOException { + private static BArray readFromPipedStream(BObject iterator, InputStream pipedInStream) throws IOException { byte[] pipelinedBuffer = new byte[BUFFER_SIZE]; int pipelinedIn = pipedInStream.read(pipelinedBuffer); if (pipelinedIn == -1) { + closeNativeStream(iterator, PIPED_INPUT_STREAM); return null; } if (pipelinedIn < pipelinedBuffer.length) { @@ -158,6 +185,7 @@ public static void closeDecryptedStream(BObject iterator) throws BError { public static void closeEncryptedStream(BObject iterator) throws BError { closeNativeStream(iterator, TARGET_STREAM); + closeNativeStream(iterator, COMPRESSED_DATA_STREAM); closeDataGenerator(iterator); closeNativeStream(iterator, DATA_STREAM); closeNativeStream(iterator, ENCRYPTED_OUTPUT_STREAM); @@ -168,6 +196,7 @@ public static void closeEncryptedStream(BObject iterator) throws BError { public static void closeEncryptedSourceStreams(BObject iterator) throws BError { closeNativeStream(iterator, INPUT_STREAM_TO_ENCRYPT); closeNativeStream(iterator, TARGET_STREAM); + closeNativeStream(iterator, COMPRESSED_DATA_STREAM); closeDataGenerator(iterator); closeNativeStream(iterator, DATA_STREAM); closeNativeStream(iterator, ENCRYPTED_OUTPUT_STREAM); From a44340956fb6cd02a5032b31e80f1f4b5a808dee Mon Sep 17 00:00:00 2001 From: Krishnananthalingam Tharmigan <63336800+TharmiganK@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:24:17 +0530 Subject: [PATCH 36/44] Apply suggestions from code review --- docs/spec/spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index ccbdee1e..74864189 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -537,7 +537,7 @@ key and return an encrypted stream ```ballerina stream inputStream = check io:fileReadBlocksAsStream("input.txt"); -stream|io:Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); +stream|crypto:Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); ``` ### 5.2. [Decryption](#52-decryption) @@ -632,7 +632,7 @@ PGP private key and passphrase and return a decrypted stream. ```ballerina stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); -stream|io:Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); +stream|crypto:Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); ``` ## 6. [Sign and Verify](#6-sign-and-verify) From c79289034ea447eea22f65b3a02a7c6c43f39ae3 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Thu, 10 Oct 2024 11:22:32 +0530 Subject: [PATCH 37/44] Remove unused constants --- .../java/io/ballerina/stdlib/crypto/BallerinaInputStream.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java index 909367eb..08fb2b60 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java @@ -39,12 +39,8 @@ public class BallerinaInputStream extends InputStream { public static final String STREAM_VALUE = "value"; public static final String BAL_STREAM_NEXT = "next"; - public static final String ERROR_OCCURRED_WHILE_CLOSING_THE_STREAM = "Error occurred while closing the stream"; public static final String ERROR_OCCURRED_WHILE_READING_THE_STREAM = "Error occurred while reading the next " + "element from the stream"; - public static final String INTERRUPTED_ERROR_WHILE_READING_THE_STREAM = ERROR_OCCURRED_WHILE_READING_THE_STREAM + - ": interrupted exception"; - public static final String ERR_MSG_FORMAT = "%s: %s"; public static final String UNEXPECTED_TYPE_ERROR = ERROR_OCCURRED_WHILE_READING_THE_STREAM + ": unexpected value type"; From 0f85fef6c0ba4b60cdd01a949bdb23611ad0a4ae Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Fri, 11 Oct 2024 10:40:17 +0530 Subject: [PATCH 38/44] Rename PGP stream APIs --- ballerina/encrypt_decrypt.bal | 10 +++++----- ballerina/tests/encrypt_decrypt_pgp_test.bal | 16 ++++++++-------- docs/spec/spec.md | 4 ++-- .../stdlib/crypto/nativeimpl/Decrypt.java | 2 +- .../stdlib/crypto/nativeimpl/Encrypt.java | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index ba52052f..543d34f3 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -263,13 +263,13 @@ public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Opt # Returns the PGP-encrypted stream of the content given in the input stream. # ```ballerina # stream inputStream = check io:fileReadBlocksAsStream("input.txt"); -# stream|Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); +# stream|crypto:Error encryptedStream = crypto:encryptStreamAsPgp(inputStream, "public_key.asc"); # ``` # # + inputStream - The content to be encrypted as a stream # + privateKeyPath - Path to the private key # + return - Encrypted stream or else a `crypto:Error` if the key is invalid -public isolated function encryptStreamPgp(stream inputStream, string publicKeyPath, +public isolated function encryptStreamAsPgp(stream inputStream, string publicKeyPath, *Options options) returns stream|Error = @java:Method { 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; @@ -284,7 +284,7 @@ public isolated function encryptStreamPgp(stream inputStream, st # ``` # # + cipherText - The encrypted content to be decrypted -# + privateKeyPath - Path to the private key +# + privateKey - # + passphrase - passphrase of the private key # + return - Decrypted data or else a `crypto:Error` if the key or passphrase is invalid public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, byte[] passphrase) @@ -297,14 +297,14 @@ public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, by # ```ballerina # byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); # stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); -# stream|Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); +# stream|crypto:Error decryptedStream = crypto:decryptStreamFromPgp(inputStream, "private_key.asc", passphrase); # ``` # # + inputStream - The encrypted content as a stream # + privateKeyPath - Path to the private key # + passphrase - passphrase of the private key # + return - Decrypted stream or else a `crypto:Error` if the key or passphrase is invalid -public isolated function decryptStreamPgp(stream inputStream, string privateKeyPath, +public isolated function decryptStreamFromPgp(stream inputStream, string privateKeyPath, byte[] passphrase) returns stream|Error = @java:Method { 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; diff --git a/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index c9188d7f..0b84d0e6 100644 --- a/ballerina/tests/encrypt_decrypt_pgp_test.bal +++ b/ballerina/tests/encrypt_decrypt_pgp_test.bal @@ -68,8 +68,8 @@ isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() return isolated function testEncryptAndDecryptStreamWithPgp() returns error? { byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); - stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH); - stream decryptedStream = check decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + stream encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH); + stream decryptedStream = check decryptStreamFromPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); byte[] expected = check io:fileReadBytes(SAMPLE_TEXT); byte[] actual = []; @@ -86,8 +86,8 @@ isolated function testEncryptAndDecryptStreamWithPgp() returns error? { isolated function testEncryptAndDecryptStreamWithPgpWithOptions() returns error? { byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); - stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); - stream decryptedStream = check decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + stream encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); + stream decryptedStream = check decryptStreamFromPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); byte[] expected = check io:fileReadBytes(SAMPLE_TEXT); byte[] actual = []; @@ -104,8 +104,8 @@ isolated function testEncryptAndDecryptStreamWithPgpWithOptions() returns error? isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPrivateKey() returns error? { byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); - stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); - stream|Error result = decryptStreamPgp(encryptedStream, PGP_INVALID_PRIVATE_KEY_PATH, passphrase); + stream encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); + stream|Error result = decryptStreamFromPgp(encryptedStream, PGP_INVALID_PRIVATE_KEY_PATH, passphrase); if result is Error { check encryptedStream.close(); check inputStream.close(); @@ -124,8 +124,8 @@ isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPrivateKey() isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPassphrase() returns error? { byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); - stream encryptedStream = check encryptStreamPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); - stream|Error result = decryptStreamPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); + stream encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false); + stream|Error result = decryptStreamFromPgp(encryptedStream, PGP_PRIVATE_KEY_PATH, passphrase); if result is Error { check encryptedStream.close(); check inputStream.close(); diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 74864189..40d51519 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -537,7 +537,7 @@ key and return an encrypted stream ```ballerina stream inputStream = check io:fileReadBlocksAsStream("input.txt"); -stream|crypto:Error encryptedStream = crypto:encryptStreamPgp(inputStream, "public_key.asc"); +stream|crypto:Error encryptedStream = crypto:encryptStreamAsPgp(inputStream, "public_key.asc"); ``` ### 5.2. [Decryption](#52-decryption) @@ -632,7 +632,7 @@ PGP private key and passphrase and return a decrypted stream. ```ballerina stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); -stream|crypto:Error decryptedStream = crypto:decryptStreamPgp(inputStream, "private_key.asc", passphrase); +stream|crypto:Error decryptedStream = crypto:decryptStreamFromPgp(inputStream, "private_key.asc", passphrase); ``` ## 6. [Sign and Verify](#6-sign-and-verify) 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 428cda38..ff5d81ec 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 @@ -113,7 +113,7 @@ public static Object decryptPgp(BArray cipherTextValue, BString privateKeyPath, } } - public static Object decryptStreamPgp(Environment environment, BStream inputBalStream, BString privateKeyPath, + public static Object decryptStreamFromPgp(Environment environment, BStream inputBalStream, BString privateKeyPath, BArray passphrase) { byte[] passphraseInBytes = passphrase.getBytes(); byte[] privateKey; 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 aa2870a8..50aea037 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 @@ -130,7 +130,7 @@ public static Object encryptPgp(BArray plainTextValue, BString publicKeyPath, BM } } - public static Object encryptStreamPgp(Environment environment, BStream inputBalStream, BString publicKeyPath, + public static Object encryptStreamAsPgp(Environment environment, BStream inputBalStream, BString publicKeyPath, BMap options) { byte[] publicKey; try { From 03c6d8f0a257dfb57becb111c7ebbffedf356cd5 Mon Sep 17 00:00:00 2001 From: Krishnananthalingam Tharmigan <63336800+TharmiganK@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:10:59 +0530 Subject: [PATCH 39/44] Apply suggestions from code review Co-authored-by: Bhashinee --- ballerina/encrypt_decrypt.bal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 375d9b4f..94cb9215 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -267,7 +267,7 @@ public isolated function encryptPgp(byte[] plainText, string publicKey, *Options # ``` # # + inputStream - The content to be encrypted as a stream -# + privateKey - Path to the private key +# + publicKey - Path to the public key # + return - Encrypted stream or else a `crypto:Error` if the key is invalid public isolated function encryptStreamAsPgp(stream inputStream, string publicKey, *Options options) returns stream|Error = @java:Method { From f019b5436a4ea194661ae70686be614298959ee7 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 14 Oct 2024 08:56:10 +0530 Subject: [PATCH 40/44] Add missing doc for options --- ballerina/encrypt_decrypt.bal | 1 + 1 file changed, 1 insertion(+) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index 94cb9215..b4ced6ab 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -268,6 +268,7 @@ public isolated function encryptPgp(byte[] plainText, string publicKey, *Options # # + inputStream - The content to be encrypted as a stream # + publicKey - Path to the public key +# + options - PGP encryption options # + return - Encrypted stream or else a `crypto:Error` if the key is invalid public isolated function encryptStreamAsPgp(stream inputStream, string publicKey, *Options options) returns stream|Error = @java:Method { From db345a05ffd0ad82d6bfc17324c40cf092849f6e Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 14 Oct 2024 08:56:30 +0530 Subject: [PATCH 41/44] Replace experimental graalvm options --- .../io.ballerina.stdlib/crypto-native/native-image.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/crypto-native/native-image.properties b/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/crypto-native/native-image.properties index 806d73de..45f2f156 100644 --- a/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/crypto-native/native-image.properties +++ b/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/crypto-native/native-image.properties @@ -14,5 +14,6 @@ # specific language governing permissions and limitations # under the License. -Args = -H:ClassInitialization=org.bouncycastle.jcajce.provider.drbg.DRBG\$Default:rerun,org.bouncycastle.jcajce.provider.drbg.DRBG\$NonceAndIV:rerun \ +Args = --initialize-at-run-time=org.bouncycastle.jcajce.provider.drbg.DRBG\$Default \ + --initialize-at-run-time=org.bouncycastle.jcajce.provider.drbg.DRBG\$NonceAndIV \ --features=io.ballerina.stdlib.crypto.svm.BouncyCastleFeature From a333117d487de5158005e889db4a95b94f5a2637 Mon Sep 17 00:00:00 2001 From: Krishnananthalingam Tharmigan <63336800+TharmiganK@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:12:37 +0530 Subject: [PATCH 42/44] Apply suggestions from code review --- ballerina/encrypt_decrypt.bal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index b4ced6ab..eb4ebeef 100644 --- a/ballerina/encrypt_decrypt.bal +++ b/ballerina/encrypt_decrypt.bal @@ -111,7 +111,7 @@ public isolated function encryptAesCbc(byte[] input, byte[] key, byte[] iv, AesP # + padding - The padding algorithm # + return - Encrypted data or else a `crypto:Error` if the key is invalid public isolated function encryptAesEcb(byte[] input, byte[] key, AesPadding padding = PKCS5) - returns byte[]|Error = @java:Method { + returns byte[]|Error = @java:Method { name: "encryptAesEcb", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" } external; @@ -189,7 +189,7 @@ public isolated function decryptRsaEcb(byte[] input, PrivateKey|PublicKey key, R # + padding - The padding algorithm # + return - Decrypted data or else a `crypto:Error` if the key is invalid public isolated function decryptAesCbc(byte[] input, byte[] key, byte[] iv, AesPadding padding = PKCS5) - returns byte[]|Error = @java:Method { + returns byte[]|Error = @java:Method { name: "decryptAesCbc", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" } external; @@ -279,7 +279,7 @@ public isolated function encryptStreamAsPgp(stream inputStream, # ```ballerina # byte[] message = "Hello Ballerina!".toBytes(); # byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); -# +# # byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); # byte[] decryptedMessage = check crypto:decryptPgp(cipherText, "private_key.asc", passphrase); # ``` From 9deb3cdc9a09b55e7177301879d22c3f5ec2ef90 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 18 Nov 2024 15:59:29 +0530 Subject: [PATCH 43/44] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 088071ec..db9ed17f 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.10.0-20240926-231800-8a5a4343" +distribution-version = "2201.11.0-20241117-133400-a3054b77" [[package]] org = "ballerina" @@ -107,7 +107,7 @@ modules = [ [[package]] org = "ballerina" name = "time" -version = "2.5.1" +version = "2.6.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] From df0e19da5d8ef5b171fe6d155358219aa1f957f1 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 18 Nov 2024 16:03:48 +0530 Subject: [PATCH 44/44] Update lang APIs --- .../java/io/ballerina/stdlib/crypto/BallerinaInputStream.java | 2 +- .../java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java | 2 +- .../java/io/ballerina/stdlib/crypto/nativeimpl/Encrypt.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java index 08fb2b60..6c0a5e8f 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java @@ -82,7 +82,7 @@ public Object getNext() { } private Object callBalStreamMethod(String functionName) { - return environment.getRuntime().call(ballerinaStream.getIteratorObj(), functionName); + return environment.getRuntime().callMethod(ballerinaStream.getIteratorObj(), functionName, null); } @Override 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 ff5d81ec..2d1dd003 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 @@ -19,9 +19,9 @@ package io.ballerina.stdlib.crypto.nativeimpl; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.PredefinedTypes; import io.ballerina.runtime.api.creators.TypeCreator; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.PredefinedTypes; import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BMap; 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 50aea037..a5d8ef33 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 @@ -19,9 +19,9 @@ package io.ballerina.stdlib.crypto.nativeimpl; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.PredefinedTypes; import io.ballerina.runtime.api.creators.TypeCreator; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.PredefinedTypes; import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BArray;