Quickstart Guide

In this guide, we will get you started with OpenPGP using PGPainless as quickly as possible.

At first though, you need to decide which API you want to use;

  • PGPainless’ core API is powerful and heavily customizable

  • The SOP API is a bit less powerful, but dead simple to use

The SOP API is the recommended way to go if you just want to get started already.

In case you need more technical documentation, Javadoc can be found in the following places:

SOP API with pgpainless-sop

The Stateless OpenPGP Protocol (SOP) defines a simplistic interface for the most important OpenPGP operations. It allows you to encrypt, decrypt, sign and verify messages, generate keys and add/remove ASCII armor from data. However, it does not yet provide tools for key management. Furthermore, the implementation is deciding for you, which (secure) algorithms to use, and it doesn’t let you change those.

If you want to read more about the background of the SOP protocol, there is a whole chapter dedicated to it.


PGPainless’ releases are published to and can be fetched from Maven Central. To get started, you first need to include pgpainless-sop in your projects build script.

// If you use Gradle
dependencies {
    implementation "org.pgpainless:pgpainless-sop:XYZ"

// If you use Maven


Replace XYZ with the current version, in this case 1.3.6!

The entry point to the API is the SOP interface, for which pgpainless-sop provides a concrete implementation SOPImpl.

// Instantiate the API
SOP sop = new SOPImpl();

Now you are ready to go!

Generate a Key

To generate a new OpenPGP key, the method SOP.generateKey() is your friend:

// generate key
byte[] keyBytes = sop.generateKey()
        .userId("John Doe <john.doe@pgpainless.org>")

The call userId(String userId) can be called multiple times to add multiple user-ids to the key, but it MUST be called at least once. The argument given in the first invocation will become the keys primary user-id.

Optionally, the key can be protected with a password by calling withKeyPassword(String password). If this method is not called, the key will be unprotected.

The generate() method call generates the key and returns a Ready object. This in turn can be used to write the result to a stream via writeTo(OutputStream out), or to get the result as bytes via getBytes(). In both cases, the resulting output will be the UTF8 encoded, ASCII armored OpenPGP secret key.

To disable ASCII armoring, call noArmor() before calling generate().

At the time of writing, the resulting OpenPGP secret key will consist of a certification-capable 256-bits ed25519 EdDSA primary key, a 256-bits ed25519 EdDSA subkey used for signing, as well as a 256-bits X25519 ECDH subkey for encryption.

The whole key does not have an expiration date set.

Extract a Certificate

Now that you generated your secret key, you probably want to share the public key with your contacts. To extract the OpenPGP public key (which we will call certificate from now on) from the secret key, use the SOP.extractCert() method call:

// extract certificate
byte[] certificateBytes = sop.extractCert()

The key(_) method either takes a byte array (like in the example), or an InputStream. In both cases it returns another Ready object from which the certificate can be accessed, either via writeTo(OutputStream out) or getBytes().

By default, the resulting certificate will be ASCII armored, regardless of whether the input key was armored or not. To disable ASCII armoring, call noArmor() before calling key(_).

In our example, certificateBytes can now safely be shared with anyone.

Apply / Remove ASCII Armor

Perhaps you want to print your secret key onto a piece of paper for backup purposes, but you accidentally called noArmor() when generating the key.

To add ASCII armor to some binary OpenPGP data, the armor() API can be used:

// wrap data in ASCII armor
byte[] armoredData = sop.armor()

The data(_) method can either be called by providing a byte array, or an InputStream.


There is a label(ArmorLabel label) method, which could theoretically be used to define the label used in the ASCII armor header. However, this method is not (yet?) supported by pgpainless-sop and will currently throw an UnsupportedOption exception. Instead, the implementation will figure out the data type and set the respective label on its own.

To remove ASCII armor from armored data, simply use the dearmor() API:

// remove ASCII armor
byte[] binaryData = sop.unarmor()

Once again, the data(_) method can be called either with a byte array or an InputStream as argument.

If the input data is not validly armored OpenPGP data, the data(_) method call will throw a BadData exception.

Encrypt a Message

Now lets get to the juicy part and finally encrypt a message! In this example, we will assume that Alice is the sender that wants to send a message to Bob. Beforehand, Alice acquired Bobs certificate, e.g. by fetching it from a key server.

To encrypt a message, you can make use of the encrypt() API:

// encrypt and sign a message
byte[] aliceKey = ...;  // Alice' secret key
byte[] aliceCert = ...; // Alice' certificate (e.g. via extractCert())
byte[] bobCert = ...;   // Bobs certificate

byte[] plaintext = "Hello, World!\n".getBytes(); // plaintext

byte[] ciphertext = sop.encrypt()
        // encrypt for each recipient
        // Optionally: Sign the message
        .withKeyPassword("sw0rdf1sh") // if signing key is protected
        // provide the plaintext

Here you encrypt the message for each recipient (Alice probably wants to be able to decrypt the message too!) by calling withCert(_) with the recipients certificate as argument. It does not matter, if the certificate is ASCII armored or not, and the method can either be called with a byte array or an InputStream as argument.

The API not only supports asymmetric encryption via OpenPGP certificates, but it can also encrypt messages symmetrically using one or more passwords. Both mechanisms can even be used together in the same message! To (additionally or exclusively) encrypt the message for a password, simply call withPassword(String password) before the plaintext(_) method call.

It is recommended (but not required) to sign encrypted messages. In order to sign the message before encryption is applied, call signWith(_) with the signing key as argument. This method call can be repeated multiple times to sign the message with multiple signing keys.

If any keys used for signing are password protected, you need to provide the signing key password via withKeyPassword(_). It does not matter in which order signing keys and key passwords are provided, the implementation will figure out matches on its own. If different key passwords are used, the withKeyPassword(_) method can be called multiple times.

By default, the encrypted message will be ASCII armored. To disable ASCII armor, call noArmor() before the plaintext(_) method call.

Lastly, you need to provide the plaintext by calling plaintext(_) with either a byte array or an InputStream as argument. The ciphertext can then be accessed from the resulting Ready object as usual.

Decrypt a Message

Now let’s switch perspective and help Bob decrypt the message from Alice.

Decrypting encrypted messages is done in a similar fashion using the decrypt() API:

// decrypt a message and verify its signature(s)
byte[] aliceCert = ...; // Alice' certificate
byte[] bobKey = ...;    // Bobs secret key 
byte[] bobCert = ...;   // Bobs certificate

byte[] ciphertext = ...; // the encrypted message

ReadyWithResult<DecryptionResult> readyWithResult = sop.decrypt()
        .withKeyPassword("password123") // if decryption key is protected

The ReadyWithResult<DecryptionResult> can now be processed in two different ways, depending on whether you want the plaintext as bytes or simply write it out to an OutputStream.

To get the plaintext bytes directly, you shall proceed as follows:

ByteArrayAndResult<DecryptionResult> bytesAndResult = readyWithResult.toByteArrayAndResult();
DecryptionResult result = bytesAndResult.getResult();
byte[] plaintext = bytesAndResult.getBytes();

If you instead want to write the plaintext out to an OutputStream, the following code can be used:

OutputStream out = ...;
DecryptionResult result = readyWithResult.writeTo(out);

Note, that in both cases you acquire a DecryptionResult object. This contains information about the message, such as which signatures could successfully be verified.

If you provided the senders certificate for the purpose of signature verification via verifyWith(_), you now probably want to check, if the message was actually signed by the sender by checking result.getVerifications().


Signature verification will be discussed in more detail in section “Verifications”.

If the message was encrypted symmetrically using a password, you can also decrypt is symmetrically by calling withPassword(String password) before the ciphertext(_) method call. This method call can be repeated multiple times. The implementation will try different passwords until it finds a matching one.

Sign a Message

There are three different main ways of signing a message:

  • Inline Signatures

  • Cleartext Signatures

  • Detached Signatures

An inline-signature will be part of the message itself (e.g. like with messages that are encrypted and signed). Inline-signed messages are not human-readable without prior processing.

A cleartext signature makes use of the cleartext signature framework. Messages signed in this way do have an ASCII armor header and footer, yet the content of the message is still human-readable without special software.

Lastly, a detached signature can be distributed as an extra file alongside the message without altering it. This is useful if the plaintext itself cannot be modified (e.g. if a binary file is signed).

The SOP API can generate all of those signature types.


Let’s start with an inline signature:

byte[] signingKey = ...;
byte[] message = ...;

byte[] inlineSignedMessage = sop.inlineSign()
        .mode(InlineSignAs.Text) // or 'Binary'

You can choose between two different signature formats which can be set using mode(InlineSignAs mode). The default value is Binary. You can also set it to Text which signals to the receiver that the data is UTF8 text.


For inline signatures, do NOT set the mode() to CleartextSigned, as that will create message which uses the cleartext signature framework (see further below).

You must provide at least one signing key using key(_) in order to be able to sign the message.

If any key is password protected, you need to provide its password using withKeyPassword(_) which can be called multiple times to provide multiple passwords.

Once you provide the plaintext using data(_) with either a byte array or an InputStream as argument, you will get a Ready object back, from which the signed message can be retrieved as usual.

By default, the signed message will be ASCII armored. This can be disabled by calling noArmor() before the data(_) method call.

Cleartext Signatures

A cleartext-signed message can be generated in a similar way to an inline-signed message, however, there are is one subtle difference:

byte[] signingKey = ...;
byte[] message = ...;

byte[] cleartextSignedMessage = sop.inlineSign()
        .mode(InlineSignAs.CleartextSigned) // This MUST be set


In order to produce a cleartext-signed message, the signature mode MUST be set to CleartextSigned by calling mode(InlineSignAs.CleartextSigned).


Calling noArmor() will have no effect for cleartext-signed messages, so such method call will be ignored.

Detached Signatures

As the name suggests, detached signatures are detached from the message itself and can be distributed separately.

To produce a detached signature, the detachedSign() API is used:

byte[] signingKey = ...;
byte[] message = ...;

ReadyWithResult<SigningResult> readyWithResult = sop.detachedSign()

Here you have the choice, how you want to write out the signature. If you want to write the signature to an OutputStream, you can do the following:

OutputStream out = ...;
SigningResult result = readyWithResult.writeTo(out);

If instead you want to get the signature as a byte array, do this instead:

ByteArrayAndResult<SigningResult> bytesAndResult = readyWithResult.toByteArrayAndResult();
SigningResult result = bytesAndResult.getResult();
byte[] detachedSignature = bytesAndResult.getBytes();

In any case, the detached signature can now be distributed alongside the original message.

By default, the resulting detached signature will be ASCII armored. This can be disabled by calling noArmor() prior to calling data(_).

The SigningResult object you got back in both cases contains information about the signature.

Verify a Signature

In order to verify signed messages, there are two API endpoints available.

Inline and Cleartext Signatures

To verify inline-signed messages, or messages that make use of the cleartext signature framework, use the inlineVerify() API:

byte[] signingCert = ...;
byte[] signedMessage = ...;

ReadyWithResult<List<Verification>> readyWithResult = sop.inlineVerify()

The cert(_) method MUST be called at least once. It takes either a byte array or an InputStream containing an OpenPGP certificate. If you are not sure, which certificate was used to sign the message, you can provide multiple certificates.

It is also possible to reject signatures that were not made within a certain time window by calling notBefore(Date timestamp) and/or notAfter(Date timestamp). Signatures made before the notBefore(_) or after the notAfter(_) constraints will be rejected.

You can now either write out the plaintext message to an OutputStream

OutputStream out = ...;
List<Verifications> verifications = readyWithResult.writeTo(out);

… or you can acquire the plaintext message as a byte array directly:

ByteArrayAndResult<List<Verifications>> bytesAndResult = readyWithResult.toByteArrayAndResult();
byte[] plaintextMessage = bytesAndResult.getBytes();
List<Verifications> verifications = bytesAndResult.getResult();

In both cases, the plaintext message will have the signatures stripped.

Detached Signatures

To verify detached signatures (signatures that come separate from the message itself), you can use the detachedVerify() API:

byte[] signingCert = ...;
byte[] message = ...;
byte[] detachedSignature = ...;

List<Verification> verifications = sop.detachedVerify()

You can provide one or more OpenPGP certificates using cert(_), providing either a byte array or an InputStream.

The detached signatures need to be provided separately using the signatures(_) method call. You can provide as many detached signatures as you like, and those can be binary or ASCII armored.

Like with Inline Signatures, you can constrain the time window for signature validity using notAfter(_) and notBefore(_).


In all above cases, the verifications list will contain Verification objects for each verifiable, valid signature. Those objects contain information about the signatures: verification.getSigningCertFingerprint() will return the fingerprint of the certificate that created the signature. verification.getSigningKeyFingerprint() will return the fingerprint of the used signing subkey within that certificate.

Detach Signatures from Messages

It is also possible, to detach inline or cleartext signatures from signed messages to transform them into detached signatures. The same way you can turn inline or cleartext signed messages into plaintext messages.

To detach signatures from messages, use the inlineDetach() API:

byte[] signedMessage = ...;

ReadyWithResult<Signatures> readyWithResult = sop.inlineDetach()
ByteArrayAndResult<Signatures> bytesAndResult = readyWithResult.toByteArrayAndResult();

byte[] plaintext = bytesAndResult.getBytes();
Signatures signatures = bytesAndResult.getResult();
byte[] encodedSignatures = signatures.getBytes();

By default, the signatures output will be ASCII armored. This can be disabled by calling noArmor() prior to message(_).

The detached signatures can now be verified like in the section above.

PGPainless API with pgpainless-core

The pgpainless-core module contains the bulk of the actual OpenPGP implementation.

This is a quickstart guide. For more in-depth exploration of the API, checkout .


This chapter is work in progress.


PGPainless’ releases are published to and can be fetched from Maven Central. To get started, you first need to include pgpainless-core in your projects build script:

// If you use Gradle
dependencies {
    implementation "org.pgpainless:pgpainless-core:XYZ"

// If you use Maven

This will automatically pull in PGPainless’ dependencies, such as Bouncy Castle.


Replace XYZ with the current version, in this case 1.3.6!

The entry point to the API is the PGPainless class. For many common use-cases, examples can be found in the examples package. There is a very good chance that you can find code examples there that fit your needs.

Read and Write Keys

Reading keys from ASCII armored strings or from binary files is easy:

String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...;
PGPSecretKeyRing secretKey = PGPainless.readKeyRing()

Similarly, keys or certificates can quickly be exported:

// ASCII armored key
PGPSecretKeyRing secretKey = ...;
String armored = PGPainless.asciiArmor(secretKey);
// binary (unarmored) key
byte[] binary = secretKey.getEncoded();

Generate a Key

PGPainless comes with a method to quickly generate modern OpenPGP keys. There are some predefined key archetypes, but it is possible to fully customize the key generation to fit your needs.

// EdDSA primary key with EdDSA signing- and XDH encryption subkeys
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
        .modernKeyRing("Romeo <romeo@montague.lit>", "thisIsAPassword");

// RSA key without additional subkeys
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
        .simpleRsaKeyRing("Juliet <juliet@montague.lit>", RsaLength._4096);

As you can see, it is possible to generate all kinds of different keys.

Extract a Certificate

If you have a secret key, you might want to extract a public key certificate from it:

PGPSecretKeyRing secretKey = ...;
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey);

Apply / Remove ASCII Armor

ASCII armor is a layer of radix64 encoding that can be used to wrap binary OpenPGP data in order to make it save to transport via text-based channels (e.g. email bodies).

The way in which ASCII armor can be applied depends on the type of data that you want to protect. The easies way to ASCII armor an OpenPGP key or certificate is by using PGPainless’ asciiArmor() method:

PGPPublicKey certificate = ...;
String asciiArmored = PGPainless.asciiArmor(certificate);

If you want to ASCII armor ciphertext, you can enable ASCII armoring during encrypting/signing by requesting PGPainless to armor the result:

ProducerOptions producerOptions = ...; // prepare as usual (see next section)

producerOptions.setAsciiArmor(true); // enable armoring

EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()


If you have an already encrypted / signed binary message and want to add ASCII armoring retrospectively, you need to make use of BouncyCastle’s ArmoredOutputStream as follows:

InputStream binaryOpenPgpIn = ...; // e.g. new ByteArrayInputStream(binaryMessage);

OutputStream output = ...; // e.g. new ByteArrayOutputStream();
ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(output);

Streams.pipeAll(binaryOpenPgpIn, armorOut);
armorOut.close(); // important!

The output stream will now contain the ASCII armored representation of the binary data.

If the data you want to wrap in ASCII armor is non-OpenPGP data (e.g. the String “Hello World!”), you need to use the following code:

InputStream inputStream = ...;
OutputStream output = ...;

EncryptionStream armorStream = PGPainless.encryptAndOrSign()

Streams.pipeAll(inputStream, armorStream);

To remove ASCII armor, you can make use of BouncyCastle’s ArmoredInputStream as follows:

InputStream input = ...; // e.g. new ByteArrayInputStream(armoredString.getBytes(StandardCharsets.UTF8));
OutputStream output = ...;

ArmoredInputStream armorIn = new ArmoredInputStream(input);
Streams.pipeAll(armorIn, output);

The output stream will now contain the binary OpenPGP data.

Encrypt and/or Sign a Message

Encrypting and signing messages is done using the same API in PGPainless. The type of action depends on the configuration of the ProducerOptions class, which in term accepts SigningOptions and EncryptionOptions objects:

// Encrypt only
ProducerOptions options = ProducerOptions.encrypt(encryptionOptions);

// Sign only
ProducerOptions options = ProducerOptions.sign(signingOptions);

// Sign and encrypt
ProducerOptions options = ProducerOptions.signAndEncrypt(signingOptions, encryptionOptions);

The ProducerOptions object can then be passed into the encryptAndOrSign() API:

InputStream plaintext = ...; // The data that shall be encrypted and/or signed
OutputStream ciphertext = ...; // Destination for the ciphertext

EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
        .withOptions(options); // pass in the options object

Streams.pipeAll(plaintext, encryptionStream); // pipe the data through
encryptionStream.close(); // important! Close the stream to finish encryption/signing

EncryptionResult result = encryptionStream.getResult(); // metadata

The ciphertext output stream now contains the encrypted and/or signed data.

Now lets take a look at the configuration of the SigningOptions object and how to instruct PGPainless to add a simple signature to the message:

PGPSecretKeyRing signingKey = ...; // Key used for signing
SecretKeyRingProtector protector = ...; // Protector to unlock the signing key

SigningOptions signOptions = SigningOptions.get()
        .addSignature(protector, signingKey);

This will add an inline signature to the message.

It is possible to add multiple signatures from different keys by repeating the addSignature() method call.

If instead of an inline signature, you want to create a detached signature instead (e.g. because you do not want to alter the data you are signing), you can add the signature as follows:

signOptions.addDetachedSignature(protector, signingKey);

Passing in the SigningOptions object like this will result in the signature not being added to the message itself. Instead, the signature can later be acquired from the EncryptionResult object via EncryptionResult.getDetachedSignatures(). That way, it can be distributed independent of the message.

The EncryptionOptions object can be configured in a similar way:

PGPPublicKey certificate = ...;

EncryptionOptions encOptions = EncryptionOptions.get()

Once again, it is possible to add multiple recipients by repeating the addRecipient() method call.

You can also encrypt a message to a password like this:


Both methods can be used in combination to create a message which can be decrypted with either a recipients secret key or the passphrase.

Decrypt and/or Verify a Message

Decryption and verification of a message is both done using the same API. Whether a message was actually signed / encrypted can be determined after the message has been processed by checking the OpenPgpMetadata object which can be obtained from the DecryptionStream.

To configure the decryption / verification process, the ConsumerOptions object is used:

PGPPublicKeyRing verificationCert = ...; // optional, signers certificate for signature verification
PGPSecretKeyRing decryptionKey = ...; // optional, decryption key

ConsumerOptions options = ConsumerOptions.get()
        .addVerificationCert(verificationCert) // add a verification cert for signature verification
        .addDecryptionKey(decryptionKey); // add a secret key for message decryption

Both verification certificates and decryption keys are optional. If you know the message is signed, but not encrypted you can omit providing a decryption key. Same goes for if you know that the message is encrypted, but not signed. In this case you can omit the verification certificate.

On the other hand, providing these parameters does not hurt. PGPainless will ignore unused keys / certificates, so if you provide a decryption key and the message is not encrypted, nothing bad will happen.

It is possible to provide multiple verification certs and decryption keys. PGPainless will pick suitable ones on the fly. If the message is signed with key 0xAAAA and you provide certificates 0xAAAA and 0xBBBB, it will verify with cert 0xAAAA and ignore 0xBBBB.

To do the actual decryption / verification of the message, do the following:

InputStream ciphertext = ...; // encrypted and/or signed message
OutputStream plaintext = ...; // destination for the plaintext

ConsumerOptions options = ...; // see above
DecryptionStream consumerStream = PGPainless.decryptAndOrVerify()

Streams.pipeAll(consumerStream, plaintext);
consumerStream.close(); // important!

// The result will contain metadata of the message
OpenPgpMetadata result = consumerStream.getResult();

After the message has been processed, you can consult the OpenPgpMetadata object to determine the nature of the message:

boolean wasEncrypted = result.isEncrypted();
SubkeyIdentifier decryptionKey = result.getDecryptionKey();
Map<SubkeyIdentifier, PGPSignature> validSignatures = result.getVerifiedSignatures();
boolean wasSignedByCert = result.containsVerifiedSignatureFrom(certificate);

// For files:
String fileName = result.getFileName();
Date modificationData = result.getModificationDate();

Verify a Signature

In some cases, detached signatures are distributed alongside the message. This is the case for example with Debians Release and Release.gpg files. Here, Release is the plaintext message, which is unaltered by the signing process while Release.gpg contains the detached OpenPGP signature.

To verify a detached signature, you need to call the PGPainless API like this:

InputStream plaintext = ...; // e.g. new FileInputStream(releaseFile);
InputStream detachedSignature = ...; // e.g. new FileInputStream(releaseGpgFile);
PGPPublicKeyRing certificate = ...; // e.g. debians public signing key

ConsumerOptions options = ConsumerOptions.get()
        .addVerificationCert(certificate) // provide certificate for verification
        .addVerificationOfDetachedSignatures(detachedSignature) // provide detached signature

DecryptionStream verificationStream = PGPainless.decryptAndOrVerify()

Streams.drain(verificationStream); // push all the data through the stream
verificationStream.close(); // finish verification

OpenPgpMetadata result = verificationStream.getResult(); // get metadata of signed message
assertTrue(result.containsVerifiedSignatureFrom(certificate)); // check if message was in fact signed