Import an encrypted private key into a Java KeyStore

Last month, I talked about parsing a decrypted OpenSSL-formatted RSA key into a JKS-formatted Java Keystore — something that, surprisingly, neither Sun nor Oracle ever bothered to implement in the standard keytool that comes with the JDK. You should probably review that post before you read this one, since I talk there a lot about the Base64-encoded ASN.1-formatted encoding that OpenSSL outputs by default. The utility that I presented in last month's post, though, has one slightly annoying limitation - you must manually decrypt the private key file before you can import it this way. By default, for security reasons, OpenSSL always encrypts the private key even as it's generated. As it turns out, the encryption that it uses is well documented and well standardized, and it's possible to extend my KeyImport utility to decrypt the file in memory, granted (of course) that you have the decryption key handy.

To see how this works, take a look at the contents of a sample private key file, in figure 1 below:

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQItmQ4gm2gJy0CAggA
MBQGCCqGSIb3DQMHBAg9NJTnjKg6aASCBMhp0b5nXyEp4j8ekugAVAdDQqXgHytD
Tgd30CZLkXCO0lZ8dtu394DX+v54GOD5U7esl6/aUL6KQdd23wMeo2uIWVf0bbiT
MmTjtgy58ziV8pVnzTU7yo+PVJpBK7jh4QZjpdKLT3Y2GIQjaF/uQAgabVJpJmx/
hI7KjJ8mXctTcJKvxtqnBpG22La3F2b1YKUMXPbH5iiuK/YKyAg2FdPTYtCh6SyD
Bq2JAwokwrgFo2lUtLl/0ofZW+iDtUUydpvwIbG2jFKAF1LkN4bmIPsQ644NhGL4
EQVDEsnowrChY5gkRsmQ9/6vM+Ywc73uiFi3YXakszDWkOCf+jSg0YtykOo8W3dW
tdmSn/4k4KsGyE/OzDgGRA9SaYrGdcxv9Tf0Ag/aOfU5d7UUBYAA9rnU04haM1zt
oVE/dA86j1XGhZOZo42Sh2V0p2a12BBNZE5nHLZea3ZGb+KmPH2peUpsU0cN1yhK
Q3Ix2qyaSGLzmSZjrz/QR/TUDdnp5neumD1dHCVjcBi6aKhjFXAq32PeQsQ6ZKzZ
9OQN3hvnHhv1NbHC6IOT6vNfqe1yRgiCw9iTJCNBCkN2m05sTMvTjf93naTJfMiC
sR4sKdaM6XGBo3/0U3y6mdQsaXZp1efOFjEhXLVC/XSPaHo+AowSg3JK0m+n6iEX
sqaaAtPo9vAw7hyhWc1D0G3F8wkblKguMcbaFkKRuIHBX2rNWBG+rjOO5/XG6SzA
piPEEyxnix12/X9eMVld+6I0wfgfJ54URRXWzvFovMcSxmaH93TmqdO2StuzAPys
ncltUq2BrAgHKcmZGvw1hxD5sVHNwLcY5tkkbXzG77j5xuzyr5YtSuspmYCeykbg
qdY+dZ1I946e7IRWLoEui1om1nWQ0UhDm0zBUip8m4qWBsGFkiO89+UAwMsnyuss
8PW+OOG1G6lLlZg4xxHorVMvjvPJLRL++XFI1S/L9A5lj23HDbVWrn2cdGsMrn3U
BXjNGrHX6G4A03hgOBox2nNW4rLjGDyplWvYWh2UhjDajl2/W/6a6/fbtRTZK9EH
hv2ms0sXnp5gdeh6n9yW+Rd1+DDJ1ydfFEwi14v3EFqk4MTShl0EK0LP7ob860e3
yYpR5YkX/rSJrZcB767J+pmKU/iKAcTpFmZAJflvMDNETARFi6mczOWeGwwGQHy2
NhgOdZ5Op4AX472aGBSokk0RZmEMRVn1XLhq8jr/SEZyi1g5NjF/o+sNvM9Y1Zcr
FHSdXZZbE9WIHqoHhqjHdK5EwbnkkcFHJw7AEeEoA1Wm8OipFvar143tVz3vBRuo
y5ZIaIrV20RuK/sfcEuDbC3xSkVK4kKW6vMczGy8ySckeQLXU4c+JNmfBqGfHVgM
ugsCDFYf4h4AF1S/jjZ8EilVq2jleWCmIfehl2iSFD1eGZvQkZBzS0vfuJ3vF053
KHMw+A53ZoyP4VeZXfqsJkRfCHdO5uCB/AwyWTO5dxvWhw4HVib6T/Z0KK6zZdYQ
iZXf3HZB8+4g4J/SzRyuWyFNPQBJudBDCXhTv80arUJ/mzcUYqumtGrn0Cq5FIZA
0SwrD3aBShifkNYno7H/rkgn3aWDpHlS5DSUe8+a9OPDjDJsjCDYC9sCYlANupb6
Vh8=
-----END ENCRYPTED PRIVATE KEY-----

