Skip to content

KeyRings

Justin Ludwig edited this page Sep 19, 2019 · 7 revisions

Key Rings

A Ring in JPGPJ is a collection of keys -- essentially equivalent to a GnuPG "keyring" (ie what you see when you run the gpg --list-keys command).

A Key in JPGPJ is a collection of subkeys. A key is like an identity (like a person, or persona, or non-human actor, etc), and different subkeys are used by the same identity for different cryptographic purposes (like a hammer is used to hammer nails, a screwdriver is used to tighten screws, etc).

A Subkey in JPGPJ is a public-key pair. It may consist of only the public part of the pair, or it may include both the public and private parts. Each subkey is designated for a specific cryptographic purpose (or purposes), typically either certification (ie signing other keys), encryption, or signing (ie signing messages). The passphrase for a subkey must be provided in order to use its private part (the private part is needed for signing and decryption).

When a key includes only the public part of its public-key pairs, we'll call it a "public key" (this is the version of the key you distribute publicly to everyone). When a key includes both the public and the private part of its public-key pairs, we'll call it a "secret key" (this is the version of the key you keep protected in a secure location).

JPGPJ does not support creating, modifying, or certifying keys (although the underlying Bouncy Castle implementation does). JPGPJ only supports using keys for encrypting, decrypting, signing, and verifying messages.

Importing From GnuPG

See CreatingKeys for how to create a key with GnuPG.

A simple public key file

If you have a key in your default GnuPG keyring for a user Alice <[email protected]>, you can export the public parts with this command:

gpg --export alice > /path/to/alice-pub.gpg

You can load the alice-pub.gpg file as an individual Key in JPGPJ with the following code:

Key alice = new Key(new File("/path/to/alice-pub.gpg"));

An ASCII-Armored key file

You can also export the key in ASCII Armor format (base64-encoded plain text, instead of binary), with this command:

gpg --armor --export alice > /path/to/alice-pub.asc

JPGPJ can load ASCII-Armored keys the same way (using the underlying Bouncy Castle implementation, it can automatically detect the format, so the extension of the file doesn't matter -- it could be alice-pub.asc or alice-pub.gpg or alice-pub.txt or just alice-pub, etc):

Key alice = new Key(new File("/path/to/alice-pub.asc"));

An input stream

You can also load a Key from a raw input stream, like if you downloaded it from the web:

Key alice;
InputStream in = new URL("https://example.com/alice.gpg").connect();
try {
    alice = new Key(in);
} finally {
    in.close();
}

An ASCII-Armored string

Or load a Key from an ASCII-Armored string, like if you embedded it in some code:

String aliceArmored = "" +
"-----BEGIN PGP PUBLIC KEY BLOCK-----" +
"" +
"mQENBFbbwVEBCADx5BwVVL7lrU73U0iYNQkv9dxOAHDEnbitL5FqEjmEk3sClM1H" +
// ...
"=AVaJ" +
"-----END PGP PUBLIC KEY BLOCK-----";
Key alice = new Key(aliceArmored);

A Ring constructor

You can also load a key into a JPGPJ Ring, either via a Ring constructor:

Ring ring = new Ring(new File("/path/to/alice-pub.asc"));

The Ring.load() method

Or loading it into an existing Ring:

Ring ring = new Ring();
ring.load(new File("/path/to/alice-pub.asc"));

A simple secret key file

If you have a key in your default GnuPG keyring for a user Bob <[email protected]>, you can export both the public and private parts with this command:

gpg --export-secret-keys bob > /path/to/bob.gpg

You can load the bob.gpg file as an individual Key in JPGPJ with the following code:

Key alice = new Key(new File("/path/to/bob.gpg"));

All the previous examples also work the same for secret keys (ie the combined public/private parts of a key).

Multiple keys in the same file

You can export multiple keys from GnuPG into the same file. For example, to export both Alice and Bob's public keys from above, you'd use this command:

gpg --armor --export alice bob > /path/to/alice-bob-pub.asc

Then you could import both keys in JPGPJ with the following code:

Ring ring = new Ring(new File("/path/to/alice-bob-pub.asc"));

Multiple keys in multiple files

You can also import keys from multiple files into the same JPGPJ Ring. Like if you export the public keys for Alice and Bob into one file, and the secret key for Carol into another file:

gpg --armor --export alice bob > /path/to/alice-bob-pub.asc
gpg --armor --export-secret-keys carol > /path/to/carol.asc

You could import them all into JPGPJ with the following code:

Ring ring = new Ring(
    new File("/path/to/alice-bob-pub.asc"),
    new File("/path/to/carol.asc")
);

Or:

Ring ring = new Ring();
ring.load(new File("/path/to/alice-bob-pub.asc"));
ring.load(new File("/path/to/carol.asc"));

Or:

Ring ring = new Ring(new File("/path/to/alice-bob-pub.asc"));
ring.load(new File("/path/to/carol.asc"));

Or:

Ring ring = new Ring(new File("/path/to/alice-bob-pub.asc"));
ring.getKeys().add(new Key(new File("/path/to/carol.asc")));

Or:

Ring aliceAndBob = new Ring(new File("/path/to/alice-bob-pub.asc"));
Ring ring = new Ring();
ring.getKeys().addAll(aliceAndBob.getKeys().findAll("alice"));
ring.getKeys().addAll(aliceAndBob.getKeys().findAll("bob"));
ring.load(new File("/path/to/carol.asc"));

Setting Passphrases

To use a secret key to sign a message when encrypting it, or to decrypt a message, you must supply its passphrase. The secret key material will be decrypted on the fly with the passphrase as it's needed (so if it's never used, it will never be decrypted). However, once decrypted, the secret key material will be sitting around in memory until it gets garbage-collected by the JVM and overwritten by something else (so, potentially it may exist in memory for a very long time). Similarly, if you provide the passphrase as a String object, it will sit in memory until the key associated with it is garbage-collected by the JVM and overwritten by something else. Neither the decrypted key nor the passphrase will be written to disk -- unless they're sent to swap by the OS.

