How to encrypt RSA private key with PBE in PKCS#5 format in Java with IAIK JCE?

2.6k Views Asked by At

I've created an RSA Key Pair. Now, I'm trying to encrypt the private key with a DES algorithm, format it to PKCS#5 and print it on the console. Unfortunately, the generated private key does not work. When I try to use it, after entering the right passphrase, the ssh client returns the passphrase is not valid:

Load key "test.key": incorrect passphrase supplied to decrypt private key

Could please someone tells me where I'm wrong?

This is the code:

private byte[] iv;

public void generate() throws Exception {
    RSAKeyPairGenerator generator = new RSAKeyPairGenerator();
    generator.initialize(2048);
    KeyPair keyPair = generator.generateKeyPair();

    String passphrase = "passphrase";
    byte[] encryptedData = encrypt(keyPair.getPrivate().getEncoded(), passphrase);
    System.out.println(getPrivateKeyPem(Base64.encodeBase64String(encryptedData)));
}

private byte[] encrypt(byte[] data, String passphrase) throws Exception {
    String algorithm = "PBEWithMD5AndDES";
    salt = new byte[8];
    int iterations = 1024;

    // Create a key from the supplied passphrase.
    KeySpec ks = new PBEKeySpec(passphrase.toCharArray());
    SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm);
    SecretKey key = skf.generateSecret(ks);

    // Create the salt from eight bytes of the digest of P || M.
    MessageDigest md = MessageDigest.getInstance("MD5");
    md.update(passphrase.getBytes());
    md.update(data);
    byte[] digest = md.digest();
    System.arraycopy(digest, 0, salt, 0, 8);
    AlgorithmParameterSpec aps = new PBEParameterSpec(salt, iterations);

    Cipher cipher = Cipher.getInstance(AlgorithmID.pbeWithSHAAnd3_KeyTripleDES_CBC.getJcaStandardName());
    cipher.init(Cipher.ENCRYPT_MODE, key, aps);
    iv = cipher.getIV();
    byte[] output = cipher.doFinal(data);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    out.write(salt);
    out.write(output);
    out.close();
    return out.toByteArray();
}

private String getPrivateKeyPem(String privateKey) throws Exception {
    StringBuffer formatted = new StringBuffer();
    formatted.append("-----BEGIN RSA PRIVATE KEY----- " + LINE_SEPARATOR);

    formatted.append("Proc-Type: 4,ENCRYPTED" + LINE_SEPARATOR);
    formatted.append("DEK-Info: DES-EDE3-CBC,");
    formatted.append(bytesToHex(iv));

    formatted.append(LINE_SEPARATOR);
    formatted.append(LINE_SEPARATOR);

    Arrays.stream(privateKey.split("(?<=\\G.{64})")).forEach(line -> formatted.append(line + LINE_SEPARATOR));
    formatted.append("-----END RSA PRIVATE KEY-----");

    return formatted.toString();
}

private String bytesToHex(byte[] bytes) {
    char[] hexArray = "0123456789ABCDEF".toCharArray();
    char[] hexChars = new char[bytes.length * 2];
    for (int j = 0; j < bytes.length; j++) {
        int v = bytes[j] & 0xFF;
        hexChars[j * 2] = hexArray[v >>> 4];
        hexChars[j * 2 + 1] = hexArray[v & 0x0F];
    }
    return new String(hexChars);
}

And this is the generated private key in PKCS#5 PEM format:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,CA138D5D3C048EBD