Figure 1: Example private key file

If you have already read my previous post, you'll recognize this right away as a Base64-encoding. You might reasonably suspect that the Base64-encoding is of some encrypted gobbledygook that you need an encryption algorithm and a decryption key to make any sense of. As it turns out, you'd be half-right. What you see in figure 1 is a Base64-encoding of ASN.1-encoded information. However, only roughly the last half is an encrypted byte string that you need an encryption algorithm and a decryption key to make any sense of: the first half tells you what the encryption algorithm is and, although it obviously doesn't include the decryption key, it does include some vital information on exactly how to apply that key to the encrypted byte string. By including this information in the encrypted structure, OpenSSL is free to make modifications to the encryption in the future without breaking backward compatibility with other tools like Apache, which need to read this data. In this post, I'll walk through the process of decoding, interpreting the meta-information in the header, and decrypting the private key which makes up the actual payload of the key itself.

Figure 2 is a breakdown of the ASN.1 structure that results from Base64-decoding the encrypted private key of figure 1.

Figure 2: ASN.1 structure of an encrypted private key

I've added line breaks and indentation for a marginal level of readability here, but this is still pretty much impenetrable at this point, even if you understand ASN.1. Recall from my last post that ASN.1 consists of a series of byte-long tag values each followed by a variable length size indication followed by data. The two tags I discussed in last month's post are 0x30, which indicates that a structure of more ASN.1-encoded data is contained in the data element, and 0x02 which indicates that the data should be interpreted as a big-endian two's-complement representation of an integer.