However, if you provide the passphrase as a char[] instead of as a String, the passphrase can be zeroed-out after it's used -- and JPGPJ will do that for you automatically if you call the clearSecrets() method of a Subkey (or set the passphrase to a different value). See the Cleaning up memory section for examples.

On individual subkeys

You can set individual passphrases on individual subkeys like this:

Key alice = new Key(new File("/path/to/alice.asc"));
for (Subkey subkey : alice.getSubkeys())
    if (subkey.matches("A123B456"))
        subkey.setPassphrase("password123");
    else if (subkey.matches("C789DEF0"))
        subkey.setPassphrase("correct horse battery staple");

On an entire key

Or if all subkeys of a key use the same passphrase (which is the usual case), you can set them all at once via the Key object, like this:

Key alice = new Key(new File("/path/to/alice.asc"));
alice.setPassphrase("password123");

Or when loading the Key like this:

Key alice = new Key(new File("/path/to/alice.asc"), "password123");

No passphrase

Or to indicate that the secret key is not encrypted with a passphrase, you can flag that on individual subkeys, like this:

Key alice = new Key(new File("/path/to/alice.asc"));
for (Subkey subkey : alice.getSubkeys())
    if (subkey.matches("A123B456"))
        subkey.setNoPassphrase(true);
    else if (subkey.matches("C789DEF0"))
        subkey.setPassphrase("correct horse battery staple");

Or the entire key like this:

Key alice = new Key(new File("/path/to/alice.asc"));
alice.setNoPassphrase(true);

Or when loading the Key like this:

Key alice = new Key(new File("/path/to/alice.asc"), Key.NO_PASSPHRASE);

Cleaning up memory

The best way to provide a passphrase to JPGPJ is as a char[], rather than as a String -- that way, the char[] can be zeroed-out after use. If you call the clearSecrets() method of a Decryptor, Encryptor, Ring, Key, or Subkey, JPGPJ will zero-out the char[] passphrases you supplied for the subkeys (as well as release the secret-key material cached in memory for garbage collection), so that it cannot be recovered from memory later by an attacker.

