8

I have an encrypted private key and I know the password.

I need to decrypt it using a Java library.

I'd prefer not to use BouncyCastle though, unless there is no other option. Based on previous experience, there is too much change and not enough documentation.

The private key is in this form:

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

X5h7SUDStF1tL16lRM+AfZb1UBDQ0D1YbQ6vmIlXiK....
.....
/KK5CZmIGw==
-----END RSA PRIVATE KEY-----

I believe the key data is Base64 encoded since I see \r\n after 64 characters.

I tried the following to decrypt the key:

import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public String decrypt(String keyDataStr, String passwordStr){
  // This key data start from "X5... to ==" 
  char [] password=passwordStr.toCharArray();
  byte [] keyDataBytes=com.sun.jersey.core.util.Base64.decode(keyDataStr);

  PBEKeySpec pbeSpec = new PBEKeySpec(password);
  EncryptedPrivateKeyInfo pkinfo = new EncryptedPrivateKeyInfo(keyDataBytes);
  SecretKeyFactory skf = SecretKeyFactory.getInstance(pkinfo.getAlgName());
  Key secret = skf.generateSecret(pbeSpec);
  PKCS8EncodedKeySpec keySpec = pkinfo.getKeySpec(secret);
  KeyFactory kf = KeyFactory.getInstance("RSA");
  PrivateKey pk=kf.generatePrivate(keySpec);
  return pk.toString();
}

I get this Exception

java.io.IOException: DerInputStream.getLength(): lengthTag=50, too big.
    at sun.security.util.DerInputStream.getLength(DerInputStream.java:561)
    at sun.security.util.DerValue.init(DerValue.java:365)
    at sun.security.util.DerValue.<init>(DerValue.java:294)
    at javax.crypto.EncryptedPrivateKeyInfo.<init> (EncryptedPrivateKeyInfo.java:84)

Am I passing the right parameter to EncryptedPrivateKeyInfo constructor?

How can I make this work?

I tried what Ericsonn suggested, with one small change since I am working Java 7, I could not use Base64.getMimeCoder() instead I used Base64.decode and I am getting this error I am getting an error like this Input length must be multiple of 8 when decrypting with padded cipher at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:750)

static RSAPrivateKey decrypt(String keyDataStr, String ivHex, String password)
            throws GeneralSecurityException, UnsupportedEncodingException
          {
            byte[] pw = password.getBytes(StandardCharsets.UTF_8);
            byte[] iv = h2b(ivHex);
            SecretKey secret = opensslKDF(pw, iv);
            Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
            byte [] keyBytes=Base64.decode(keyDataStr.getBytes("UTF-8"));
            byte[] pkcs1 = cipher.doFinal(keyBytes);
            /* See note for definition of "decodeRSAPrivatePKCS1" */
            RSAPrivateCrtKeySpec spec = decodeRSAPrivatePKCS1(pkcs1);
            KeyFactory rsa = KeyFactory.getInstance("RSA");
            return (RSAPrivateKey) rsa.generatePrivate(spec);
          }

          private static SecretKey opensslKDF(byte[] pw, byte[] iv)
            throws NoSuchAlgorithmException
          {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(pw);
            md5.update(iv);
            byte[] d0 = md5.digest();
            md5.update(d0);
            md5.update(pw);
            md5.update(iv);
            byte[] d1 = md5.digest();
            byte[] key = new byte[24];
            System.arraycopy(d0, 0, key, 0, 16);
            System.arraycopy(d1, 0, key, 16, 8);
            return new SecretKeySpec(key, "DESede");
          }

          private static byte[] h2b(CharSequence s)
          {
            int len = s.length();
            byte[] b = new byte[len / 2];
            for (int src = 0, dst = 0; src < len; ++dst) {
              int hi = Character.digit(s.charAt(src++), 16);
              int lo = Character.digit(s.charAt(src++), 16);
              b[dst] = (byte) (hi << 4 | lo);
            }
            return b;
          }
          static RSAPrivateCrtKeySpec decodeRSAPrivatePKCS1(byte[] encoded)
          {
            ByteBuffer input = ByteBuffer.wrap(encoded);
            if (der(input, 0x30) != input.remaining())
              throw new IllegalArgumentException("Excess data");
            if (!BigInteger.ZERO.equals(derint(input)))
              throw new IllegalArgumentException("Unsupported version");
            BigInteger n = derint(input);
            BigInteger e = derint(input);
            BigInteger d = derint(input);
            BigInteger p = derint(input);
            BigInteger q = derint(input);
            BigInteger ep = derint(input);
            BigInteger eq = derint(input);
            BigInteger c = derint(input);
            return new RSAPrivateCrtKeySpec(n, e, d, p, q, ep, eq, c);
          }

          private static BigInteger derint(ByteBuffer input)
          {
            byte[] value = new byte[der(input, 0x02)];
            input.get(value);
            return new BigInteger(+1, value);
          }


          private static int der(ByteBuffer input, int exp)
          {
            int tag = input.get() & 0xFF;
            if (tag != exp)
              throw new IllegalArgumentException("Unexpected tag");
            int n = input.get() & 0xFF;
            if (n < 128)
              return n;
            n &= 0x7F;
            if ((n < 1) || (n > 2))
              throw new IllegalArgumentException("Invalid length");
            int len = 0;
            while (n-- > 0) {
              len <<= 8;
              len |= input.get() & 0xFF;
            }
            return len;
          }

