Skip to content

DecryptingFiles

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

Decrypting Files

To decrypt a message, create a new Decryptor object. Add to its ring the secret key of each expected message recipient, and the public key of each expected message signer. Then run the decrypt() method of the Decryptor -- this will attempt to decrypt the message using one of the secret keys on the ring flagged as forDecryption (and that has its passphrase supplied), and verify the message using one of the keys on the ring flagged as forVerification. See KeyRings for examples of how to load keys and manipulate their usage flags.

Note that you can reuse Decryptor objects (including sharing the same Decryptor among multiple threads) -- just be careful with MultiThreading.

The following examples will all use a Decryptor configured with Alice's secret key and Bob's public key (so as to decrypt messages for Alice, and verify that they have been signed by either Alice or Bob):

Decryptor decryptor = new Decryptor(
    new Key(new File("/path/to/alice-sec.gpg"), "password123"),
    new Key(new File("/path/to/bob-pub.gpg"))
);

Note that you need to first supply the passphrase for a secret key before using it to sign or decrypt a message. In the above example, the passphrase password123 was supplied to the Key constructor for Alice's key (which will be used as the passphrase for all of that key's subkeys).

If you attempt to decrypt a message that was not encrypted for a secret key on the Decryptor object's ring (or is on the ring, but without a passphrase supplied or not flagged as forDecryption), a DecryptionException will be raised with an exception message "no suitable decryption key found".

Decrypting Files