+aZNZJKLvNtlmnkg+rFK6NFm45pQJNnJB9ddQ3Rc5Ak0C/Igm9EqHoOS+iy+PPjx
pEKbhc4Qe3U0GOT9L5oN7iaWL82gUznRLRyUXtOrGcpE7TyrE+rydD9BsslJPCe+
y7a9LnSNZuJpJPnJCeKwzy5FGVv2KmDzGTcs9IqCMKgV69qf83pOJU6Dk+bvh9YP
3I05FHeaQYQk8c3t3onfljVIaYOfbNYFLZgNgGtPzFD4OpuDypei/61i3DeXyFUA
SNSY5fPwp6iSeSKtwduSEJMX31TKSpqWeZmEmMNcnh8oZz2E0jRWkbkaFuZfNtqt
aVpLN49oRpbsij+i1+udyuIXdBGRYt9iDZKnw+LDjC3X9R2ceq4AOdfsmEVYbO1i
YNms9eXSkANuchiI2YqkKsCwqI5S8S/2Xj76zf+pCDhCTYGV3RygkN6imX/Qg2eF
LOricZZTF/YPcKnggqNrZy4KSUzAgZ9NhzWCWOCiGFcQLYIo+qDoJ8t4FwxQYhx9
7ckzXML0n0q5ba5pGekLbBUJ9/TdtnqfqmYrHX+4OlrR7XAu478v2QH6/QtNKdZf
VRTqmKKH0n8JL9AgaXWipQstW5ERNZJ9YPBASQzewVNLv4gRZRTw8bYcU/hiPbWp
eqULYYI9324RzY3UTsz3N9X+zQsT02zNdxud7XmmoHL493yyvqT9ERmF4uckGYei
HZ16KFeKQXE9z+x0WNFAKX3nbttVlN5O7TAmUolFTwu11UDsJEjrYMZRwjheAZyD
UnV1LwhFT+QA0r68Mto3poxpAawCJqPP50V4jbhsOb0J7sxT8fo2mBVSxTdb9+t1
lG++x/gHcK51ApK1tF1FhRRKdtOzSib376Kmt23q0jVDNVyy09ys+8LRElOAY1Es
LIuMMM3F7l+F4+knKh3/IkPZwRIz3f9fpsVYIePPS1bUdagzNoMqUkTwzmq6vmUP
C5QvN6Z5ukVCObK+T8C4rya8KQ/2kwoSCRDIX6Mzpnqx6SoO4mvtBHvPcICGdOD6
aX/SbLd9J2lenTxnaAvxWW0jkF6q9x9AAIDdXTd9B5LnOG0Nq+zI+6THL+YpBCB9
6oMO4YChFNoEx0HZVdOc8E7xvXU2NqinmRnyh7hCR5KNfzsNdxg1d8ly67gdZQ1Q
bk1HPKvr6T568Ztapz1J/O6YWRIHdrGyA6liOKdArhhSI9xdk3H3JFNiuH+qkSCB
0mBYdS0BVRVdKbKcrk4WRHZxHsDsQn1/bPxok4dCG/dGO/gT0QlxV+hOV8h/4dJO
mcUvzdW4I8XKrX5KlTGNusVRiFX3Cy8FFZQtSxdWzr6XR6u0bUKS+KjDl1KoFxPH
GwYSTkJVE+fbjsSisQwXjWnwGGkNDuQ1IIMJOAHMK4Mly1jMdFF938WNY7NS4bIb
IXXkRdwxhdkRDiENSMXY8YeCNBJMjqdXZtR4cwGEXO+G+fpT5+ZrfPbQYO+0E0r4
wGPKlrpeeR74ALiaUemUYVIdw0ezlGvdhul2KZx4L82NpI6/JQ7shq9/BEW2dWhN
aDuWri2obsNL3kk2VBWPNiE6Rn/HtjwKn7ioWZ3IIgOgyavcITPBe0FAjxmfRs5w
VWLFBXqcyV9cu1xS4GoCNLk0MrVziUCwHmwkLIzQZos=
-----END RSA PRIVATE KEY-----

Thanks in advance.

2

There are 2 best solutions below

1
On

mister

I think that before invoking encrypt you need to decrypt two times more for security reasons. Instead of salt use also pepper salt and pepper. Do not mix algorithm with aes256.

Kind regards, rajeesh

2
On