In figure 2, above, I've added indentation to illustrate the structural relationships. Structure elements are shown indented two spaces underneath their parent element. As you can see, it's a deeply-nested structure format — six levels deep (for some reason, ASN.1 users tend to go structure-crazy). There's one 0x02 tag, but two new tags that I didn't discuss last time: 0x04 and 0x06. 0x04 is what ASN.1 just refers to as an OCTET STRING indicating that what follows is arbitrary data; it's up to the reader of the file to recognize what it's for and what to do with it (recall that ASN.1 elements don't have names, just positions). The other new tag, 0x06, is called an object identifier. Object Identifiers are used quite a bit throughout X.509 and the rest of the PKI-related standards; they're used to refer to things that usually have English-sounding names, but which need to be represented unambiguously within the structure. ASN.1 defines a fairly complex hierarchy of object identifiers: users of ASN.1 can register in the OID hierarchy and define new identifiers for their own purposes. In this case, the object identifiers identify, unambiguously, cryptographic protocols. Since this private key was encrypted using the DES3 encryption algorithm, in CBC mode, rather than including a human-readable (and therefore ambiguous) string like "DES-EDE3-CBC", it instead includes the object identifier 2A864886F70D0307 which means precisely that, but in a way that cannot be subject to interpretation.

In fact, you can see that there are three object identifiers in the structure in figure 2:

  • 2A864886F70D01050D
  • 2A864886F70D01050C
  • 2A864886F70D0307
You may notice that they all begin with the prefix 2A864886F70D. This is the namespace assigned to RSA Data Security, Inc.: anything prefixed with that code belongs to (and is specified by) that organization.

There's a complex encoding associated with theses object identifiers: 2A864886F70D actually represents the dotted-decimal 1.2.840.113549.1 which is itself part of a tree-hierarchy of authorities: 1.2 represents the ISO, 840 is the code assigned to ISO members within the US, and finally 113549 is the code assigned to the company named RSA Data Security. The encoding is very compact and complex, but you don't need to understand it to deal with these OIDs: just treat them as opaque data strings that each refer to something very specific. If you're curious to learn more, this tutorial goes into a lot of detail.

If you were to go look up the official ISO specification for each of these OIDS, you'd find that what each of these OIDs actually refer to are the algorithms PBES2, PBKDF2 and DES-EDE3-CBC, respectively. I'll examine each of these in turn.

PBES2 stands for Password-Based Encryption Standard v2.0, which is specified in PKCS #5. PKCS #5 in turn specifies exactly how one should securely go about encrypting data using a passphrase supplied by a human user. That this would require such a detailed specification may seem surprising. When I create a private key, OpenSSL prompts me for a passphrase that I must provide in order to later decrypt the private key. You might expect OpenSSL to take that passphrase and use it directly to create an encryption key for the symmetric encryption algorithm being used, but there are some good reasons why it doesn't. First, most symmetric encryption algorithms — in particular DES3 which is used by OpenSSL to encrypt private keys — needs a very specifically sized key (24 bytes in the case of DES3: no more, no less). If the user's password is longer, it would have to be truncated, and if shorter it would have to be padded. Second, users can only supply passwords using characters that they can type — which excludes over 50% of the range of available bytes.

For these reasons, PKCS #5 mandates that a particular key stretching algorithm be used. In this case PBKDF2 (Password Based Key Derivation Function #2), the second OID you'll find in the encrypted private key structure, was chosen. PBKDF #2 is an algorithm that takes as input a random "salt" value along with a key "seed" (i.e. what the user actually thinks of as the password) and iterates a specific number of times to produce a much stronger key value than the user's human-readable passphrase. The user's supplied passphrase is fed into the PBKDF2 algorithm to produce as many bits as are needed by the encryption algorithm and thus must be fed back into it in order to produce the decryption key. Luckily, Java ships with a PBKDF2 implementation, so you don't have to understand all of the details to use it.

The last OID in the list is the specification of the encryption algorithm itself: DES-EDE3-CBC. the algorithm is DES3, aka triple DES, which is the standard DES algorithm applied three times using three separate eight-byte keys. It's also referred to as DES-EDE because the DES algorithm is applied first in encrypt mode, then in decrypt mode (using a different key) then again in encrypt mode using yet a third key (EDE stands for Encrypt/Decrypt/Encrypt). Again, Java takes care of the implementation details for you, you just have to provide it with the key, the initialization vector, and the encrypted data and it handles the rest.

After all of the header information, there's a long byte string: this is the encyrpted payload, the private key, itself. The goal of this post is to develop some Java software that will apply the first few parameters to the last parameter and produce a decrypted private key (which can then be imported into an existing Java Key Store). Fortunately for you and I, most of this comes standard with Java, so it's just a matter of putting it all together.

Figure 3, below, expands on figure 2 with an explanation of what each element is for. Notice that the nesting of the individual structures implies that each sub-structure is dependent on what precedes it.

PBKDF2 iteration count
30 82 050Etop-level containing structure
  30 40
structure containing unencrypted meta information
    06 09 2A864886F70D01050D
PBES 2 declaration that indicates that what follows is PKCS #5 formatted
    30 33
PBES #2 parameters
      30 1B
Yet another structure (I told you ASN.1 is structure-crazy)
        06 09 2A864886F70D01050C
PBKDF2 key-stretching algorithm
        30 0E
PBKDF2 parameters
          04 08 B66438826DA0272D
PBKDF2 salt
          02 02 0800
          30 14
top-level containing structure
            06 08 2A864886F70D0307
DES-EDE3-CBC
            04 08 3D3494E78CA83A68 
DES Initialization vector
04 82 04C8 69D1BE675F2129E...top-level containing structure

Figure 3: Breakdown of each element of encoded structure

For the purposes of this post, I'm going to cheat just a bit and assume that the structure of the key file will always be exactly like this one. That's a relatively safe assumption, though, since OpenSSL has output this exactly same private key format for as long as it has existed; if they do change it, I'll have to make a few modifications.

Recall from my last post that my overly-simplistic ASN.1 parser went through the structure recursively and just collected a list of all of the integers that it found. I'll extend this same simplistic structure by having it collect not just the integers, but the object IDs and byte strings that it finds — this means that I'm ignoring the structural dependencies of the elements and treating them as if they had just been handed to me in order.

Once I've determined that I'm dealing with DES3 in CBC mode, decrypting is a relatively straightforward matter of invoking the correct JDK APIs as shown in example 1:

private static byte[] decrypt(byte[] key, byte[] iv, byte[] encrypted)	{
  DESedeKeySpec desKeySpec = new DESedeKeySpec(key);
  SecretKeySpec desKey = new SecretKeySpec(desKeySpec.getKey(), "DESede");
  Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
  IvParameterSpec iv = new IvParameterSpec(iv);
  cipher.init(Cipher.DECRYPT_MODE, desKey, iv);
  byte[] decrypted = cipher.doFinal(encrypted);
}

Example 1: Decrypt a 3DES-encoded byte array

The IV (initialization vector) and the encrypted string are part of the ASN.1 structure. The key, of course, isn't; it's derived by applying the PBKDF2 "key stretching" algorithm against the user's input using the provided salt and iteration count as shown in example 2:

private static byte[] stretchKey(String password, byte[] salt, int iterationCount) {
  KeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), 
    salt,
    iterationCount,
    192);   // length of a DES3 key
    SecretKeyFactory fact = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

  return fact.generateSecret(pbeKeySpec).getEncoded();
}

