When it comes to encryption, Bouncy Castle is probably the most popular library in the Java ecosystem. It has great algorithm support and is actively maintained.
However, when trying to use it to add OpenPGP support to your application, you quickly find yourself digging through StackOverflow posts with walls of hard-to-read source code. To even create a simple encrypted, signed message, you are supposed to wrap at least three different OutputStreams obtained from different Factories, each initialized with tons of obscure parameters such as buffer sizes and Provider classes.
The OpenPGP protocol has a mechanism of signaling algorithm support through fields on the recipients' public key. Are you supposed to extract those yourself? How are you supposed to know which algorithms are safe?
This is exactly the situation I found myself in four years ago. The Bouncy Castle OpenPGP API is mighty and powerful, but unfortunately very low-level and requires you to do the heavy lifting yourself. This is okay-ish if you are familiar with the OpenPGP protocol, but you might not be an expert and merely want to get the job done. Leaving all the security-critical setup work to the consumer of the API is a bad idea.
That's why I decided to create PGPainless. Originally, I was looking for other alternatives and stumbled across a library called bouncy-gpg, but I quickly figured that it was not suiting my needs and decided to go my own way. Nevertheless, bouncy-gpg heavily influenced PGPainless' development, especially in the early phases.
PGPainless internally makes use of Bouncy Castle but hides away much of the complexity by making use of the builder pattern, but on a higher abstraction level than Bouncy Castle. It gets you the job done as quickly as possible, and, if you do not provide specifics such as encryption algorithms, it chooses safe and secure defaults for you.
Especially when it comes to signature verification, many posts on StackOverflow (and even Bouncy Castle's own examples!!) merely show you how to check the correctness of a signature. They do not discuss checking a signature for validity. A signature might be cryptographically correct while being invalid due to a revoked signing key, for example. There is a whole suite of checks that needs to be performed to check if a signature really is valid at a certain reference time. PGPainless performs those checks for you.
Now let's check out some examples, shall we?
Let's start with the first thing every user probably wants to do; Generate a fresh OpenPGP key:
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Romeo <email@example.com>", "p4ssw0rd");
The result is a password-protected OpenPGP key that makes use of modern EdDSA and XDH subkeys. The API abstracted away all the annoying stuff such as which algorithms to use for the keys, which algorithm preferences (hash-, symmetric- and compression algorithms) to set, and, of course, binding the keys and user-id together using binding signatures. Meanwhile, there is a more flexible API
PGPainless.buildKeyRing() which allows the user to change all those parameters to their liking.
Now let's sign and encrypt a message:
// We generated our key in the previous example PGPSecretKeyRing secretKey = ...; SecretKeyRingProtector protector = SecretKeyRingProtector .unlockAnyKeyWith(Passphrase.fromPassword("p4ssw0rd")); // Extract our own public key certificate PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); // Read Juliets public key certificate from somewhere PGPPublicKeyRing julietsCertificate = PGPainless.readKeyRing() .publicKeyRing(julietsCertInputStream); // The message we want to encrypt and sign InputStream plaintextIn = ...; // The destination to where we want to write the ciphertext OutputStream ciphertextOut = ...; // e.g. new ByteArrayOutputStream(); // Set up the stream EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.signAndEncrypt( new EncryptionOptions() .addRecipient(certificate) .addRecipient(julietsCertificate), new SigningOptions() .addInlineSignature(protector, secretKey) ) ); // Encrypt and sign Streams.pipeAll(plaintextIn, encryptionStream); encryptionStream.close(); // Information about the encryption (algorithms, detached signatures etc.) EncryptionResult result = encryptionStream.getResult();
This is still not a one-liner, but it is a huge improvement compared to what you'd otherwise need to do when using Bouncy Castle directly.
Verification and decryption are done in a similar way. If you follow along at home, note that we need Juliet's secret key to decrypt the message and Romeo's certificate to verify the signature.
// Set up the stream DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(encryptedInputStream) .withOptions(new ConsumerOptions() .addDecryptionKey(julietsSecretKey, secretKeyProtector) .addVerificationCert(romeosCertificate) ); // Decrypt and verify the data Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); // Result contains information like signature status etc. OpenPgpMetadata metadata = decryptionStream.getResult();
Internally, PGPainless parses the encrypted message, sets up all the various wrapped streams, checks used algorithms against a sane Policy, decrypts the data, does integrity checks, and verifies the signature correctness and validity by evaluating Romeo's certificate.
The API is capable of way more though, such as symmetric encryption using a passphrase, creating signatures on other users' certificates, modifying keys (adding user-ids and subkeys, changing passphrases, revoking, and expiring keys and user-ids...), and so on. There is an example package that demonstrates many of those use cases.
There is one more thing I want to touch on. The Stateless OpenPGP Protocol is a draft for a standardized OpenPGP command line interface. The draft defines common operations such as generating keys, encrypting/decrypting messages, creating and verifying signatures and so on.
PGPainless provides both a programmatic adaption of this API, as well as a CLI app. This means that if your app needs to do basic OpenPGP operations, you can simply depend on a module called sop-java which defines the programmatic SOP interface (here is a guide). This interface can be implemented by any OpenPGP library, but PGPainless provides its own implementation through the module pgpainless-sop. That way you can benefit from a super simple-to-use OpenPGP API while not being locked in to use PGPainless.
If you are developing an OpenPGP library for the Java ecosystem, please consider creating an implementation of sop-java with it, as this will not only allow your library to be plugged into projects that happen to use sop-java, but also allows your implementation to be plugged into Sequoia-PGP's OpenPGP Interoperability Test Suite, so you can benefit from a great number of test vectors to uncover bugs and interop issues.
Lastly, this is a free software project, meaning all the code is available under the Apache 2.0 license. Development is done both on GitHub and on Codeberg. If you find this project useful, please consider spreading the word and contributing.