Here's an example of decrypting a file, and then calling clearSecrets() to zero-out the passphrase:

Key key = new Key(new File("path/to/alice.asc"));
Decryptor decryptor = new Decryptor(key);
char[] passphrase = ... // load this as a char[] from some source
try {
    key.setPassphraseChars(passphrase);
    decryptor.decrypt(
        new File("path/to/ciphertext.txt.gpg"),
        new File("path/back-to/plaintext.txt")
    );
} finally {
    key.clearSecrets();
}

Or another example, where clearSecrets() is called after decryption if the decryptor was successfully constructed, otherwise the passphrase is zeroed-out manually:

Decryptor decryptor = null;
char[] passphrase = ... // load this as a char[] from some source
try {
    decryptor = new Decryptor(
        new Key(new File("path/to/my/keys/alice-pub.gpg")),
        new Key(new File("path/to/my/keys/bob-sec.gpg"), passphrase)
    );
    decryptor.decrypt(
        new File("path/to/ciphertext.txt.gpg"),
        new File("path/back-to/plaintext.txt")
    );
} finally {
    if (decryptor != null)
        decryptor.clearSecrets();
    else
        Arrays.fill(passphrase, (char) 0);
}

Or another example, where the passphrase is always just manually zeroed-out after signing the new encrypted file:

char[] passphrase = ... // load this as a char[] from some source
try {
    new Encryptor(
        new Key(new File("path/to/my/keys/alice-sec.gpg"), passphrase),
        new Key(new File("path/to/my/keys/bob-pub.gpg"))
    ).encrypt(
        new File("path/to/plaintext.txt"),
        new File("path/to/ciphertext.txt.gpg")
    );
} finally {
    Arrays.fill(passphrase, (char) 0);
}

If your own application caches a Key or Ring (or Encryptor or Decryptor) in memory with passphrases set, it would be best to try to call clearSecrets() if possible before your application shuts down (or releases the cached keys). While there's no mechanism in java to guarantee that shut-down or clean-up logic is always run, adding clearSecrets() to your application's usual shut-down/clean-up logic will at least allow cached char[] passphrases to be zeroed under ideal conditions.

For example, you might have a Spring service that caches a Ring, and implements Spring InitializingBean and DisposableBean interfaces to set up and shut down the service. In that case, call clearSecrets() in the shut-down method to zero-out any cached passphrases:

public class MyService implements InitializingBean, DisposableBean {
    private Ring ring;

    public void afterPropertiesSet() throws Exception {
        char[] passphrase = ... // load this as a char[] from some source
        ring = new Ring(
            new Key(new File("path/to/my/keys/alice-sec.gpg"), passphrase),
            new Key(new File("path/to/my/keys/bob-pub.gpg"))
        );
    }

    public void destroy() throws Exception {
        if (ring != null)
            ring.clearSecrets();
    }
}

Setting Usage Flags

When loading a key, JPGPJ will read each subkey's usage flags and apply them to the forSigning, forVerification, forEncryption, and forDecryption fields of the Subkey object. Signing subkeys that include both the public and private parts of the key pair will be flagged as both forSigning and forVerification; whereas signing subkeys that include only the public part will be flagged as forVerification only. Similarly, encryption subkeys that include both the public and private parts will be flagged as both forEncryption and forDecryption; whereas encryption subkeys that include only the public part will be flagged as forEncryption only.

When using a key for signing that has multiple signing subkeys flagged as forSigning, JPGPJ will automatically select the last subkey. This matches typical GnuPG usage of users who have a subkey flagged for both certification and signing (often considered the "master" subkey, with the private part stored offline), as well as an additional signing subkey (used for day-to-day message signing). However, after loading a key, you can programmatically adjust which subkeys to use for signing by manipulating the forSigning flag of its subkeys with the isForSigning() and setForSigning() methods of a Subkey. JPGPJ doesn't persist any changes you make to keys or subkeys.