Example 2: Create key material from a passphrase

So, when you put this all together and decrypt it, what is the final output? Yet another ASN.1-formatted structure, of course! However, this isn't the exact same ASN.1 structure that contained the RSA key that I examined last month. Recall that, once decrypted, an RSA-formatted key started with a header line "BEGIN RSA PRIVATE KEY", and a DSA-formatted key starts with "BEGIN DSA PRIVATE KEY", but an encrypted key like the one illustrated in figure 1 just starts with "BEGIN ENCRYPTED PRIVATE KEY": that means that, to figure out if it's an RSA or a DSA (or even something more exotic, like ECDSA) private key, you have to decrypt it first. So the encoded ASN.1 structure starts with a header declaration indicating what sort of key it is (i.e. how to interpret the contained byte structure) followed by the actual key material itself, which is in exactly the same format as I examined last month. So, you need to make one more last pass through the ASN.1 parser to get the OID that declares the actual private key algorithm along with the bytes containing the key material itself. These bytes can then be ASN.1 parsed (again!) using the same parsing routine that I developed in my previous post.

Example 3 is a revised copy of the KeyImport utility from last month, updated to permit the importation of encrypted private keys. New code is highlighted in bold.

import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.math.BigInteger;
import java.security.KeyStore;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.GeneralSecurityException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.SecretKeySpec;

class KeyImportException extends Exception  {
  public KeyImportException(String msg)  {
    super(msg);
  }
}

/**
 * Import an existing key into a keystore.  This creates a key entry as though you had
 * submitted a "genkey" request so that a subsequent "import" command will import the
 * certificate correctly.
 */
public class KeyImport  {
  private static final int invCodes[] = {
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 64, -1, -1,
    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
  };