1640 is keyDataStr.length() and 1228 is keyBytes.length

rimsoft
  • 105
  • 1
  • 1
  • 5
  • I googled around for the error and found [this](http://stackoverflow.com/questions/23126282/java-apns-certificate-error-with-derinputstream-getlength-lengthtag-109-too), hope that helps. – kazagistar Feb 08 '16 at 18:48
  • You want to convert the key to a proper PKCS#8 one. See [here](http://stackoverflow.com/questions/20065304/what-is-the-differences-between-begin-rsa-private-key-and-begin-private-key) – President James K. Polk Feb 08 '16 at 18:55
  • Also see [How to get the java.security.PrivateKey object from RSA Privatekey.pem file?](http://stackoverflow.com/q/7525679) and [Using a PEM encoded, encrypted private key to sign a message natively](http://stackoverflow.com/q/1580012). Both suggest to convert the private key to PKCS #8. – jww Feb 08 '16 at 23:05
  • When you run your new code, how long is `keyDataStr` (`keyDataStr.length()`)? What about `keyBytes.length`? – erickson Feb 10 '16 at 17:33
  • 1228 not divisble by 8 – rimsoft Feb 10 '16 at 20:51
  • What should be keyDataStr be ? Should it be the string: "X5h7SUDStF1tL16lRM+AfZb1UBDQ0D1YbQ6vmIlXiK.... ..... /KK5CZmIGw==" OR starting with "-----BEGIN..... " ? – rimsoft Feb 10 '16 at 22:26
  • Starting with "X5h...". What is 1228? `keyDataStr.length()`? or `keyBytes.length`? – erickson Feb 11 '16 at 18:22
  • Sorry I made a mistake earlier it is 1640 is keyDataStr.length() and 1228 is keyBytes.length – rimsoft Feb 11 '16 at 19:34
  • That is strange. I'm not sure why your key would be longer than 1200 bytes. Is there any chance there is some corrupted data in `keyDataStr`? Is every line 64 characters long (except the last)? Is 1640 counting "\r\n" characters? – erickson Feb 11 '16 at 20:09
  • Yes, The keyDataStr has \r\n in it .. so I length() should reflect that. – rimsoft Feb 12 '16 at 00:47
  • I also want to let you know the openssl rsa -in <> -out <> works, So I am prepping a ugly backup solution to call openssl from java. I would prefer java native solution as possible. Thanks a lot for your help... I am clueless on how to debug this at the moment. – rimsoft Feb 12 '16 at 00:49
  • @rimsoft I believe the problem is your Base-64 decoding. I think the whitespace inside the string may be messing up your decoder somehow. Because 1640 characters with internal line breaks (i.e., between lines but not at the end) should decode to 1176 bytes. If you have to use that poor quality decoder, then remove all whitespace in `keyDataStr` before decoding: `keyDataStr = keyDataStr.replaceAll("\\s+", "");` What library are you using for base-64 in Java 7? – erickson Feb 12 '16 at 17:02
  • com.sun.jersey.core.util.Base64; – rimsoft Feb 12 '16 at 17:10
  • Yes, I reviewed the code for that class, and it doesn't handle whitespace; it will corrupt data containing extraneous characters, rather than signalling an error. You need to strip those out first. – erickson Feb 12 '16 at 17:23
  • You are right, I am using this now, byte [] keyBytes=DatatypeConverter.parseBase64Binary(keyDataStr); and I see following lengths Length:1640 LengthBytes:1192 and I get a Excess Data error static RSAPrivateCrtKeySpec decodeRSAPrivatePKCS1(byte[] encoded) { ByteBuffer input = ByteBuffer.wrap(encoded); if (der(input, 0x30) != input.remaining()) throw new IllegalArgumentException("Excess data"); – rimsoft Feb 12 '16 at 20:57
  • Ericson - You are just awesome.. I think I am closer. I has NoPadding for the padding earlier. (One of failed attempts I inherited). I got your Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding"); And I got past errors... the only question, RSAPrivateKey object(rpk), I am doing Base64 encoding of rpk.getEncoded()... that result does not match with what I get from openssl. I am happy I am this far.. Thanks to you. – rimsoft Feb 13 '16 at 05:17
  • @rimsoft They are probably different formats. You'd have to show the OpenSSL command you are using to decrypt, but I'm guessing that you end up with only the RSA private key in PKCS #1 format, while the Java `getEncoded()` method is giving you a PKCS #8 wrapper around the private key. – erickson Feb 14 '16 at 04:00
  • If you need more help, be sure to put @erickson at the beginning of your comment, or I don't get a notification. – erickson Feb 14 '16 at 04:01
  • @erickson : openssl rsa -in -out This is how I am running openssl command, so which one is correct ? – rimsoft Feb 14 '16 at 23:03
  • Both are right, just different formats. `rsa` is using PKCS #1, Java is using PKCS #8. With OpenSSL, you can use `openssl pkcs8 -topk8 -nocrypt < encrypted.pem > decryptedpk8.pem` to get the same output as Java. – erickson Feb 15 '16 at 02:25
  • @erickson : You are right output matches. You have been a great support, but much better than the vendors I am dealing with. – rimsoft Feb 15 '16 at 06:28
  • Glad to hear it. If you feel like your question is resolved, please click the check mark on my answer to mark it as "accepted. – erickson Feb 15 '16 at 18:14
  • @erickson: I will, Have a followup probably I will post a different question for that. How do I covert PKCS#8 key to PKCS#12... Looks like my other vendor I am pushing the key to needs in that form. Any ways thanks a lot. Will mark as resolved. – rimsoft Feb 15 '16 at 23:08

2 Answers2

9

You need to use a non-standard, OpenSSL method for deriving the decryption key. Then use that to decrypt the PKCS-#1–encoded key—what you are working with is not a PKCS #8 envelope. You'll also need the IV from the header as input to these processes.

It looks something like this:

  static RSAPrivateKey decrypt(String keyDataStr, String ivHex, String password)
    throws GeneralSecurityException
  {
    byte[] pw = password.getBytes(StandardCharsets.UTF_8);
    byte[] iv = h2b(ivHex);
    SecretKey secret = opensslKDF(pw, iv);
    Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
    byte[] pkcs1 = cipher.doFinal(Base64.getMimeDecoder().decode(keyDataStr));
    /* See note for definition of "decodeRSAPrivatePKCS1" */
    RSAPrivateCrtKeySpec spec = decodeRSAPrivatePKCS1(pkcs1);
    KeyFactory rsa = KeyFactory.getInstance("RSA");
    return (RSAPrivateKey) rsa.generatePrivate(spec);
  }

  private static SecretKey opensslKDF(byte[] pw, byte[] iv)
    throws NoSuchAlgorithmException
  {
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    md5.update(pw);
    md5.update(iv);
    byte[] d0 = md5.digest();
    md5.update(d0);
    md5.update(pw);
    md5.update(iv);
    byte[] d1 = md5.digest();
    byte[] key = new byte[24];
    System.arraycopy(d0, 0, key, 0, 16);
    System.arraycopy(d1, 0, key, 16, 8);
    return new SecretKeySpec(key, "DESede");
  }

  private static byte[] h2b(CharSequence s)
  {
    int len = s.length();
    byte[] b = new byte[len / 2];
    for (int src = 0, dst = 0; src < len; ++dst) {
      int hi = Character.digit(s.charAt(src++), 16);
      int lo = Character.digit(s.charAt(src++), 16);
      b[dst] = (byte) (hi << 4 | lo);
    }
    return b;
  }

This is already a lot of code, so I will link to another answer for the definition of the decodeRSAPrivatePKCS1() method.

Community
  • 1
  • 1
erickson
  • 265,237
  • 58
  • 395
  • 493
  • @rimsoft In your question, it's the value `"56F3A98D9CFFA77A"`. An IV is some random data used as the first block of cipher text when 3DES is used in CBC mode; it's there so that even if you used the same key for many encryption operations, an attacker wouldn't be able to tell. In this case, the same value is also used as a salt when deriving the encryption key, so that an attacker can't precompute a lot of keys for the most common passwords. – erickson Feb 08 '16 at 22:02
  • Thanks a lot. Will try it and share the results – rimsoft Feb 08 '16 at 22:10
  • *"You need to use a non-standard, OpenSSL method ..."* - I believe OpenSSL is PKCS#5 v1.5 for key sizes up to and including the cipher's block size. If the required key is over 16 bytes (for example, AES-256 needs 32 bytes), then a non-standard extension is engaged. – jww Feb 08 '16 at 22:56
  • @jww Yes, it would be PBKDF1 for a shorter key. This particular case uses the non-standard extension. Also, I didn't test, but I think that for DES, OpenSSL would ignore the PKCS #5 v1.5 specification for choosing the IV from the derived output, and instead use the salt as the IV, as it does for 3DES. – erickson Feb 08 '16 at 23:15
  • I tried, I am getting an error like this Input length must be multiple of 8 when decrypting with padded cipher at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:750) – rimsoft Feb 09 '16 at 02:39
  • @rimsoft Edit your question to remove old code and replace with your current code and error message. – erickson Feb 09 '16 at 16:48
  • Thanks; this opensslKDF worked for me, for decrypting an encrypted private key that had been generated by openssl. – Cheeso Jul 06 '17 at 18:05
3

Java code example below shows how to construct the decryption key to obtain the underlying RSA key from an encrypted private key created using the openssl 1.0.x genrsa command; specifically from the following genrsa options that may have been leveraged:

-des encrypt the generated key with DES in cbc mode

-des3 encrypt the generated key with DES in ede cbc mode (168 bit key)

-aes128, -aes192, -aes256 encrypt PEM output with cbc aes

Above options result in encrypted RSA private key of the form ...

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AAA,BBB
...

Where AAA would be one of:

DES-CBC, DES-EDE3-CBC, AES-128-CBC, AES-192-CBC, AES-256-CBC

AND BBB is the hex-encoded IV value

KeyFactory factory = KeyFactory.getInstance("RSA");
KeySpec keySpec = null;
RSAPrivateKey privateKey = null;

Matcher matcher = OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_PATTERN.matcher(pemContents);
if (matcher.matches())
{
    String encryptionDetails = matcher.group(1).trim(); // e.g. AES-256-CBC,XXXXXXX
    String encryptedKey = matcher.group(2).replaceAll("\\s", ""); // remove tabs / spaces / newlines / carriage return etc

    System.out.println("PEM appears to be OpenSSL Encrypted RSA Private Key; Encryption details : "
        + encryptionDetails + "; Key : " + encryptedKey);

    byte[] encryptedBinaryKey = java.util.Base64.getDecoder().decode(encryptedKey);

    String[] encryptionDetailsParts = encryptionDetails.split(",");
    if (encryptionDetailsParts.length == 2)
    {
        String encryptionAlgorithm = encryptionDetailsParts[0];
        String encryptedAlgorithmParams = encryptionDetailsParts[1]; // i.e. the initialization vector in hex

        byte[] pw = new String(password).getBytes(StandardCharsets.UTF_8);
        byte[] iv = fromHex(encryptedAlgorithmParams);

        MessageDigest digest = MessageDigest.getInstance("MD5");

        // we need to come up with the encryption key
        
        // first round digest based on password and first 8-bytes of IV ..
        digest.update(pw);
        digest.update(iv, 0, 8);

        byte[] round1Digest = digest.digest(); // The digest is reset after this call is made.
        
        // second round digest based on first round digest, password, and first 8-bytes of IV ...
        digest.update(round1Digest);
        digest.update(pw);
        digest.update(iv, 0, 8);

        byte[] round2Digest = digest.digest();

        Cipher cipher = null;
        SecretKey secretKey = null;
        byte[] key = null;
        byte[] pkcs1 = null;

        if ("AES-256-CBC".equals(encryptionAlgorithm))
        {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            key = new byte[32]; // 256 bit key  (block size still 128-bit)
            System.arraycopy(round1Digest, 0, key, 0, 16);
            System.arraycopy(round2Digest, 0, key, 16, 16);

            secretKey = new SecretKeySpec(key, "AES");
        }
        else if ("AES-192-CBC".equals(encryptionAlgorithm))
        {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            key = new byte[24]; // key size of 24 bytes
            System.arraycopy(round1Digest, 0, key, 0, 16);
            System.arraycopy(round2Digest, 0, key, 16, 8);

            secretKey = new SecretKeySpec(key, "AES");
        }
        else if ("AES-128-CBC".equals(encryptionAlgorithm))
        {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            key = new byte[16]; // 128 bit key
            System.arraycopy(round1Digest, 0, key, 0, 16);

            secretKey = new SecretKeySpec(key, "AES");
        }
        else if ("DES-EDE3-CBC".equals(encryptionAlgorithm))
        {
            cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
            
            key = new byte[24]; // key size of 24 bytes
            System.arraycopy(round1Digest, 0, key, 0, 16);
            System.arraycopy(round2Digest, 0, key, 16, 8);

            secretKey = new SecretKeySpec(key, "DESede");
        }
        else if ("DES-CBC".equals(encryptionAlgorithm))
        {
            cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
            
            key = new byte[8]; // key size of 8 bytes
            System.arraycopy(round1Digest, 0, key, 0, 8);

            secretKey = new SecretKeySpec(key, "DES");
        }

        cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));

        pkcs1 = cipher.doFinal(encryptedBinaryKey);

        keySpec = pkcs1ParsePrivateKey(pkcs1);

        privateKey = (RSAPrivateKey) factory.generatePrivate(keySpec);
    }
}

The regular expression ...

static final String OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_REGEX = "\\s*" 
+ "-----BEGIN RSA PUBLIC KEY-----" + "\\s*"
+ "Proc-Type: 4,ENCRYPTED" + "\\s*"
+ "DEK-Info:" + "\\s*([^\\s]+)" + "\\s+"
+ "([\\s\\S]*)"
+ "-----END RSA PUBLIC KEY-----" + "\\s*";

static final Pattern OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_PATTERN = Pattern.compile(OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_REGEX);

the fromHex(...) method ...

public static byte[] fromHex(String hexString)
{
    byte[] bytes = new byte[hexString.length() / 2];
    for (int i = 0; i < hexString.length(); i += 2)
    {
        bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
            + Character.digit(hexString.charAt(i + 1), 16));
    }
    return bytes;
}
Community
  • 1
  • 1