You can also turn off the usage of a subkey for signing, verification, encryption, or decryption completely (for example if you load a key into a keyring for the exclusive purpose of using it to encrypt a message but not sign it, or vice versa) by manipulating these forSigning, forVerification, forEncryption, and forDecryption flags via their getters and setters (ie isForSigning(), setForSigning(), etc).

Alternately, you can use the following Key subclasses as a convenient way to designate the usage for a key:

  • KeyForSigning: turns off the forVerification, forEncryption, and forDecryption flags for each subkey, and ensures that at least one subkey has the forSigning flag turned on
  • KeyForVerification: turns off the forSigning, forEncryption, and forDecryption flags for each subkey, and turns on the forVerification flag of each subkey
  • KeyForEncryption: turns off the forSigning, forVerification, and forDecryption flags for each subkey, and ensures that at least one subkey has the forEncryption flag turned on
  • KeyForDecryption: turns off the forSigning, forVerification, and forEncryption flags for each subkey, and turns on the forDecryption flag of each subkey

Prevent a key from being used for encryption

For example, say you want to encrypt a message for Alice and Bob, and sign it as Carol -- but not encrypt it for Carol. You could use the KeyForEncryption class to load Alice and Bob's public keys, and the KeyForSinging class to load Carol's secret key:

new Encryptor(
    new KeyForEncryption(new File("/path/to/alice-pub.asc")),
    new KeyForEncryption(new File("/path/to/bob-pub.asc")),
    new KeyForSigning(new File("/path/to/carol.asc", "hunter2"))
).encrypt(
    new File("path/to/plaintext.txt"),
    new File("path/to/ciphertext.txt.gpg")
);

Alternately, if you load Alice and Bob's public keys, and Carol's secret key, into a ring like this:

Ring ring = new Ring(
    new Key(new File("/path/to/alice-pub.asc")),
    new Key(new File("/path/to/bob-pub.asc")),
    new Key(new File("/path/to/carol.asc"), "hunter2")
);

You can turn off the forEncryption usage of Carol's key like this:

for (Key key: ring.findAll("carol"))
    for (Subkey subkey: key.getSubkeys())
        subkey.setForEncryption(false);

Then when you use the ring to encrypt a message, it will be encrypted for just Alice and Bob, not Carol (but still will be signed by Carol):

new Encryptor(ring).encrypt(
    new File("path/to/plaintext.txt"),
    new File("path/to/ciphertext.txt.gpg")
);

Prevent a key from being used for verification

As another example, say you had the secret keys for Alice and Bob, and wanted to use them to decrypt a message, but not trust Alice or Bob's signing of the message (instead, require that Carol had signed the message). You could the KeyForDecryption class to load Alice and Bob's secret keys, and the KeyForVerification class to load Carol's public key:

new Decryptor(
    new KeyForDecryption(new File("/path/to/alice.asc"), "password123"),
    new KeyForDecryption(new File("/path/to/bob.asc"), "b0bru1z!"),
    new KeyForVerification(new File("/path/to/carol-pub.asc"))
).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Alternately, if you load Alice and Bob's secret keys, and Carol's public key, into a ring like this:

Ring ring = new Ring(
    new Key(new File("/path/to/alice.asc"), "password123"),
    new Key(new File("/path/to/bob.asc"), "b0bru1z!"),
    new Key(new File("/path/to/carol-pub.asc"))
);

You can turn off the forVerification usage of Alice and Bob's keys like this:

for (Key key: ring.getKeys())
    if (!key.matches("alice") || !key.matches("bob"))
        for (Subkey subkey: key.getSubkeys())
            subkey.setForVerification(false);

Then when you use this ring to decrypt a message, JPGPJ will be able to decrypt a message encrypted for either Alice or Bob, but will verify that it was signed by Carol:

new Decryptor(ring).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Supported Key Types

Bouncy Castle, and therefore JPGPJ, supports the public-key algorithms specified in RFC 4880:

  • RSA
  • Elgamal
  • DSA
  • ECDH
  • ECDSA