  private static byte[] OID_RSA_FORMAT = { (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, 
       (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01 };
  private static byte[] OID_DSA_FORMAT = { (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, 
       (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x02 }; 

  private static byte[] base64Decode(String input)    {
    if (input.length() % 4 != 0)    {
        throw new IllegalArgumentException("Invalid base64 input");
    }
    byte decoded[] = new byte[((input.length() * 3) / 4) - 
      (input.indexOf('=') > 0 ? (input.length() - input.indexOf('=')) : 0)];
    char[] inChars = input.toCharArray();
    int j = 0;
    int b[] = new int[4];
    for (int i = 0; i < inChars.length; i += 4)     {
      b[0] = invCodes[inChars[i]];
      b[1] = invCodes[inChars[i + 1]];
      b[2] = invCodes[inChars[i + 2]];
      b[3] = invCodes[inChars[i + 3]];
      decoded[j++] = (byte) ((b[0] << 2) | (b[1] >> 4));
      if (b[2] < 64)      {
        decoded[j++] = (byte) ((b[1] << 4) | (b[2] >> 2));
        if (b[3] < 64)  {
          decoded[j++] = (byte) ((b[2] << 6) | b[3]);
        }
      }
    }

    return decoded;
  }

  /**
   * Bare-bones ASN.1 parser that can only deal with a structure that contains integers
   * (as I expect for the RSA private key format given in PKCS #1 and RFC 3447).
   * @param b the bytes to be parsed as ASN.1 DER
   * @param integers an output array to which all integers encountered during the parse
   *   will be appended in the order they're encountered.  It's up to the caller to determine
   *   which is which.
   * @param oids an output array of all 0x06 tags
   * @param byteStrings an output array of all 0x04 tags
   */
  private static void ASN1Parse(byte[] b, 
                                List<BigInteger> integers,
                                List<byte[]> oids,
                                List<byte[]> byteStrings) throws KeyImportException  {
    int pos = 0;
    while (pos < b.length)  {
      byte tag = b[pos++];
      int length = b[pos++];
      if ((length & 0x80) != 0)  {
        int extLen = 0;
        for (int i = 0; i < (length & 0x7F); i++)  {
          extLen = (extLen << 8) | (b[pos++] & 0xFF);
        }
        length = extLen;
      }
      byte[] contents = new byte[length];
      System.arraycopy(b, pos, contents, 0, length);
      pos += length;

      if (tag == 0x30)  {  // sequence
        ASN1Parse(contents, integers, oids, byteStrings);
      } else if (tag == 0x02)  {  // Integer
        BigInteger i = new BigInteger(contents);
        integers.add(i);
      } else if (tag == 0x04)  { // byte string
        byteStrings.add(contents);
      } else if (tag == 0x06) { // OID
        oids.add(contents);
      } else if (tag == 0x05)  { // String
        // Ignore this.  It comes up in the RSA format, but only as a placeholder.
      } else  {
        throw new KeyImportException("Unsupported ASN.1 tag " + tag + " encountered.  Is this a " +
          "valid RSA key?");
      }
    }
  }

  private static byte[] stretchKey(String password, byte[] salt, int iterationCount)
      throws GeneralSecurityException  {
    KeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), 
      salt,
      iterationCount,
      192);    // length of a DES3 key
    SecretKeyFactory fact = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

    return fact.generateSecret(pbeKeySpec).getEncoded();
  }

  private static byte[] decrypt(byte[] key, byte[] iv, byte[] encrypted) 
      throws GeneralSecurityException  {
    DESedeKeySpec desKeySpec = new DESedeKeySpec(key);
    SecretKeySpec desKey = new SecretKeySpec(desKeySpec.getKey(), "DESede");
    Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    IvParameterSpec ivSpec = new IvParameterSpec(iv);
    cipher.init(Cipher.DECRYPT_MODE, desKey, ivSpec);
    return cipher.doFinal(encrypted);
  }

  /**
   * Read a PKCS #8, Base64-encrypted file as a Key instance.  This handles encrypted or
   * decrypted and RSA or DSA.  It only accepts Base64 encoded input (i.e. not DER).
   */
  private static PrivateKey readPrivateKeyFile(String keyFileName,
                                               String keyFilePassword) 
      throws IOException,
             GeneralSecurityException,
             KeyImportException  {
    BufferedReader in = new BufferedReader(new FileReader(keyFileName));
    try  {
      String line;
      boolean encrypted = false;
      boolean readingKey = false;
      boolean pkcs8Format = false;
      boolean rsaFormat = false;
      boolean dsaFormat = false;
      StringBuffer base64EncodedKey = new StringBuffer();
      while ((line = in.readLine()) != null)  {
        if (readingKey)  {
          if (line.trim().equals("-----END RSA PRIVATE KEY-----"))  {  // PKCS #1
            readingKey = false;
          } else if (line.trim().equals("-----END DSA PRIVATE KEY-----"))  {
            readingKey = false;
          } else if (line.trim().equals("-----END PRIVATE KEY-----"))  {  // PKCS #8
            readingKey = false;
          } else if (line.trim().equals("-----END ENCRYPTED PRIVATE KEY-----"))  {
            readingKey = false;
          } else  {
            base64EncodedKey.append(line.trim());
          }
        } else if  (line.trim().equals("-----BEGIN RSA PRIVATE KEY-----"))  {
          readingKey = true;
          rsaFormat = true;
        } else if  (line.trim().equals("-----BEGIN DSA PRIVATE KEY-----"))  {
          readingKey = true;
          dsaFormat = true;
        } else if  (line.trim().equals("-----BEGIN PRIVATE KEY-----"))  {
          readingKey = true;
          pkcs8Format = true;
        } else if  (line.trim().equals("-----BEGIN ENCRYPTED PRIVATE KEY-----"))  {
          readingKey = true;
          encrypted = true;
        }
      }
      if (base64EncodedKey.length() == 0)  {
        throw new IOException("File '" + keyFileName + 
          "' did not contain an unencrypted private key");
      }

      byte[] bytes = base64Decode(base64EncodedKey.toString());

      if (encrypted)  {
        List<BigInteger> pkcs5Integers = new ArrayList<BigInteger>();
        List<byte[]> oids = new ArrayList<byte[]>();
        List<byte[]> byteStrings = new ArrayList<byte[]>();
        ASN1Parse(bytes, pkcs5Integers, oids, byteStrings);

        byte[] salt = byteStrings.get(0);
        int iterationCount = pkcs5Integers.get(0).intValue();

        if (keyFilePassword == null)  {
          throw new KeyImportException("This is an encrypted key file.  An -importPassword is required");
        }

        // XXX I should be verifying the key-stretching algorithm OID here
        byte[] key = stretchKey(keyFilePassword, salt, iterationCount);
        byte[] encryptedBytes = byteStrings.get(2);
        byte[] iv = byteStrings.get(1);
        // XXX I should be verifying the encryption algorithm OID here
        bytes = decrypt(key, iv, encryptedBytes);

        // Parse the decrypted output to determine its type (RSA or DSA)
        pkcs5Integers = new ArrayList<BigInteger>();
        oids = new ArrayList<byte[]>();
        byteStrings = new ArrayList<byte[]>();
        ASN1Parse(bytes, pkcs5Integers, oids, byteStrings);

        if (Arrays.equals(oids.get(0), OID_RSA_FORMAT))  {
          bytes = byteStrings.get(0);
          rsaFormat = true;
        } else if (Arrays.equals(oids.get(0), OID_DSA_FORMAT))  {
          bytes = byteStrings.get(0);
          dsaFormat = true;
        } else  {
          System.out.println("Unrecognized key format");
          System.exit(0);
        }
      }

      // PKCS #8 as in: http://www.agentbob.info/agentbob/79-AB.html
      KeyFactory kf = null;
      KeySpec spec = null;
      if (pkcs8Format)  {
        kf = KeyFactory.getInstance("RSA");
        spec = new PKCS8EncodedKeySpec(bytes);
      } else if (rsaFormat)  {
        kf = KeyFactory.getInstance("RSA");
        List<BigInteger> rsaIntegers = new ArrayList<BigInteger>();
        ASN1Parse(bytes, rsaIntegers, null, null);
        if (rsaIntegers.size() < 8)  {
          throw new KeyImportException("'" + keyFileName + 
            "' does not appear to be a properly formatted RSA key");
        }
        BigInteger publicExponent = rsaIntegers.get(2);
        BigInteger privateExponent = rsaIntegers.get(3);
        BigInteger modulus = rsaIntegers.get(1);
        BigInteger primeP = rsaIntegers.get(4);
        BigInteger primeQ = rsaIntegers.get(5);
        BigInteger primeExponentP = rsaIntegers.get(6);
        BigInteger primeExponentQ = rsaIntegers.get(7);
        BigInteger crtCoefficient = rsaIntegers.get(8);
        spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent,
          primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient);
      } else if (dsaFormat)  {
        kf = KeyFactory.getInstance("DSA");
        List<BigInteger> dsaIntegers = new ArrayList<BigInteger>();
        ASN1Parse(bytes, dsaIntegers, null, null);
        if (dsaIntegers.size() < 5)  {
          throw new KeyImportException("'" + keyFileName + 
            "' does not appear to be a properly formatted DSA key");
        }
        BigInteger privateExponent = dsaIntegers.get(1);
        BigInteger publicExponent = dsaIntegers.get(2);
        BigInteger P = dsaIntegers.get(3);
        BigInteger Q = dsaIntegers.get(4);
        BigInteger G = dsaIntegers.get(5);
        spec = new DSAPrivateKeySpec(privateExponent, P, Q, G);
      }
      return kf.generatePrivate(spec);
    } finally  {
      in.close();
    }
  }

  static class Parameter  {
    String flag;
    boolean required;
    String description;
    String defaultValue;

    public Parameter(String flag, boolean required, String description, String defaultValue)  {
      this.flag = flag;
      this.required = required;
      this.description = description;
      this.defaultValue = defaultValue;
    }

    public boolean equals(Object o)  {
      return (o instanceof Parameter) && (this.flag.equals(((Parameter) o).flag));
    }
  }

  private static String KEY_FILE = "-keyFile";
  private static String ALIAS = "-alias";
  private static String CERT_FILE = "-certificateFile";
  private static String KEY_STORE = "-keystore";
  private static String KEY_STORE_PASSWORD = "-keystorePassword";
  private static String KEY_STORE_TYPE = "-keystoreType";
  private static String KEY_PASSWORD = "-keyPassword";
  private static String IMPORT_PASSWORD = "-importPassword";

  private static List<Parameter> paramDesc = Arrays.asList(
    new Parameter[] {
      new Parameter(KEY_FILE, true, "Name of file containing a private key in PEM or DER form", null),
      new Parameter(ALIAS, true, "The alias that this key should be imported as", null),
      new Parameter(CERT_FILE, true, "Name of file containing the certificate that corresponds to the key named by '-keyFile'", null),
      new Parameter(KEY_STORE, false, "Name of the keystore to import the private key into.", "~/.keystore"),
      new Parameter(KEY_STORE_PASSWORD, false, "Keystore password", "changeit"),
      new Parameter(KEY_STORE_TYPE, false, "Type of keystore; must be JKS or PKCS12", "JKS"),
      // If this password is different than the key store password, Tomcat (at least) chokes on 
			// it with: java.security.UnrecoverableKeyException: Cannot recover key
      new Parameter(KEY_PASSWORD, false, "The password to protect the imported key with", "changeit"),
      new Parameter(IMPORT_PASSWORD, false, "The password that the imported key is encrypted with, if encrypted", "changeit")
    });

  private static void usage()  {
    for (Parameter param : paramDesc)  {
      System.out.println(param.flag + "\t" + (param.required ? "required" : "optional") + "\t" +
        param.description + "\t" + 
        (param.defaultValue != null ? ("default '" + param.defaultValue + "'") : ""));
    }
  }

  public static void main(String[] args) throws IOException, 
                                                GeneralSecurityException,
                                                KeyImportException  {
    Map<String, String> parsedArgs = new HashMap<String, String>();
    for (Parameter param : paramDesc)  {
      if (param.defaultValue != null)  {
        parsedArgs.put(param.flag, param.defaultValue);
      }
    }
    for (int i = 0; i < args.length; i += 2)  {
      parsedArgs.put(args[i], args[i + 1]);
    }

    boolean invalidParameters = false;
    for (Parameter param : paramDesc)  {
      if (param.required && parsedArgs.get(param.flag) == null)  {
        System.err.println("Missing required parameter " + param.flag);
        invalidParameters = true;
      }
    }
    for (String key : parsedArgs.keySet())  {
      if (!paramDesc.contains(new Parameter(key, false, null, null)))  {
        System.err.println("Invalid parameter '" + key + "'");
        invalidParameters = true;
      }
    }
    if (invalidParameters)  {
      usage();
      System.exit(0);
    }

    KeyStore ks = KeyStore.getInstance(parsedArgs.get(KEY_STORE_TYPE));
    InputStream keyStoreIn = new FileInputStream(parsedArgs.get(KEY_STORE));
    try  {
      ks.load(keyStoreIn, parsedArgs.get(KEY_STORE_PASSWORD).toCharArray());
    } finally  {
      keyStoreIn.close();
    }

    Certificate cert;
    CertificateFactory fact = CertificateFactory.getInstance("X.509");
    FileInputStream certIn = new FileInputStream(parsedArgs.get(CERT_FILE));
    try  {
      cert = fact.generateCertificate(certIn);
    } finally  {
      certIn.close();
    }

    PrivateKey privateKey = readPrivateKeyFile(parsedArgs.get(KEY_FILE),
      parsedArgs.get(IMPORT_PASSWORD));
    ks.setKeyEntry(parsedArgs.get(ALIAS), privateKey, 
      parsedArgs.get(KEY_PASSWORD).toCharArray(), new Certificate[] {cert});

    OutputStream keyStoreOut = new FileOutputStream(parsedArgs.get(KEY_STORE));
    try  {
      ks.store(keyStoreOut, parsedArgs.get(KEY_STORE_PASSWORD).toCharArray());
    } finally  {
      keyStoreOut.close();
    }
  }
}

Example 3: KeyImport utility with decrypt capability

As you can see, there's not much change — the JDK does most of the heavy lifting by providing DES and PBKDF2 implementations.

This post details how to decrypt and import a very specific format of encrypted private key: the PKCS #5 format. There are yet more formats you may come across, but this has been the default output of the OpenSSL req command since its first release. If you do encounter a new format, you should be able to easily make use of the code presented here to incorporate the new format into this keyimport utility.

Add a comment:

Completely off-topic or spam comments will be removed at the discretion of the moderator.

You may preserve formatting (e.g. a code sample) by indenting with four spaces preceding the formatted line(s)

Name: Name is required
Email (will not be displayed publicly):
Comment:
Comment is required
My Book

I'm the author of the book "Implementing SSL/TLS Using Cryptography and PKI". Like the title says, this is a from-the-ground-up examination of the SSL protocol that provides security, integrity and privacy to most application-level internet protocols, most notably HTTP. I include the source code to a complete working SSL implementation, including the most popular cryptographic algorithms (DES, 3DES, RC4, AES, RSA, DSA, Diffie-Hellman, HMAC, MD5, SHA-1, SHA-256, and ECC), and show how they all fit together to provide transport-layer security.

My Picture

Joshua Davies

Past Posts