Skip to content

Commit

Permalink
Merge pull request #567 from ballerina-platform/pgp-files
Browse files Browse the repository at this point in the history
[Update 11 Feature] Add support for PGP encryption/decryption with streams
  • Loading branch information
TharmiganK authored Nov 18, 2024
2 parents 16e5127 + df0e19d commit 40658a2
Show file tree
Hide file tree
Showing 22 changed files with 1,011 additions and 92 deletions.
18 changes: 9 additions & 9 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
[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"
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"
Expand Down
29 changes: 26 additions & 3 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

[ballerina]
dependencies-toml-version = "2"
distribution-version = "2201.9.0"
distribution-version = "2201.11.0-20241117-133400-a3054b77"

[[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"},
Expand All @@ -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"
Expand Down Expand Up @@ -67,6 +81,15 @@ name = "lang.object"
version = "0.0.0"
scope = "testOnly"

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

[[package]]
org = "ballerina"
name = "test"
Expand All @@ -84,7 +107,7 @@ modules = [
[[package]]
org = "ballerina"
name = "time"
version = "2.4.0"
version = "2.6.0"
dependencies = [
{org = "ballerina", name = "jballerina.java"}
]
Expand Down
31 changes: 31 additions & 0 deletions ballerina/encrypt_decrypt.bal
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ 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<byte[], error?> inputStream = check io:fileReadBlocksAsStream("input.txt");
# stream<byte[], crypto:Error?>|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<byte[], error?> inputStream, string publicKey,
*Options options) returns stream<byte[], Error?>|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();
Expand All @@ -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<byte[], error?> inputStream = check io:fileReadBlocksAsStream("pgb_encrypted.txt");
# stream<byte[], crypto:Error?>|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<byte[], error?> inputStream, string privateKey,
byte[] passphrase) returns stream<byte[], Error?>|Error = @java:Method {
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt"
} external;
4 changes: 2 additions & 2 deletions ballerina/private_public_key.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down
81 changes: 81 additions & 0 deletions ballerina/stream_iterators.bal
Original file line number Diff line number Diff line change
@@ -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;
}
82 changes: 80 additions & 2 deletions ballerina/tests/encrypt_decrypt_pgp_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// specific language governing permissions and limitations
// under the License.

import ballerina/io;
import ballerina/test;

@test:Config {}
Expand Down Expand Up @@ -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");
}
Expand All @@ -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<byte[], error?> inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT);
stream<byte[], error?> encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH);
stream<byte[], error?> 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<byte[], error?> inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT);
stream<byte[], error?> encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false);
stream<byte[], error?> 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<byte[], error?> inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT);
stream<byte[], error?> encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false);
stream<byte[], error?>|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<byte[], error?> inputStream = check io:fileReadBlocksAsStream(SAMPLE_TEXT);
stream<byte[], error?> encryptedStream = check encryptStreamAsPgp(inputStream, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false);
stream<byte[], error?>|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");
}
}
12 changes: 12 additions & 0 deletions ballerina/tests/resources/sample.txt
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit 40658a2

Please sign in to comment.