diff --git a/.github/workflows/build-timestamped-master.yml b/.github/workflows/build-timestamped-master.yml index c8e42fa1..86da4816 100644 --- a/.github/workflows/build-timestamped-master.yml +++ b/.github/workflows/build-timestamped-master.yml @@ -12,5 +12,5 @@ jobs: call_workflow: name: Run Build Workflow if: ${{ github.repository_owner == 'ballerina-platform' }} - uses: ballerina-platform/ballerina-library/.github/workflows/build-timestamp-master-template.yml@main + uses: ballerina-platform/ballerina-library/.github/workflows/build-timestamp-master-template.yml@java21 secrets: inherit diff --git a/.github/workflows/build-with-bal-test-graalvm.yml b/.github/workflows/build-with-bal-test-graalvm.yml index 000baac8..3d5d2e99 100644 --- a/.github/workflows/build-with-bal-test-graalvm.yml +++ b/.github/workflows/build-with-bal-test-graalvm.yml @@ -30,7 +30,7 @@ jobs: call_stdlib_workflow: name: Run StdLib Workflow if: ${{ github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository_owner == 'ballerina-platform') }} - uses: ballerina-platform/ballerina-library/.github/workflows/build-with-bal-test-graalvm-template.yml@main + uses: ballerina-platform/ballerina-library/.github/workflows/build-with-bal-test-graalvm-template.yml@java21 with: lang_tag: ${{ inputs.lang_tag }} lang_version: ${{ inputs.lang_version }} diff --git a/.github/workflows/central-publish.yml b/.github/workflows/central-publish.yml index 11922b55..ebe213e7 100644 --- a/.github/workflows/central-publish.yml +++ b/.github/workflows/central-publish.yml @@ -15,7 +15,7 @@ jobs: call_workflow: name: Run Central Publish Workflow if: ${{ github.repository_owner == 'ballerina-platform' }} - uses: ballerina-platform/ballerina-library/.github/workflows/central-publish-template.yml@main + uses: ballerina-platform/ballerina-library/.github/workflows/central-publish-template.yml@java21 secrets: inherit with: environment: ${{ github.event.inputs.environment }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 460928fe..f84a847d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -9,7 +9,7 @@ jobs: call_workflow: name: Run Release Workflow if: ${{ github.repository_owner == 'ballerina-platform' }} - uses: ballerina-platform/ballerina-library/.github/workflows/release-package-template.yml@main + uses: ballerina-platform/ballerina-library/.github/workflows/release-package-template.yml@java21 secrets: inherit with: package-name: crypto diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3b7c1462..c534e2fa 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,5 +10,5 @@ jobs: call_workflow: name: Run PR Build Workflow if: ${{ github.repository_owner == 'ballerina-platform' }} - uses: ballerina-platform/ballerina-library/.github/workflows/pull-request-build-template.yml@main + uses: ballerina-platform/ballerina-library/.github/workflows/pull-request-build-template.yml@java21 secrets: inherit diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 458aab57..d91a5f37 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -9,5 +9,5 @@ jobs: call_workflow: name: Run Trivy Scan Workflow if: ${{ github.repository_owner == 'ballerina-platform' }} - uses: ballerina-platform/ballerina-library/.github/workflows/trivy-scan-template.yml@main + uses: ballerina-platform/ballerina-library/.github/workflows/trivy-scan-template.yml@java21 secrets: inherit diff --git a/README.md b/README.md index 8cdcb701..0ab9206f 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ This repository only contains the source code for the module. ### Set up the prerequisites -1. Download and install Java SE Development Kit (JDK) version 17 (from one of the following locations). +1. Download and install Java SE Development Kit (JDK) version 21 (from one of the following locations). * [Oracle](https://www.oracle.com/java/technologies/downloads/) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 74a87bac..93cdd313 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" @@ -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.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]] +[[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" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 271dd6ac..088071ec 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,13 +5,14 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240926-231800-8a5a4343" [[package]] org = "ballerina" name = "crypto" -version = "2.7.3" +version = "2.8.0" dependencies = [ + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "test"}, @@ -21,6 +22,19 @@ modules = [ {org = "ballerina", packageName = "crypto", moduleName = "crypto"} ] +[[package]] +org = "ballerina" +name = "io" +version = "1.6.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" @@ -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" @@ -84,7 +107,7 @@ modules = [ [[package]] org = "ballerina" name = "time" -version = "2.4.0" +version = "2.5.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] diff --git a/ballerina/encrypt_decrypt.bal b/ballerina/encrypt_decrypt.bal index bca9e3c9..b4ced6ab 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; @@ -260,11 +260,26 @@ public isolated function encryptPgp(byte[] plainText, string publicKey, *Options '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|crypto:Error encryptedStream = crypto:encryptStreamAsPgp(inputStream, "public_key.asc"); +# ``` +# +# + 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 { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt" +} external; + # Returns the PGP-decrypted value of the given PGP-encrypted data. # ```ballerina # byte[] message = "Hello Ballerina!".toBytes(); # byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc"); -# +# # byte[] passphrase = check io:fileReadBytes("pass_phrase.txt"); # byte[] decryptedMessage = check crypto:decryptPgp(cipherText, "private_key.asc", passphrase); # ``` @@ -278,3 +293,19 @@ public isolated function decryptPgp(byte[] cipherText, string privateKey, byte[] name: "decryptPgp", '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|crypto:Error decryptedStream = crypto:decryptStreamFromPgp(inputStream, "private_key.asc", passphrase); +# ``` +# +# + inputStream - The encrypted content as a stream +# + privateKey - 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 decryptStreamFromPgp(stream inputStream, string privateKey, + byte[] passphrase) returns stream|Error = @java:Method { + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt" +} external; 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/stream_iterators.bal b/ballerina/stream_iterators.bal new file mode 100644 index 00000000..b936b6e1 --- /dev/null +++ b/ballerina/stream_iterators.bal @@ -0,0 +1,81 @@ +// 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 { + 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/ballerina/tests/encrypt_decrypt_pgp_test.bal b/ballerina/tests/encrypt_decrypt_pgp_test.bal index 2451177c..0b84d0e6 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 {} @@ -41,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"); } @@ -55,8 +56,85 @@ 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 testEncryptAndDecryptStreamWithPgp() returns error? { + byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + 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 = []; + check from byte[] bytes in decryptedStream + do { + actual.push(...bytes); + }; + test:assertEquals(actual, expected); +} + +@test:Config { + serialExecution: true +} +isolated function testEncryptAndDecryptStreamWithPgpWithOptions() returns error? { + byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes(); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + 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 = []; + check from byte[] bytes in decryptedStream + do { + actual.push(...bytes); + }; + test:assertEquals(actual, expected); +} + +@test:Config { + serialExecution: true +} +isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPrivateKey() returns error? { + byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + 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(); + 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"); + } +} + +@test:Config { + serialExecution: true +} +isolated function testNegativeEncryptAndDecryptStreamWithPgpInvalidPassphrase() returns error? { + byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes(); + stream inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT); + 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(); + 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"); + } +} 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..8f1a9a0f 100644 --- a/ballerina/tests/test_utils.bal +++ b/ballerina/tests/test_utils.bal @@ -14,27 +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 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/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 2f59bdae..06f57fe2 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/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 = "@toml.version@" path = "../native/build/libs/crypto-native-@project.version@.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcpkix-jdk18on" version = "@bouncycastle.version@" path = "./lib/bcpkix-jdk18on-@bouncycastle.version@.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcprov-jdk18on" version = "@bouncycastle.version@" path = "./lib/bcprov-jdk18on-@bouncycastle.version@.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcutil-jdk18on" version = "@bouncycastle.version@" path = "./lib/bcutil-jdk18on-@bouncycastle.version@.jar" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "org.bouncycastle" artifactId = "bcpg-jdk18on" version = "@bouncycastle.version@" 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/changelog.md b/changelog.md index f4a09171..1abac4da 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 streams](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) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 7c5850ae..40d51519 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,43 @@ 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 stream, encrypt it using the PGP public +key and return an encrypted stream + +```ballerina +stream inputStream = check io:fileReadBlocksAsStream("input.txt"); +stream|crypto:Error encryptedStream = crypto:encryptStreamAsPgp(inputStream, "public_key.asc"); +``` + ### 5.2. [Decryption](#52-decryption) #### 5.2.1. [RSA-ECB](#521-rsa-ecb) @@ -574,6 +612,29 @@ 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 stream, decrypt it using the +PGP private key and passphrase and return a decrypted stream. + +```ballerina +stream inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt"); +stream|crypto:Error decryptedStream = crypto:decryptStreamFromPgp(inputStream, "private_key.asc", passphrase); +``` + ## 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. diff --git a/gradle.properties b/gradle.properties index ac5ea3e8..7a2f95cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,15 @@ 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 +githubSpotbugsVersion=6.0.18 githubShadowVersion=7.1.2 undercouchDownloadVersion=5.4.0 researchgateReleaseVersion=2.8.0 ballerinaGradlePluginVersion=2.0.1 nativeImageVersion=22.2.0 -ballerinaLangVersion=2201.9.0 -stdlibTimeVersion=2.4.0 +ballerinaLangVersion=2201.10.0-20240926-231800-8a5a4343 +stdlibTimeVersion=2.5.1-20240930-120200-e59222b +stdlibIoVersion=1.6.2-20240928-084100-656404f diff --git a/native/build.gradle b/native/build.gradle index ed242952..55e1105d 100644 --- a/native/build.gradle +++ b/native/build.gradle @@ -45,8 +45,11 @@ checkstyle { checkstyleMain.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") spotbugsMain { - effort "max" - reportLevel "low" + def classLoader = plugins["com.github.spotbugs"].class.classLoader + def SpotBugsConfidence = classLoader.findLoadedClass("com.github.spotbugs.snom.Confidence") + def SpotBugsEffort = classLoader.findLoadedClass("com.github.spotbugs.snom.Effort") + effort = SpotBugsEffort.MAX + reportLevel = SpotBugsConfidence.LOW reportsDir = file("$project.buildDir/reports/spotbugs") reports { html.enabled true 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..08fb2b60 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/BallerinaInputStream.java @@ -0,0 +1,127 @@ +/* + * 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.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; + +/** + * Represents a Ballerina stream as an {@link InputStream}. + * + * @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_READING_THE_STREAM = "Error occurred while reading the next " + + "element from the stream"; + 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; + this.environment = environment; + } + + @Override + public int read() throws IOException { + if (endOfStream) { + return -1; + } + if (Objects.isNull(buffer) || !buffer.hasRemaining()) { + boolean result = pollNext(); + if (!result) { + endOfStream = true; + return -1; + } + } + return buffer.get() & 0xFF; + } + + @Override + public void close() throws IOException { + Object result = callBalStreamMethod(BAL_STREAM_CLOSE); + if (result instanceof BError bError) { + throw new IOException((bError).getMessage()); + } + } + + public Object getNext() { + return callBalStreamMethod(BAL_STREAM_NEXT); + } + + private Object callBalStreamMethod(String functionName) { + return environment.getRuntime().call(ballerinaStream.getIteratorObj(), functionName); + } + + @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 52ffec48..faf301a5 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,15 @@ private Constants() {} public static final String GCM = "GCM"; public static final String AES = "AES"; public static final String RSA = "RSA"; + + 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 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 3b188291..810d0470 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; @@ -48,12 +49,17 @@ import java.util.Objects; import java.util.Optional; +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; + /** * Provides functionality for PGP decryption operations. * * @since 2.7.0 */ -public class PgpDecryptionGenerator { +public final class PgpDecryptionGenerator { static { if (Objects.isNull(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME))) { @@ -86,14 +92,19 @@ 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); 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; @@ -109,9 +120,17 @@ 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); + return new KeyEncryptedResult(pgpPrivateKey.get(), publicKeyEncryptedData); + } + + private record KeyEncryptedResult(PGPPrivateKey pgpPrivateKey, PGPPublicKeyEncryptedData publicKeyEncryptedData) { + } + + 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. @@ -123,42 +142,63 @@ public Object decrypt(byte[] encryptedBytes) throws PGPException, IOException { } } - private static void decrypt(OutputStream clearOut, Optional 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)) { + private static void decrypt(OutputStream clearOut, PGPPrivateKey pgpPrivateKey, + PGPPublicKeyEncryptedData publicKeyEncryptedData) throws IOException, PGPException { + 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(); + JcaPGPObjectFactory decCompObjFac = new JcaPGPObjectFactory(decryptedCompressedIn); + PGPCompressedData pgpCompressedData = (PGPCompressedData) decCompObjFac.nextObject(); - try (InputStream compressedDataStream = new BufferedInputStream(pgpCompressedData.getDataStream())) { - JcaPGPObjectFactory pgpCompObjFac = new JcaPGPObjectFactory(compressedDataStream); + try (InputStream compressedDataStream = new BufferedInputStream(pgpCompressedData.getDataStream())) { + JcaPGPObjectFactory pgpCompObjFac = new JcaPGPObjectFactory(compressedDataStream); - Object message = pgpCompObjFac.nextObject(); + 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); - } + 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"); - } - } + } + // Perform the integrity check + if (publicKeyEncryptedData.isIntegrityProtected() && !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) { + iteratorObj.addNativeData(KEY_ENCRYPTED_DATA, publicKeyEncryptedData); + 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 { + throw new PGPException("Unknown message type encountered during decryption"); } } } 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..e9ccff50 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; @@ -48,6 +49,14 @@ 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.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; +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. * @@ -65,21 +74,20 @@ 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, - boolean withIntegrityCheck) { + boolean withIntegrityCheck) { this.compressionAlgorithm = compressionAlgorithm; this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; this.armor = armor; 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); + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm); PGPEncryptedDataGenerator pgpEncryptedDataGenerator = new PGPEncryptedDataGenerator( // Configure the encrypted data generator new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) @@ -95,17 +103,48 @@ 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(); } + public void encryptStream(InputStream publicKeyIn, BObject iteratorObj) + throws IOException, PGPException { + SequentialBufferedPipe pipe = new SequentialBufferedPipe(); + OutputStream encryptOut = pipe.getOutputStream(); + iteratorObj.addNativeData(PIPED_OUTPUT_STREAM, encryptOut); + InputStream pipedInputStream = pipe.getInputStream(); + 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_STREAM, compressedOutStream); + 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); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - encryptStream(outputStream, inputStream, clearData.length, publicKeyIn); + encryptStream(outputStream, inputStream, publicKeyIn); return ValueCreator.createArrayValue(outputStream.toByteArray()); } } @@ -124,25 +163,31 @@ 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]; + 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; - 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); } } + 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/SequentialBufferedPipe.java b/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java new file mode 100644 index 00000000..4da7162f --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/SequentialBufferedPipe.java @@ -0,0 +1,89 @@ +/* + * 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; +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/Decrypt.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decrypt.java index ffa4adc5..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 @@ -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; @@ -42,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) { @@ -77,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); @@ -90,14 +102,36 @@ 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)) { 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()); + } + } + + public static Object decryptStreamFromPgp(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()); + } + + try (InputStream keyStream = new ByteArrayInputStream(privateKey)) { + InputStream cipherTextStream = new BallerinaInputStream(environment, inputBalStream); + PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(keyStream, passphraseInBytes); + 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); + } 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..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 @@ -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.END_OF_INPUT_STREAM; +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,35 @@ 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()); + } + } + + public static Object encryptStreamAsPgp(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(END_OF_INPUT_STREAM, 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 new file mode 100644 index 00000000..a2cbf3ee --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/StreamUtils.java @@ -0,0 +1,229 @@ +/* + * 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.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 org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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.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; +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. + * + * @since 2.8.0 + */ +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"; + 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() { + } + + public static Object readDecryptedStream(BObject iterator) { + Object stream = iterator.getNativeData(TARGET_STREAM); + if (Objects.isNull(stream) || !(stream instanceof InputStream inputStream)) { + return CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, TARGET_STREAM)); + } + try { + 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) { + 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(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())); + } + } + + public static Object readEncryptedStream(BObject iterator) { + NativeData nativeData = getNativeData(iterator); + + try { + if (Boolean.FALSE.equals(nativeData.endOfStream())) { + writeToOutStream(iterator, nativeData.inputStream(), nativeData.outputStream()); + } + return readFromPipedStream(iterator, nativeData.pipedInStream()); + } catch (IOException e) { + return CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_READING_THE_STREAM, e.getMessage())); + } catch (BError e) { + return e; + } + } + + 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 InputStream pipedInStream)) { + throw CryptoUtils.createError(String.format(NATIVE_DATA_NOT_AVAILABLE_ERROR, PIPED_INPUT_STREAM)); + } + + 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, endOfStream); + } + + private record NativeData(InputStream inputStream, OutputStream outputStream, InputStream pipedInStream, + Boolean endOfStream) { + } + + 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) { + 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 result = inputStream.read(inputBuffer); + if (result == -1) { + iterator.addNativeData(END_OF_INPUT_STREAM, true); + closeEncryptedSourceStreams(iterator); + } + if (result > 0) { + outputStream.write(inputBuffer, 0, result); + } + } + + 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); + closeNativeStream(iterator, COMPRESSED_DATA_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); + closeNativeStream(iterator, COMPRESSED_DATA_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 { + generator.close(); + } catch (IOException e) { + throw CryptoUtils.createError(String.format(ERROR_OCCURRED_WHILE_CLOSING_THE_GENERATOR, e.getMessage())); + } + } +} 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 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. --> - + + + +