There is no such thing as PKCS#5 format. PKCS#5 primarily defines two password-based key derivation functions and password-based encryption schemes using them, plus a password-based MAC scheme, but does not define any format for the data. (It does define ASN.1 OIDs for these operations, and ASN.1 structures for their parameters -- primarily PBKDF2 and PBES2, because the only parameter for PBKDF1 and PBES1 is the salt.) PKCS#5 also defines a padding scheme for the CBC mode data encryption; this padding was slightly enhanced by PKCS#7 and used by many other applications which usually call it PKCS5 padding or PKCS7 padding. None of these are data formats, and none of them involves RSA (or other) private keys as such.

The file format you apparently want is the one used by OpenSSH (for a long time always, then for the last few years as the default, until OpenSSH 7.8 just a month ago made it optional) and as a result also used by other software that wants to be compatible or even interchangeable with OpenSSH. This format is actually defined by OpenSSL, which OpenSSH has long used for most of its cryptography. (Following Heartbleed, OpenSSH created a fork of OpenSSL called LibreSSL, which tries to be more robust and secure internally but intentionally maintains the same external interfaces and formats, and in any case hasn't been widely adopted.)

It is one of several 'PEM' formats defined by OpenSSL, and is mostly described on the man page for a number of 'PEM' routines including PEM_write[_bio]_RSAPrivateKey -- on your system if you have OpenSSL and it's not Windows, or on the web with the encryption part near the end in the section 'PEM ENCRYPTION FORMAT', and the EVP_BytesToKey routine it references similarly on its own man page. In short: it does not use the pbeSHAwith3_keyTripleDES-CBC (meaning SHA1) scheme defined by PKCS#12/rfc7292 or the pbeMD5withDES-CBC scheme defined by PKCS#5/rfc2898 in PBES1. Instead it uses EVP_BytesToKey (which is partly based on PBKDF1) with md5 and 1 iteration, and salt equal to the IV, to derive the key, and then encrypts/decrypts with any supported symmetric cipher mode that uses an IV (thus not stream or ECB) but usually defaulting to DES-EDE3 (aka 3key-TripleDES) CBC as you ask for. Yes, EVP_BytesToKey with niter=1 is a poor PBKDF and makes these files insecure unless you use a very strong password; there are numerous Qs about that already.

And finally the plaintext of this file format is not the PKCS#8 (generic) encoding returned by [RSA]PrivateKey.getEncoded() but rather the RSA-only format defined by PKCS#1/rfc8017 et pred. And the empty line between the Proc-type and DEK-info headers and the base64 is required, and the line terminator on the dashes-END line may be needed depending on what software does the reading.

The easiest way to do this is to use software already compatible with OpenSSL private-key PEM format(s), including OpenSSL itself. Java can run an external program: OpenSSH's ssh-keygen if you have it, or openssl genrsa if you have that. The BouncyCastle bcpkix library supports this and other OpenSSL PEM formats. If 'ssh client' is jsch, that normally reads keyfiles in several formats including this one, but com.jcraft.jsch.KeyPairRSA actually supports generating a key and writing it in this PEM format as well. Puttygen also supports this format, but the other formats it can convert from and to aren't Java-friendly. I'm sure there are more.

But if you need to do it in your own code, here's how:

    // given [RSA]PrivateKey privkey, get the PKCS1 part from the PKCS8 encoding
    byte[] pk8 = privkey.getEncoded();
    // this is wrong for RSA<=512 but those are totally insecure anyway
    if( pk8[0]!=0x30 || pk8[1]!=(byte)0x82 ) throw new Exception();
    if( 4 + (pk8[2]<<8 | (pk8[3]&0xFF)) != pk8.length ) throw new Exception();
    if( pk8[4]!=2 || pk8[5]!=1 || pk8[6]!= 0 ) throw new Exception();
    if( pk8[7] != 0x30 || pk8[8]==0 || pk8[8]>127 ) throw new Exception();
    // could also check contents of the AlgId but that's more work
    int i = 4 + 3 + 2 + pk8[8];
    if( i + 4 > pk8.length || pk8[i]!=4 || pk8[i+1]!=(byte)0x82 ) throw new Exception();
    byte[] old = Arrays.copyOfRange (pk8, i+4, pk8.length);
    
    // OpenSSL-Legacy PEM encryption = 3keytdes-cbc using random iv 
    // key from EVP_BytesToKey(3keytdes.keylen=24,hash=md5,salt=iv,,iter=1,outkey,notiv)
    byte[] passphrase = "passphrase".getBytes(); // charset doesn't matter for test value
    byte[] iv = new byte[8]; new SecureRandom().nextBytes(iv); // maybe SIV instead?
    MessageDigest pbh = MessageDigest.getInstance("MD5");
    byte[] derive = new byte[32]; // round up to multiple of pbh.getDigestLength()=16
    for(int off = 0; off < derive.length; off += 16 ){
        if( off>0 ) pbh.update(derive,off-16,16);
        pbh.update(passphrase); pbh.update(iv); 
        pbh.digest(derive, off,  16);
    }
    Cipher pbc = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    pbc.init (Cipher.ENCRYPT_MODE, new SecretKeySpec(derive,0,24,"DESede"), new IvParameterSpec(iv));
    byte[] enc = pbc.doFinal(old);
    
    // write to PEM format (substitute other file if desired)
    System.out.println ("-----BEGIN RSA PRIVATE KEY-----");
    System.out.println ("Proc-Type: 4,ENCRYPTED");
    System.out.println ("DEK-Info: DES-EDE3-CBC," + DatatypeConverter.printHexBinary(iv));
    System.out.println (); // empty line
    String b64 = Base64.getEncoder().encodeToString(enc);
    for( int off = 0; off < b64.length(); off += 64 )
        System.out.println (b64.substring(off, off+64<b64.length()?off+64:b64.length()));
    System.out.println ("-----END RSA PRIVATE KEY-----");

Finally, OpenSSL format requires the encryption IV and the PBKDF salt be the same, and it makes that value random, so I did also. The computed value you used for salt only, MD5(password||data), vaguely resembles the synthetic-IV (SIV) construction that is now accepted for use with encryption, but it is not the same, plus I don't know if any competent analyst has considered the case where SIV is also used for PBKDF salt, so I would be reluctant to rely on this technique here. If you want to ask about that point, it's not really a programming Q and would be more suitable on cryptography.SX or maybe security.SX.


added for comments:

That code's output works for me with puttygen from 0.70, both on Windows (from upstream=chiark) and on CentOS6 (from EPEL). According to the source, the error message you gave occurs only if cmdgen has called key_type in sshpubk.c which recognized the first line as beginning with "-----BEGIN " but not "-----BEGIN OPENSSH PRIVATE KEY" (which is a very different format), then via import_ssh2 and openssh_pem_read called load_openssh_pem_key in import.c which does NOT find the first line beginning with "-----BEGIN " and ending with "PRIVATE KEY-----". This is very weird because both of those PLUS "RSA " in between is generated by my code and is needed for OpenSSH (or openssl) to accept it. Try looking at every byte of the first line at least (maybe first two lines) with something like cat -vet or sed -n l or in a pinch od -c.

RFC 2898 is rather old now; good practice today is usually 10s of thousands to 100s of thousands of iterations, and better practice is not to use an iterated hash at all but instead something memory-hard like scrypt or Argon2. But as I already wrote, OpenSSL legacy PEM encryption, which was designed back in the 1990s, uses ONE (un, eine, 1) iteration and therefore is a POOR and INSECURE scheme. Nobody can change it now because that's how it was designed. If you want decent PBE, don't use this format.

If you need a key only for SSH: OpenSSH (for several years now) supports and recent versions of Putty(gen) can import the OpenSSH-defined 'new format', which uses bcrypt, but jsch can't. OpenSSH (using OpenSSL) can also read (PEM) PKCS8 which allows PBKDF2 (better though not best) with iterations as desired, and it looks like jsch can, but not Putty(gen). I don't know for Cyberduck or other implementations.