Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for PGP Encryption/Decryption #555

Merged
merged 13 commits into from
May 13, 2024

Conversation

dinukaamarasinghe817
Copy link
Contributor

@dinukaamarasinghe817 dinukaamarasinghe817 commented May 9, 2024

Purpose

To Fix : ballerina-platform/ballerina-library#6462

Examples

Checklist

  • Linked to an issue
  • Updated the changelog
  • Added tests
  • Updated the spec
  • Checked native-image compatibility

// specific language governing permissions and limitations
// under the License.

public type PgpEncryptionOptions record {|
Copy link
Member

Choose a reason for hiding this comment

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

Add API doc comments to all public types

Copy link
Member

Choose a reason for hiding this comment

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

Shall we just rename this to Options?

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

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

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

@@ -34,6 +40,11 @@
*/
public class Encrypt {

public static final String COMPRESSION_ALGORITHM = "compressionAlgorithm";
Copy link
Member

Choose a reason for hiding this comment

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

Since these constants are anyway converted to BString its better to use following approach. Same for other constants as well.

Suggested change
public static final String COMPRESSION_ALGORITHM = "compressionAlgorithm";
public static final BString COMPRESSION_ALGORITHM = StringUtils.fromString("compressionAlgorithm");

And if there constants are not use anywhere other than here, its better to mark them as private as well.

public static Object encryptPgp(BArray plainTextValue, BArray publicKeyValue, BMap options) {
byte[] plainText = plainTextValue.getBytes();
byte[] publicKey = publicKeyValue.getBytes();
InputStream publicKeyStream = new ByteArrayInputStream(publicKey);
Copy link
Member

Choose a reason for hiding this comment

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

When we create InputStream the best practice is to close them once the required work is done. And I think it is better to do that in the same place where we create the stream using a try-with-resources block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have addressed this in 78e3fb4

byte[] cipherText = cipherTextValue.getBytes();
byte[] privateKey = privateKeyValue.getBytes();
byte[] passphraseInBytes = passphrase.getBytes();
InputStream keyStream = new ByteArrayInputStream(privateKey);
Copy link
Member

Choose a reason for hiding this comment

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

Check whether this stream is properly closed. This comment [1] is valid here as well.

[1] - https://github.com/ballerina-platform/module-ballerina-crypto/pull/555/files#r1596194670

Comment on lines 92 to 94
compressedDataGenerator.close();
cipherOutStream.close();
encryptOut.close();
Copy link
Member

Choose a reason for hiding this comment

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

Rather than manually doing this it is better to use try-with-resources block to close the created IO streams.

PGPPublicKeyEncryptedData publicKeyEncryptedData = null;

Iterator<PGPEncryptedData> encryptedDataItr = pgpEncryptedDataList.getEncryptedDataObjects();
while (pgpPrivateKey == null && encryptedDataItr.hasNext()) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
while (pgpPrivateKey == null && encryptedDataItr.hasNext()) {
while (Objects.isNull(pgpPrivateKey) && encryptedDataItr.hasNext()) {

throw new PGPException("Could not generate PGPPublicKeyEncryptedData object");
}

if (pgpPrivateKey == null) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (pgpPrivateKey == null) {
if (Objects.isNull(pgpPrivateKey)) {


private PGPPrivateKey findSecretKey(long keyID) throws PGPException {
PGPSecretKey pgpSecretKey = pgpSecretKeyRingCollection.getSecretKey(keyID);
return pgpSecretKey == null ? null : pgpSecretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder()
Copy link
Member

Choose a reason for hiding this comment

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

Rather than returning null here can't we use Optionals [1] ?

[1] - https://www.baeldung.com/java-optional

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes we can, and I have addressed this in 78e3fb4

Comment on lines 90 to 93
PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(
keyStream,
passphraseInBytes
);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(
keyStream,
passphraseInBytes
);
PgpDecryptionGenerator pgpDecryptionGenerator = new PgpDecryptionGenerator(
keyStream, passphraseInBytes);

}
}

private static Optional<PGPPublicKey> extractPGPKeyFromRing(PGPPublicKeyRing pgpPublicKeyRing) {
Copy link
Member

Choose a reason for hiding this comment

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

Shall we rename this function to extractPgpKeyFromRing

# + publicKey - Public key
# + options - PGP encryption options
# + return - Encrypted data or else a `crypto:Error` if the key is invalid
public isolated function encryptPgp(byte[] plainText, byte[] publicKey, *PgpEncryptionOptions options)
Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have addressed this in 78e3fb4

// specific language governing permissions and limitations
// under the License.

# Represents the PGP encryption options
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Represents the PGP encryption options
# Represents the PGP encryption options.

boolean withIntegrityCheck = true;
|};

# Represents the compressions algorithms available in PGP
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Represents the compressions algorithms available in PGP
# Represents the compression algorithms available in PGP.

BZIP2= "3"
}

# Represent the symmetric key algorithms available in PGP
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Represent the symmetric key algorithms available in PGP
# Represent the symmetric key algorithms available in PGP.

byte[] cipherText = check encryptPgp(message, publicKey, symmetricKeyAlgorithm = AES_128, armor = false);
byte[] plainText = check decryptPgp(cipherText, privateKey, passphrase);
test:assertEquals(plainText.toBase16(), message.toBase16());
}
Copy link
Member

Choose a reason for hiding this comment

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

Shall we also add a negative test case by creating another PGP key pair, like encrypting with a public key and trying to decrypt using a wrong private key?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added two more test cases for following in here 78e3fb4

  • Invalid Private key for decryption with it's passphrase
  • Valid Private key for decryption with invalid passphrase

if plainText is Error {
test:assertEquals(plainText.message(), "Error occurred while PGP decrypt: Could Not Extract private key");
} else {
test:assertTrue(false, "Should return a crypto Error");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
test:assertTrue(false, "Should return a crypto Error");
test:assertFail("Should return a crypto Error");

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have addressed this in f24bd9a

test:assertEquals(plainText.message(),
"Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes");
} else {
test:assertTrue(false, "Should return a crypto Error");
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been addressed in f24bd9a

Copy link

Quality Gate Passed Quality Gate passed

Issues
1 New issue
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarCloud

@Bhashinee Bhashinee merged commit 2e30356 into ballerina-platform:master May 13, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding support for PGP encryption/decryption in Ballerina
5 participants