You can decrypt a file by supplying the existing "ciphertext" (encrypted) version of the file as one java.io.File object, and the desired location of the "plaintext" (decrypted) version of the file as a second java.io.File object. If the second file already exists, it will be overwritten (and you'll get an IOException if you try to use the same location for both the plaintext and ciphertext files). For example, this decrypts "path/to/ciphertext.txt.gpg" to "path/to/plaintext.txt":

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

This is equivalent to the following GnuPG command:

gpg --decrypt --output path/to/plaintext.txt path/to/ciphertext.txt.gpg

The file extensions aren't important -- you could simply name the plaintext file "path/to/foo" and ciphertext "path/to/bar". Typically an extension of .gpg (like foo.txt.gpg) is used to indicate a file decrypted as a PGP message, and an extension of .asc (like foo.txt.asc) to indicate a file decrypted as a PGP message and encoded with ASCII Armor (although sometimes these extensions are used interchangeably, or other file-naming conventions are used).

ASCII Armor

ASCII Armor Base64-encodes a PGP message so that it's safe to copy and paste as text into email messages or other files or applications. Despite its name, it doesn't provide any additional security properties (its "armor" protects PGP messages from inadvertent corruption when embedded in other files, rather than "armoring" against decryption or deliberate modification by third parties). ASCII Armor also conveniently declares what kind of data is encoded within its armor via a header and footer like this (making it easier to figure out what you've been given when someone gives you a .gpg or .asc file):

-----BEGIN PGP MESSAGE-----
Version: GnuPG v1

jA0EAwMCRPdXu3qZeLBgySHwRvh2vWI8YHXCNDwHDzkMr6ZoR9iZFDM8gaWyIz1T
x/o=
=AqCM
-----END PGP MESSAGE-----

JPGPJ reads armored key files and encrypted messages transparently, so you don't need to do anything special to decrypt a PGP message encoded with ASCII Armor.

Decrypting Streams

You can decrypt directly from a java.io.InputStream to a java.io.OutputStream. The Decryptor does not call close() on the InputStream nor flush() or close() on the OutputStream (so you should, if appropriate).

InputStream input = new URL("http://example.com/ciphertext.txt.gpg").openStream();

URLConnection post = new URL(
    "http://example.com/plaintext.txt").openConnection();
post.setDoOutput(true);
InputStream output = post.getOutputStream();

decryptor.decrypt(input, output);
input.close();
output.close();

This is equivalent to the following GnuPG command:

curl http://example.com/ciphertext.txt.gpg |
    gpg --decrypt |
    curl --data-binary @- http://example.com/plaintext.txt

Original File Metadata

PGP allows some optional metadata to be supplied with a message, such as the original file's name, last-modified date, and line-ending normalization. You can safely ignore this metadata, but it may be useful for certain use cases -- if you trust the message signer to have supplied legitimate values for the metadata (and even if you do trust the signer, it's still a good idea to validate the metadata contains reasonable values, like the message's modified date isn't from the future, or that the filename doesn't include any slashes, etc).

The following example uses the metadata extracted from the decrypted message to set the decrypted file's last-modified date and name:

File plaintext = new File("path/to/plaintext.txt");
FileMetadata metadata = decryptor.decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    plaintext
);

if (metadata.getLastModified() > 0 &&
        metadata.getLastModified() < System.currentTimeMilis())
    plaintext.setLastModified(metadata.getLastModified());

if (metadata.getName().length() > 0 &&
        Pattern.matches("[\\w\\.\\-]+", metadata.getName())) {
    File renamed = new File("path/to", metdata.getName());
    if (!renamed.exists())
        plaintext.renameTo(renamed);
}

Decrypting Without Verification

When decrypting a message, a Decryptor will also attempt to verify that the message has been signed by at least one of the keys on its ring flagged as forVerification. By default, all keys on a Decryptor object's ring will be flagged as forVerification.

No verification

To avoid verifying a message entirely, set the Decryptor object's verificationRequired property to false:

decryptor.setVerificationRequired(false);
decryptor.decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

This is equivalent to the following GnuPG command:

gpg --decrypt --skip-verify \
    --output path/to/plaintext.txt path/to/ciphertext.txt.gpg

If you do not explicitly set the Decryptor object's verificationRequired property to false and the message was not signed, or was not signed with a key on the Decryptor object's ring flagged as forVerification, a VerificationException will be raised with an exception message "content not signed with a required key".

Limiting the verification keys

To limit only a subset of the keys on a ring to be used for verification, you must explicitly turn off the forVerification flags of the keys for which you don't want to use for verification. A convenient way to do this is to load keys that you only want to decrypt with via the KeyForDecryption class -- this will ensure that the loaded key is used only for decryption, and not for verification:

Decryptor decryptor = new Decryptor(
    new KeyForDecryption(new File("/path/to/alice-sec.gpg"), "password123"),
    new KeyForVerification(new File("/path/to/bob-pub.gpg"))
).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Alternately, you can loop through all the subkeys on a ring and manipulate their forVerification flag individually:

Decryptor decryptor = new Decryptor(
    new Key(new File("/path/to/alice-sec.gpg"), "password123"),
    new Key(new File("/path/to/bob-pub.gpg"))
);
for (Key key: decryptor.getRing().findAll("alice"))
    for (Subkey subkey: key.getSubkeys())
        subkey.setForVerification(false);
decryptor.decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

In the above example, we load Alice's secret key and Bob's public key, and then turn off the forVerification flag on Alice's subkeys -- so the Decryptor will attempt to decrypt files with Alice's key, and verify that they were signed by Bob. In GnuPG, you might accomplish something similar by limiting the keys in your keystore to only Alice's and Bob's (or specifying a custom keyring with --no-default-keyring --keyring path/to/keyring.gpg), and marking Alice's key as never trusted.

See KeyRings#setting-usage-flags for further details on subkey usage flags.

Filling In Usage Flags

To decrypt messages with a key that does not have usage flags set (or has usage flags set differently than are actually used), you can use the KeyForDecryption subclass of the Key class to load the key and automatically flag all of its subkeys for (and only for) decryption. You can also use the KeyForVerification subclass of the Key class to load a key and automatically flag all of its subkeys for (and only for) verification. The following example will always only use Alice's secret key for decryption, and Bob's key for verification, regardless of the keys' embedded usage flags:

Decryptor decryptor = new Decryptor(
    new KeyForDecryption(new File("/path/to/alice-sec.gpg"), "password123"),
    new KeyForVerification(new File("/path/to/bob-sec.gpg"))
).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Compression Options

JPGPJ can handle messages compressed with any of the compression options specified in RFC 4880:

  • Uncompressed
  • ZIP
  • ZLIB
  • BZip2

Symmetric Decryption

If a message has been encrypted with a passphrase (in place of, or in addition to, a key), you can decrypt it using the same passphrase by setting the symmetricPassphrase property of a Decryptor object. For example, if you were to set up a Decryptor without any keys (and turned verification off, since you didn't supply any keys for verification), but provide it with a symmetricPassphrase, the Decryptor will attempt to use the passphrase to decrypt the message:

Decryptor decryptor = new Decryptor();
decryptor.setVerificationRequired(false);
decryptor.setSymmetricPassphrase("the eagle flies at midnight");
decryptor.decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

If you were to set up the Decryptor with Alice's secret key and Bob's public key (like most of the examples on this page), and also supply the Decryptor with a symmetricPassphrase, it will attempt to decrypt the message with Alice's key if the message was encrypted for Alice's key; but if the message was not encrypted with Alice's key, it will attempt to decrypt the message with the provided passphrase (and then verify the message's signature with either Alice's key or Bob's key):

Decryptor decryptor = new Decryptor(
    new Key(new File("/path/to/alice-sec.gpg"), "password123"),
    new Key(new File("/path/to/bob-pub.gpg"))
);
decryptor.setSymmetricPassphrase("the eagle flies at midnight");
decryptor.decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Also, if you're able to load the passphrase as a char[] instead of as a String, you can zero-out the char[] after decryption by calling the clearSecrets() method of the decryptor:

Decryptor decryptor = new Decryptor();
try {
    decryptor.setVerificationRequired(false);
    char[] passphrase = ... // load this as a char[] from some source
    decryptor.setSymmetricPassphraseChars(passphrase);
    decryptor.decrypt(
        new File("path/to/ciphertext.txt.gpg"),
        new File("path/to/plaintext.txt")
    );
} finally {
    decryptor.clearSecrets();
}

Supported Decryption Algorithms

JPGPJ can handle any of the encryption algorithms specified in RFC 4880:

  • Unencrypted
  • IDEA
  • TripleDES
  • CAST5
  • Blowfish
  • AES128
  • AES192
  • AES256
  • Twofish

Plus the Camellia family (RFC 5581):

  • Camellia128
  • Camellia192
  • Camellia256

Decryption Errors

JPGPJ may fail to decrypt a PGP message for a variety of reasons. Here are some common errors you may see:

DecryptionException

JPGPJ will raise a DecryptionException with an error message of "no suitable decryption key found" if it fails to decrypt a message for one of the following reasons:

Decryption key not on ring

One reason a DecryptionException may be raised is that the message was encrypted with a key or keys not on the Decryptor ring. When attempting to decrypt a message, JPGPJ will log an INFO level message of "not found decryption key [subkey long ID]" for every subkey that could be used to decrypt the message but is not on the Decryptor ring. If you see this log message, you need to add one of the missing keys to the Decryptor ring in order to decrypt the encrypted message.

Passphrase not set on decryption key

Another reason a DecryptionException may be raised is that the message was encrypted with a key or keys for which the passphrase has not been set. When attempting to decrypt a message, JPGPJ will log an INFO level message of "not using decryption key [subkey flags and short ID]" for every subkey that could be used to decrypt the message but either doesn't have its passphrase set, or isn't flagged for decryption. If you see this log message, make sure you have set a passphrase for the subkey.

See KeyRings#setting-passphrases for instructions on how to set the passphrase for a subkey.

Decryption subkey not flagged

Another reason a DecryptionException may be raised is that the message was encrypted with a subkey not flagged for encryption/decryption usage. When attempting to decrypt a message, JPGPJ will log an INFO level message of "not using decryption key [subkey flags and short ID]" for every subkey that could be used to decrypt the message but either doesn't have its passphrase set, or isn't flagged for decryption. If you see this log message, and you have set a passphrase for the subkey, try explicitly setting the forDecryption flag on the subkey; or, alternately, using the KeyForDecryption class to load the key, which will automatically set the forDecryption flag on all subkeys that possibly could be used for decryption.

The following example shows how to use the KeyForDecryption class to load Alice's secret key (and use it exclusively for decryption), and the KeyForVerification class to load Bob's public key (and use it exclusively for verification):

Decryptor decryptor = new Decryptor(
    new KeyForDecryption(new File("/path/to/alice-sec.gpg"), "password123"),
    new KeyForVerification(new File("/path/to/bob-pub.gpg"))
).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Symmetric passphrase not set

Another reason a DecryptionException may be raised is that the message was encrypted with a passphrase instead of a public key, and no passphrase was set on the Decryptor. If this is the case, you won't see any "not found decryption key" or "not using decryption key" log messages from the Decryptor. If the message was encrypted with a passphrase instead of a public key, you need to set that passphrase on the Decryptor itself in order to decrypt the message.

See Symmetric Decryption for instructions on how to set a symmetric passphrase.

PassphraseException

JPGPJ will raise a PassphraseException if it fails to decrypt a message for one of the following reasons:

Passphrase incorrect on decryption key

JPGPJ will raise a PassphraseException with an error message of "incorrect passphrase for subkey [subkey flags and short ID]" if it tries to decrypt a message with a secret key that has its passphrase set to an incorrect passphrase. If you see this error message, you're using the wrong passphrase for the key.

Symmetric passphrase incorrect

JPGPJ will raise a PassphraseException with an error message of "incorrect passphrase for symmetric key" if it tries to decrypt a message encrypted with a passphrase, and is using an incorrect passphrase to do so. If you see this error message, you're using the wrong symmetric passphrase.

VerificationException

JPGPJ will raise a VerificationException with an error message of "content not signed with a required key" if it fails to verify a decrypted message for one of the following reasons:

Message not signed

One reason a VerificationException may be raised is that the message was not signed at all. If the message was not signed at all, you won't see any "not found verification key" or "not using verification key" log messages (see below) from the Decryptor.

If message signatures aren't important for your use-case, turn off signature verification as described in the No verification section.

Verification key not on ring

Another reason a VerificationException may be raised is that the message was signed with a key or keys not on the Decryptor ring. When attempting to verify a message, JPGPJ will log an INFO level message of "not found verification key [subkey long ID]" for every subkey that could be used to verify the message but is not on the Decryptor ring. If you see this log message, you need to add one of the missing keys to the Decryptor ring in order to verify the encrypted message.

Verification subkey not flagged

Another reason a VerificationException may be raised is that the message was signed with a subkey not flagged for signing/verification usage. When attempting to verify a message, JPGPJ will log an INFO level message of "not using verification key [subkey flags and short ID]" for every subkey that could be used to verify the message but isn't flagged for verification. If you see this log message, try explicitly setting the forDecryption flag on the subkey; or, alternately, using the KeyForVerification class to load the key, which will automatically set the forVerification flag on all subkeys that possibly could be used for verification.

The following example shows how to use the KeyForDecryption class to load Alice's secret key (and use it exclusively for decryption), and the KeyForVerification class to load Bob's public key (and use it exclusively for verification):

Decryptor decryptor = new Decryptor(
    new KeyForDecryption(new File("/path/to/alice-sec.gpg"), "password123"),
    new KeyForVerification(new File("/path/to/bob-pub.gpg"))
).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/to/plaintext.txt")
);

Bad message signature

JPGPJ will raise a VerificationException with an error message of "bad signature for key [key uid and subkeys]" if it fails to verify a decrypted message because the signature is invalid. This would be the case if the message was tampered with (or it also could be the case that the message was signed incorrectly or in a way incompatible with the current Bouncy Castle PGP implementation).

PGPException

The above exceptions extend from the Bouncy Castle PGPException class. If an error occurs in the low-level mechanics of decrypting a message (like the message has been formatted incorrectly, or is not actually a PGP message), a generic PGPException will be raised.

IOException

JPGPJ will raise an IOException if a general I/O error occurs not specific to the decryption process (like the input file does not exist, or the input stream was cut off unexpectedly).

Overwriting encrypted file

One reason in particular an IOException might be raised is if you try to overwrite the file you're decrypting with the decrypted file. If your use-case calls for decrypting a file "in place", first decrypt the file to a temporary location, and then use the java File.renameTo() or Files.move() methods to replace the encrypted file with the decrypted version.