Make using Bouncy Castle with OpenPGP fun again!
Now with key generation! #
After more than one year of (slow..) development I am happy to announce the release of bouncy-gpg 2.2.0
.
Sample usage #
For those that do not know bouncy-gpg
, it is a java library that greatly simplifies using gpg (rfc4880) encryption in java.
The following two snippets demonstrate how to encrypt and sign a message and later to decrypt the message while checking for the signature.
Please note how bouncy-gpg conveniently wraps an OutputStream
so that anything written into it will be encrypted and signed:
final String original_message = "I love deadlines. I like the whooshing sound they make as they fly by. Douglas Adams";
// Most likely you will use one of the KeyringConfigs.... methods.
// These are wrappers for the test.
KeyringConfig keyringConfigOfSender = Configs
.keyringConfigFromResourceForSender();
ByteArrayOutputStream result = new ByteArrayOutputStream();
try (
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(result);
final OutputStream outputStream = BouncyGPG
.encryptToStream()
.withConfig(keyringConfigOfSender)
.withStrongAlgorithms()
.toRecipients("recipient@example.com", "sender@example.com")
.andSignWith("sender@example.com")
.binaryOutput()
.andWriteTo(bufferedOutputStream);
// Maybe read a file or a webservice?
final ByteArrayInputStream is = new ByteArrayInputStream(original_message.getBytes())
) {
Streams.pipeAll(is, outputStream);
// It is very important that outputStream is closed before the result stream is read.
// The reason is that GPG writes the signature at the end of the stream.
// This is triggered by closing the stream.
// In this example outputStream is closed via the try-with-resources mechanism of Java
}
result.close();
byte[] chipertext = result.toByteArray();
Please also note how bouncy-gpg also conveniently (seeing a pattern here?) provides an InputStream
that streams plaintext from a wrapped ciphertext InputStream
:
//////// Now decrypt the stream and check the signature
// Most likely you will use one of the KeyringConfigs.... methods.
// These are wrappers for the test.
KeyringConfig keyringConfigOfRecipient = Configs
.keyringConfigFromResourceForRecipient();
final OutputStream output = new ByteArrayOutputStream();
try (
final InputStream cipherTextStream = new ByteArrayInputStream(chipertext);
final BufferedOutputStream bufferedOut = new BufferedOutputStream(output);
final InputStream plaintextStream = BouncyGPG
.decryptAndVerifyStream()
.withConfig(keyringConfigOfRecipient)
.andRequireSignatureFromAllKeys("sender@example.com")
.fromEncryptedInputStream(cipherTextStream)
) {
Streams.pipeAll(plaintextStream, bufferedOut);
}
output.close();
final String decrypted_message = new String(((ByteArrayOutputStream) output).toByteArray());
New: Key generation #
Before version 2.2.0 bouncy-gpg could read the gnupg data storage (for gnupg pre 2.1), and work with exported keys (gpg --export
and gpg --export-secret-key
).
This worked for a lot of use cases. Key generation was still missing though. Release 2.2.0 changes that by providing a convenient and (as foolproof as hopefully possible) API to generate RSA keys.
As a side note: I strongly advise against using secring.gpg
or pubring.gpg
for production systems. These files are an undocumented API of gnupg: usable, but can change or show unexpected results. E.g. with GPG 2.1 the secring.gpg
file gets no longer updated and will provide you with stale data (emphasis by me):
To ease the migration to the no-secring method, gpg detects the presence of a secring.gpg and converts the keys on-the-fly to the the key store of gpg-agent (this is the private-keys-v1.d directory below the GnuPG home directory (~/.gnupg)). This is done only once and an existing secring.gpg is then not anymore touched by gpg. This allows co-existence of older GnuPG versions with GnuPG 2.1. However, any change to the private keys using the new gpg will not show up when using pre-2.1 versions of GnuPG and vice versa.
Most applications should manage their keys in an application specific database. Though this might seem more complex than just using the existing keyring files it has a some nice advantages:
- No dependency on the gpg executable
- Keys can be managed remotely (e.g. via the applications database)
- Key management is enforced to happen via the application
- Key management for distributed (scale out / cloud) systems is much easier when keys are not managed by the operating system
Although using exported keys works for many use cases, it is not ideal. The best way is to completely keep all key operations inside your application.
The most straight forward way to create a key is to call BouncyGPG::createSimpleKeyring()
:
final KeyringConfig rsaKeyRing = BouncyGPG.createSimpleKeyring()
.simpleRsaKeyRing("Juliet Capulet <juliet@example.com>", RsaLength.RSA_3072_BIT);
Here is a more complex case with dedicated subkeys for signing, encryption, and authentication:
final KeySpec signingSubey = KeySpecBuilder
.newSpec(RSAForSigningKeyType.withLength(RsaLength.RSA_2048_BIT))
.allowKeyToBeUsedTo(KeyFlag.SIGN_DATA)
.withDefaultAlgorithms();
final KeySpec authenticationSubey = KeySpecBuilder
.newSpec(RSAForEncryptionKeyType.withLength(RsaLength.RSA_2048_BIT))
.allowKeyToBeUsedTo(KeyFlag.AUTHENTICATION)
.withDefaultAlgorithms();
final KeySpec encryptionSubey = KeySpecBuilder
.newSpec(RSAForEncryptionKeyType.withLength(RsaLength.RSA_2048_BIT))
.allowKeyToBeUsedTo(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)
.withDefaultAlgorithms();
final KeySpec masterKey = KeySpecBuilder.newSpec(
RSAForSigningKeyType.withLength(RsaLength.RSA_3072_BIT)
)
.allowKeyToBeUsedTo(KeyFlag.CERTIFY_OTHER)
.withDetailedConfiguration()
.withPreferredSymmetricAlgorithms(
PGPSymmetricEncryptionAlgorithms.recommendedAlgorithms()
)
.withPreferredHashAlgorithms(
PGPHashAlgorithms.recommendedAlgorithms()
)
.withPreferredCompressionAlgorithms(
PGPCompressionAlgorithms.recommendedAlgorithms()
)
.withFeature(Feature.MODIFICATION_DETECTION)
.done();
final KeyringConfig complexKeyRing = BouncyGPG
.createKeyring()
.withSubKey(signingSubey)
.withSubKey(authenticationSubey)
.withSubKey(encryptionSubey)
.withMasterKey(masterKey)
.withPrimaryUserId("Juliet Capulet <juliet@example.com>")
.withPassphrase(Passphrase.fromString("O Romeo, Romeo! Wherefore art thou Romeo?"))
.build();
return complexKeyRing;
There is one caveat though: Generating ECC keys is not possible (though the API is already provided). I’ll add it as soon as I have time (and leasure) or someone brings along a PR.
Persisting generated keys #
The bouncy castle functions can be used to persist keys, e.g. in the database:
// 1. Create a new keyring with new keys
KeyringConfig createdKeyRing = ...
// 2. Get the persisted keys in a binary format
ByteArrayOutputStream pubKeyRingBuffer = new ByteArrayOutputStream();
createdKeyRing.getPublicKeyRings().encode(pubKeyRingBuffer);
pubKeyRingBuffer.close();
byte[] publicKey = pubKeyRingBuffer.toByteArray();
ByteArrayOutputStream secretKeyRingBuffer = new ByteArrayOutputStream();
createdKeyRing.getSecretKeyRings().encode(secretKeyRingBuffer);
secretKeyRingBuffer.close();
byte[] secretKey = secretKeyRingBuffer.toByteArray();
// 2.a
// now persist secretKey and publicKey into a database
// 3. load the persisted keys
InMemoryKeyring memoryKeyring = KeyringConfigs.forGpgExportedKeys(keyId -> PASSPHRASE.toCharArray());
memoryKeyring.addPublicKey(publicKey);
memoryKeyring.addSecretKey(secretKey);
}
The previous sample would require you to store secretKey
and publicKey
as binary objects. If you’d rather have ASCII armored key use the following snippet:
// 1. Create a new keyring with new keys
KeyringConfig createdKeyRing = ...
// 2. Get the persisted keys in a ascii format
String publicKey;
try (
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ArmoredOutputStream armored = new ArmoredOutputStream(buffer);
) {
createdKeyRing.getPublicKeyRings().encode(armored);
armored.close();
buffer.close();
publicKey = new String(buffer.toByteArray(), StandardCharsets.US_ASCII);
}
String secretKey;
try (
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ArmoredOutputStream armored = new ArmoredOutputStream(buffer);
) {
createdKeyRing.getSecretKeyRings().encode(armored);
armored.close();
buffer.close();
secretKey = new String(buffer.toByteArray(), StandardCharsets.US_ASCII);
}
// 2.a
// now persist secretKey and publicKey into a database
// 3. load the persisted keys
InMemoryKeyring memoryKeyring = KeyringConfigs.forGpgExportedKeys(keyId -> PASSPHRASE.toCharArray());
memoryKeyring.addPublicKey(publicKey.getBytes());
memoryKeyring.addSecretKey(secretKey.getBytes());
Closing thoughts #
I made the first commit to bouncy-gpg in September 2015. Since then more than 4 years and more than 400 commits have passed. According to bintray bouncy-gpg had ~75,950 downloads in 2019. Admittingly, bouncy-gpg is more of a nice product that solves a problem that not many people face. But for those who do use it, it makes a difference. I think this is reason enough to continue the project.