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 2 is a breakdown of the ASN.1 structure that results from Base64-decoding the encrypted private key of figure 1.
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-longtag
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
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.
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.
30 82 050E | top-level containing structure |
| structure containing unencrypted meta information |
| PBES 2 declaration that indicates that what follows is PKCS #5 formatted |
| PBES #2 parameters |
| Yet another structure (I told you ASN.1 is structure-crazy) |
| PBKDF2 key-stretching algorithm |
| PBKDF2 parameters |
| PBKDF2 salt |
| PBKDF2 iteration count|
| top-level containing structure |
| DES-EDE3-CBC |
| DES Initialization vector |
04 82 04C8 69D1BE675F2129E... | top-level containing 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);
}
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();
}
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();
}
}
}
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:
Thank you so much for sharing this solution. It saved me a lot of time!
I was using this code in android to import a pkcs12 private key into a client key store, but it was taking literally 2+ minutes to execute.
// THIS WORKS, BUT TAKES A LOOONG TIME, ABOUT 2 MINUTES KeyStore clientKeyStore = KeyStore.getInstance("PKCS12"); clientKeyStore.load(new FileInputStream(Mal.getInstance().getFile().getCommonDir() + "/" + "tns-client.p12" ), "xxxxxxxx".toCharArray() );
so this led me down a rabbit-hole to implement importing of private keys myself, but then I found PBES2 wasn't supported in the android device I was using.
Long story short, your solution worked perfectly, straight away and without mods. Thank you so much.
What license do you provide this source under?
thanks,
Between writing this blog entry (2016) and today (2020) opennssl changed the default key stretching
from PBKDF2WithHmacSHA1 to PBKDF2WithHmacSHA256 ("hmacWithSHA256"). It seems to be that the change is
done anytime between version 1.1.0 and 1.1.1.
The result is an error when running with a new key:
Exception in thread "main" java.security.InvalidAlgorithmParameterException: Wrong IV length: must be 8 bytes long
The result (when working only with new keys) is to change the stretchKey-method from (old)
SecretKeyFactory fact = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
to (new):
SecretKeyFactory fact = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
Me ha sido de mucha ayuda.
ahora mi duda es como mando llamar este codigo desde otra clase para su ejecucion completa
(disculpen ,soy nueva en esta)
asi que utilice esta clase tal cual esta y dentro de otra clase hice este metodo para mandarlo llamar
private void validateCertificated(String []args)throws IOException,
GeneralSecurityException { log.info("ktt validateCertificated "); log.info("ktt args "+args); get KeyImport.main(args);
}
lo que veo es que los arg vienen vacios por lo que no se como enviarle la informacion del certificado correctamente.
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});
Todavia tiene que leer el KeyStore ks de donde otro.
I have encrypted private key with me with name rsa_key.p8 in a folder. How should I use your program on that file? Can you give me sample calling code with values?
Basically I am not understanding parameters here.
I will be great help.