Java tutorial
/* * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.16/src/java/org/apache/commons/ssl/PKCS8Key.java $ * $Revision: 153 $ * $Date: 2009-09-15 22:40:53 -0700 (Tue, 15 Sep 2009) $ * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.commons.ssl; import org.apache.kerby.asn1.type.Asn1Integer; import org.apache.kerby.asn1.type.Asn1Null; import org.apache.kerby.asn1.type.Asn1ObjectIdentifier; import org.apache.kerby.asn1.type.Asn1OctetString; import org.apache.kerby.asn1.type.Asn1Sequence; import org.apache.kerby.util.Util; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.RC2ParameterSpec; import javax.crypto.spec.RC5ParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.DSAPublicKeySpec; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAPublicKeySpec; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * Utility for decrypting PKCS8 private keys. Way easier to use than * javax.crypto.EncryptedPrivateKeyInfo since all you need is the byte[] array * and the password. You don't need to know anything else about the PKCS8 * key you pass in. * </p><p> * Can handle base64 PEM, or raw DER. * Can handle PKCS8 Version 1.5 and 2.0. * Can also handle OpenSSL encrypted or unencrypted private keys (DSA or RSA). * </p><p> * The PKCS12 key derivation (the "pkcs12()" method) comes from BouncyCastle. * </p> * * @author Credit Union Central of British Columbia * @author <a href="http://www.cucbc.com/">www.cucbc.com</a> * @author <a href="mailto:juliusdavies@cucbc.com">juliusdavies@cucbc.com</a> * @author <a href="bouncycastle.org">bouncycastle.org</a> * @since 7-Nov-2006 */ public class PKCS8Key { public static final String RSA_OID = "1.2.840.113549.1.1.1"; public static final String DSA_OID = "1.2.840.10040.4.1"; public static final String PKCS8_UNENCRYPTED = "PRIVATE KEY"; public static final String PKCS8_ENCRYPTED = "ENCRYPTED PRIVATE KEY"; public static final String OPENSSL_RSA = "RSA PRIVATE KEY"; public static final String OPENSSL_DSA = "DSA PRIVATE KEY"; private final PrivateKey privateKey; private final byte[] decryptedBytes; private final String transformation; private final int keySize; private final boolean isDSA; private final boolean isRSA; /** * @param in pkcs8 file to parse (pem or der, encrypted or unencrypted) * @param password password to decrypt the pkcs8 file. Ignored if the * supplied pkcs8 is already unencrypted. * @throws java.security.GeneralSecurityException If a parsing or decryption problem * occured. * @throws java.io.IOException If the supplied InputStream could not be read. */ public PKCS8Key(final InputStream in, char[] password) throws GeneralSecurityException, IOException { this(Util.streamToBytes(in), password); } /** * @param in pkcs8 file to parse (pem or der, encrypted or unencrypted) * @param password password to decrypt the pkcs8 file. Ignored if the * supplied pkcs8 is already unencrypted. * @throws java.security.GeneralSecurityException If a parsing or decryption problem * occured. */ public PKCS8Key(final ByteArrayInputStream in, char[] password) throws GeneralSecurityException, IOException { this(Util.streamToBytes(in), password); } /** * @param encoded pkcs8 file to parse (pem or der, encrypted or unencrypted) * @param password password to decrypt the pkcs8 file. Ignored if the * supplied pkcs8 is already unencrypted. * @throws java.security.GeneralSecurityException If a parsing or decryption problem * occured. */ public PKCS8Key(final byte[] encoded, char[] password) throws GeneralSecurityException, IOException { DecryptResult decryptResult = new DecryptResult("UNENCRYPTED", 0, encoded); List pemItems = PEMUtil.decode(encoded); PEMItem keyItem = null; byte[] derBytes = null; if (pemItems.isEmpty()) { // must be DER encoded - PEMUtil wasn't able to extract anything. derBytes = encoded; } else { Iterator it = pemItems.iterator(); boolean opensslRSA = false; boolean opensslDSA = false; while (it.hasNext()) { PEMItem item = (PEMItem) it.next(); String type = item.pemType.trim().toUpperCase(); boolean plainPKCS8 = type.startsWith(PKCS8_UNENCRYPTED); boolean encryptedPKCS8 = type.startsWith(PKCS8_ENCRYPTED); boolean rsa = type.startsWith(OPENSSL_RSA); boolean dsa = type.startsWith(OPENSSL_DSA); if (plainPKCS8 || encryptedPKCS8 || rsa || dsa) { opensslRSA = opensslRSA || rsa; opensslDSA = opensslDSA || dsa; if (derBytes != null) { throw new ProbablyNotPKCS8Exception( "More than one pkcs8 " + "or OpenSSL key found in the supplied PEM Base64 stream"); } derBytes = item.getDerBytes(); keyItem = item; decryptResult = new DecryptResult("UNENCRYPTED", 0, derBytes); } } // after the loop is finished, did we find anything? if (derBytes == null) { throw new ProbablyNotPKCS8Exception( "No pkcs8 or OpenSSL key found in the supplied PEM Base64 stream"); } if (opensslDSA || opensslRSA) { String c = keyItem.cipher.trim(); boolean encrypted = !"UNKNOWN".equals(c) && !"".equals(c); if (encrypted) { decryptResult = opensslDecrypt(keyItem, password); } String oid = RSA_OID; if (opensslDSA) { oid = DSA_OID; } derBytes = formatAsPKCS8(decryptResult.bytes, oid, null); String tf = decryptResult.transformation; int ks = decryptResult.keySize; decryptResult = new DecryptResult(tf, ks, derBytes); } } PkcsStructure pkcs8; try { pkcs8 = PkcsUtil.analyze(derBytes); } catch (Exception e) { throw new ProbablyNotPKCS8Exception("asn1 parse failure: " + e); } String oid = RSA_OID; // With the OpenSSL unencrypted private keys in DER format, the only way // to even have a hope of guessing what we've got (DSA or RSA?) is to // count the number of DERIntegers occurring in the first DERSequence. int derIntegerCount = -1; if (pkcs8.derIntegers != null) { derIntegerCount = pkcs8.derIntegers.size(); } switch (derIntegerCount) { case 6: oid = DSA_OID; case 9: derBytes = formatAsPKCS8(derBytes, oid, pkcs8); pkcs8.oid1 = oid; String tf = decryptResult.transformation; int ks = decryptResult.keySize; decryptResult = new DecryptResult(tf, ks, derBytes); break; default: break; } oid = pkcs8.oid1 != null ? pkcs8.oid1 : ""; if (!oid.startsWith("1.2.840.113549.1")) { boolean isOkay = false; if (oid.startsWith("1.2.840.10040.4.")) { String s = oid.substring("1.2.840.10040.4.".length()); // 1.2.840.10040.4.1 -- id-dsa // 1.2.840.10040.4.3 -- id-dsa-with-sha1 isOkay = s.equals("1") || s.startsWith("1.") || s.equals("3") || s.startsWith("3."); } if (!isOkay) { throw new ProbablyNotPKCS8Exception( "Valid ASN.1," + " but not PKCS8 or OpenSSL format. OID=" + oid); } } boolean isRSA = RSA_OID.equals(oid); boolean isDSA = DSA_OID.equals(oid); boolean encrypted = !isRSA && !isDSA; byte[] decryptedPKCS8 = encrypted ? null : derBytes; if (encrypted) { decryptResult = decryptPKCS8(pkcs8, password); decryptedPKCS8 = decryptResult.bytes; } if (encrypted) { try { pkcs8 = PkcsUtil.analyze(decryptedPKCS8); } catch (Exception e) { throw new ProbablyBadPasswordException( "Decrypted stream not ASN.1. Probably bad decryption password."); } oid = pkcs8.oid1; isDSA = DSA_OID.equals(oid); } KeySpec spec = new PKCS8EncodedKeySpec(decryptedPKCS8); String type = "RSA"; PrivateKey pk; try { KeyFactory kf; if (isDSA) { type = "DSA"; kf = KeyFactory.getInstance("DSA"); } else { kf = KeyFactory.getInstance("RSA"); } pk = kf.generatePrivate(spec); } catch (Exception e) { throw new ProbablyBadPasswordException("Cannot create " + type + " private key from decrypted stream. Probably bad decryption password. " + e); } if (pk != null) { this.privateKey = pk; this.isDSA = isDSA; this.isRSA = !isDSA; this.decryptedBytes = decryptedPKCS8; this.transformation = decryptResult.transformation; this.keySize = decryptResult.keySize; } else { throw new GeneralSecurityException( "KeyFactory.generatePrivate() returned null and didn't throw exception!"); } } public boolean isRSA() { return isRSA; } public boolean isDSA() { return isDSA; } public String getTransformation() { return transformation; } public int getKeySize() { return keySize; } public byte[] getDecryptedBytes() { return decryptedBytes; } public PrivateKey getPrivateKey() { return privateKey; } public PublicKey getPublicKey() throws GeneralSecurityException { if (privateKey instanceof DSAPrivateKey) { DSAPrivateKey dsa = (DSAPrivateKey) privateKey; DSAParams params = dsa.getParams(); BigInteger g = params.getG(); BigInteger p = params.getP(); BigInteger q = params.getQ(); BigInteger x = dsa.getX(); BigInteger y = q.modPow(x, p); DSAPublicKeySpec dsaKeySpec = new DSAPublicKeySpec(y, p, q, g); return KeyFactory.getInstance("DSA").generatePublic(dsaKeySpec); } else if (privateKey instanceof RSAPrivateCrtKey) { RSAPrivateCrtKey rsa = (RSAPrivateCrtKey) privateKey; RSAPublicKeySpec rsaKeySpec = new RSAPublicKeySpec(rsa.getModulus(), rsa.getPublicExponent()); return KeyFactory.getInstance("RSA").generatePublic(rsaKeySpec); } else { throw new GeneralSecurityException("Not an RSA or DSA key"); } } public static class DecryptResult { public final String transformation; public final int keySize; public final byte[] bytes; protected DecryptResult(String transformation, int keySize, byte[] decryptedBytes) { this.transformation = transformation; this.keySize = keySize; this.bytes = decryptedBytes; } } private static DecryptResult opensslDecrypt(final PEMItem item, final char[] password) throws GeneralSecurityException { final String cipher = item.cipher; final String mode = item.mode; final int keySize = item.keySizeInBits; final byte[] salt = item.iv; final boolean des2 = item.des2; final DerivedKey dk = OpenSSL.deriveKey(password, salt, keySize, des2); return decrypt(cipher, mode, dk, des2, null, item.getDerBytes()); } public static Cipher generateCipher(String cipher, String mode, final DerivedKey dk, final boolean des2, final byte[] iv, final boolean decryptMode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { if (des2 && dk.key.length >= 24) { // copy first 8 bytes into last 8 bytes to create 2DES key. System.arraycopy(dk.key, 0, dk.key, 16, 8); } final int keySize = dk.key.length * 8; cipher = cipher.trim(); String cipherUpper = cipher.toUpperCase(); mode = mode.trim().toUpperCase(); // Is the cipher even available? Cipher.getInstance(cipher); String padding = "PKCS5Padding"; if (mode.startsWith("CFB") || mode.startsWith("OFB")) { padding = "NoPadding"; } String transformation = cipher + "/" + mode + "/" + padding; if (cipherUpper.startsWith("RC4")) { // RC4 does not take mode or padding. transformation = cipher; } SecretKey secret = new SecretKeySpec(dk.key, cipher); IvParameterSpec ivParams; if (iv != null) { ivParams = new IvParameterSpec(iv); } else { ivParams = dk.iv != null ? new IvParameterSpec(dk.iv) : null; } Cipher c = Cipher.getInstance(transformation); int cipherMode = Cipher.ENCRYPT_MODE; if (decryptMode) { cipherMode = Cipher.DECRYPT_MODE; } // RC2 requires special params to inform engine of keysize. if (cipherUpper.startsWith("RC2")) { RC2ParameterSpec rcParams; if (mode.startsWith("ECB") || ivParams == null) { // ECB doesn't take an IV. rcParams = new RC2ParameterSpec(keySize); } else { rcParams = new RC2ParameterSpec(keySize, ivParams.getIV()); } c.init(cipherMode, secret, rcParams); } else if (cipherUpper.startsWith("RC5")) { RC5ParameterSpec rcParams; if (mode.startsWith("ECB") || ivParams == null) { // ECB doesn't take an IV. rcParams = new RC5ParameterSpec(16, 12, 32); } else { rcParams = new RC5ParameterSpec(16, 12, 32, ivParams.getIV()); } c.init(cipherMode, secret, rcParams); } else if (mode.startsWith("ECB") || cipherUpper.startsWith("RC4")) { // RC4 doesn't require any params. // Any cipher using ECB does not require an IV. c.init(cipherMode, secret); } else { // DES, DESede, AES, BlowFish require IVParams (when in CBC, CFB, // or OFB mode). (In ECB mode they don't require IVParams). try { c.init(cipherMode, secret, ivParams); } catch (InvalidKeyException e) { // TO BE FIXED: // Handling for larger key size beyond the JRE supported strength limit. throw e; } } return c; } public static DecryptResult decrypt(String cipher, String mode, final DerivedKey dk, final boolean des2, final byte[] iv, final byte[] encryptedBytes) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { Cipher c = generateCipher(cipher, mode, dk, des2, iv, true); final String transformation = c.getAlgorithm(); final int keySize = dk.key.length * 8; byte[] decryptedBytes = c.doFinal(encryptedBytes); return new DecryptResult(transformation, keySize, decryptedBytes); } @SuppressWarnings("checkstyle:methodlength") private static DecryptResult decryptPKCS8(PkcsStructure pkcs8, char[] password) throws GeneralSecurityException { boolean isVersion1 = true; boolean isVersion2 = false; boolean usePKCS12PasswordPadding = false; boolean use2DES = false; String cipher = null; String hash = null; int keySize = -1; // Almost all PKCS8 encrypted keys use CBC. Looks like the AES OID's can // support different modes, and RC4 doesn't use any mode at all! String mode = "CBC"; // In PKCS8 Version 2 the IV is stored in the ASN.1 structure for // us, so we don't need to derive it. Just leave "ivSize" set to 0 for // those ones. int ivSize = 0; String oid = pkcs8.oid1; // PKCS12 key derivation! if (oid.startsWith("1.2.840.113549.1.12.")) { usePKCS12PasswordPadding = true; // Let's trim this OID to make life a little easier. oid = oid.substring("1.2.840.113549.1.12.".length()); if (oid.equals("1.1") || oid.startsWith("1.1.")) { // 1.2.840.113549.1.12.1.1 hash = "SHA1"; cipher = "RC4"; keySize = 128; } else if (oid.equals("1.2") || oid.startsWith("1.2.")) { // 1.2.840.113549.1.12.1.2 hash = "SHA1"; cipher = "RC4"; keySize = 40; } else if (oid.equals("1.3") || oid.startsWith("1.3.")) { // 1.2.840.113549.1.12.1.3 hash = "SHA1"; cipher = "DESede"; keySize = 192; } else if (oid.equals("1.4") || oid.startsWith("1.4.")) { // DES2 !!! // 1.2.840.113549.1.12.1.4 hash = "SHA1"; cipher = "DESede"; keySize = 192; use2DES = true; // later on we'll copy the first 8 bytes of the 24 byte DESede key // over top the last 8 bytes, making the key look like K1-K2-K1 // instead of the usual K1-K2-K3. } else if (oid.equals("1.5") || oid.startsWith("1.5.")) { // 1.2.840.113549.1.12.1.5 hash = "SHA1"; cipher = "RC2"; keySize = 128; } else if (oid.equals("1.6") || oid.startsWith("1.6.")) { // 1.2.840.113549.1.12.1.6 hash = "SHA1"; cipher = "RC2"; keySize = 40; } } else if (oid.startsWith("1.2.840.113549.1.5.")) { // Let's trim this OID to make life a little easier. oid = oid.substring("1.2.840.113549.1.5.".length()); if (oid.equals("1") || oid.startsWith("1.")) { // 1.2.840.113549.1.5.1 -- pbeWithMD2AndDES-CBC hash = "MD2"; cipher = "DES"; keySize = 64; } else if (oid.equals("3") || oid.startsWith("3.")) { // 1.2.840.113549.1.5.3 -- pbeWithMD5AndDES-CBC hash = "MD5"; cipher = "DES"; keySize = 64; } else if (oid.equals("4") || oid.startsWith("4.")) { // 1.2.840.113549.1.5.4 -- pbeWithMD2AndRC2_CBC hash = "MD2"; cipher = "RC2"; keySize = 64; } else if (oid.equals("6") || oid.startsWith("6.")) { // 1.2.840.113549.1.5.6 -- pbeWithMD5AndRC2_CBC hash = "MD5"; cipher = "RC2"; keySize = 64; } else if (oid.equals("10") || oid.startsWith("10.")) { // 1.2.840.113549.1.5.10 -- pbeWithSHA1AndDES-CBC hash = "SHA1"; cipher = "DES"; keySize = 64; } else if (oid.equals("11") || oid.startsWith("11.")) { // 1.2.840.113549.1.5.11 -- pbeWithSHA1AndRC2_CBC hash = "SHA1"; cipher = "RC2"; keySize = 64; } else if (oid.equals("12") || oid.startsWith("12.")) { // 1.2.840.113549.1.5.12 - id-PBKDF2 - Key Derivation Function isVersion2 = true; } else if (oid.equals("13") || oid.startsWith("13.")) { // 1.2.840.113549.1.5.13 - id-PBES2: PBES2 encryption scheme isVersion2 = true; } else if (oid.equals("14") || oid.startsWith("14.")) { // 1.2.840.113549.1.5.14 - id-PBMAC1 message authentication scheme isVersion2 = true; } } if (isVersion2) { isVersion1 = false; hash = "HmacSHA1"; oid = pkcs8.oid2; // really ought to be: // // if ( oid.startsWith( "1.2.840.113549.1.5.12" ) ) // // but all my tests still pass, and I figure this to be more robust: if (pkcs8.oid3 != null) { oid = pkcs8.oid3; } if (oid.startsWith("1.3.6.1.4.1.3029.1.2")) { // 1.3.6.1.4.1.3029.1.2 - Blowfish cipher = "Blowfish"; mode = "CBC"; keySize = 128; } else if (oid.startsWith("1.3.14.3.2.")) { oid = oid.substring("1.3.14.3.2.".length()); if (oid.equals("6") || oid.startsWith("6.")) { // 1.3.14.3.2.6 - desECB cipher = "DES"; mode = "ECB"; keySize = 64; } else if (oid.equals("7") || oid.startsWith("7.")) { // 1.3.14.3.2.7 - desCBC cipher = "DES"; mode = "CBC"; keySize = 64; } else if (oid.equals("8") || oid.startsWith("8.")) { // 1.3.14.3.2.8 - desOFB cipher = "DES"; mode = "OFB"; keySize = 64; } else if (oid.equals("9") || oid.startsWith("9.")) { // 1.3.14.3.2.9 - desCFB cipher = "DES"; mode = "CFB"; keySize = 64; } else if (oid.equals("17") || oid.startsWith("17.")) { // 1.3.14.3.2.17 - desEDE cipher = "DESede"; mode = "CBC"; keySize = 192; // If the supplied IV is all zeroes, then this is DES2 // (Well, that's what happened when I played with OpenSSL!) if (allZeroes(pkcs8.iv)) { mode = "ECB"; use2DES = true; pkcs8.iv = null; } } } else if (oid.startsWith("2.16.840.1.101.3.4.1.")) { // AES // 2.16.840.1.101.3.4.1.1 - id-aes128-ECB // 2.16.840.1.101.3.4.1.2 - id-aes128-CBC // 2.16.840.1.101.3.4.1.3 - id-aes128-OFB // 2.16.840.1.101.3.4.1.4 - id-aes128-CFB // 2.16.840.1.101.3.4.1.21 - id-aes192-ECB // 2.16.840.1.101.3.4.1.22 - id-aes192-CBC // 2.16.840.1.101.3.4.1.23 - id-aes192-OFB // 2.16.840.1.101.3.4.1.24 - id-aes192-CFB // 2.16.840.1.101.3.4.1.41 - id-aes256-ECB // 2.16.840.1.101.3.4.1.42 - id-aes256-CBC // 2.16.840.1.101.3.4.1.43 - id-aes256-OFB // 2.16.840.1.101.3.4.1.44 - id-aes256-CFB cipher = "AES"; if (pkcs8.iv == null) { ivSize = 128; } oid = oid.substring("2.16.840.1.101.3.4.1.".length()); int x = oid.indexOf('.'); int finalDigit; if (x >= 0) { finalDigit = Integer.parseInt(oid.substring(0, x)); } else { finalDigit = Integer.parseInt(oid); } switch (finalDigit % 10) { case 1: mode = "ECB"; break; case 2: mode = "CBC"; break; case 3: mode = "OFB"; break; case 4: mode = "CFB"; break; default: throw new RuntimeException("Unknown AES final digit: " + finalDigit); } switch (finalDigit / 10) { case 0: keySize = 128; break; case 2: keySize = 192; break; case 4: keySize = 256; break; default: throw new RuntimeException("Unknown AES final digit: " + finalDigit); } } else if (oid.startsWith("1.2.840.113549.3.")) { // Let's trim this OID to make life a little easier. oid = oid.substring("1.2.840.113549.3.".length()); if (oid.equals("2") || oid.startsWith("2.")) { // 1.2.840.113549.3.2 - RC2-CBC // Note: keysize determined in PKCS8 Version 2.0 ASN.1 field. cipher = "RC2"; keySize = pkcs8.keySize * 8; } else if (oid.equals("4") || oid.startsWith("4.")) { // 1.2.840.113549.3.4 - RC4 // Note: keysize determined in PKCS8 Version 2.0 ASN.1 field. cipher = "RC4"; keySize = pkcs8.keySize * 8; } else if (oid.equals("7") || oid.startsWith("7.")) { // 1.2.840.113549.3.7 - DES-EDE3-CBC cipher = "DESede"; keySize = 192; } else if (oid.equals("9") || oid.startsWith("9.")) { // 1.2.840.113549.3.9 - RC5 CBC Pad // Note: keysize determined in PKCS8 Version 2.0 ASN.1 field. keySize = pkcs8.keySize * 8; cipher = "RC5"; // Need to find out more about RC5. // How do I create the RC5ParameterSpec? // (int version, int rounds, int wordSize, byte[] iv) } } } // The pkcs8 structure has been thoroughly examined. If we don't have // a cipher or hash at this point, then we don't support the file we // were given. if (cipher == null || hash == null) { throw new ProbablyNotPKCS8Exception( "Unsupported PKCS8 format. oid1=[" + pkcs8.oid1 + "], oid2=[" + pkcs8.oid2 + "]"); } // In PKCS8 Version 1.5 we need to derive an 8 byte IV. In those cases // the ASN.1 structure doesn't have the IV, anyway, so I can use that // to decide whether to derive one or not. // // Note: if AES, then IV has to be 16 bytes. if (pkcs8.iv == null) { ivSize = 64; } byte[] salt = pkcs8.salt; int ic = pkcs8.iterationCount; // PKCS8 converts the password to a byte[] array using a simple // cast. This byte[] array is ignored if we're using the PKCS12 // key derivation, since that employs a different technique. byte[] pwd = new byte[password.length]; for (int i = 0; i < pwd.length; i++) { pwd[i] = (byte) password[i]; } DerivedKey dk; if (usePKCS12PasswordPadding) { MessageDigest md = MessageDigest.getInstance(hash); dk = deriveKeyPKCS12(password, salt, ic, keySize, ivSize, md); } else { if (isVersion1) { MessageDigest md = MessageDigest.getInstance(hash); dk = deriveKeyV1(pwd, salt, ic, keySize, ivSize, md); } else { Mac mac = Mac.getInstance(hash); dk = deriveKeyV2(pwd, salt, ic, keySize, ivSize, mac); } } return decrypt(cipher, mode, dk, use2DES, pkcs8.iv, pkcs8.bigPayload); } public static DerivedKey deriveKeyV1(byte[] password, byte[] salt, int iterations, int keySizeInBits, int ivSizeInBits, MessageDigest md) { int keySize = keySizeInBits / 8; int ivSize = ivSizeInBits / 8; md.reset(); md.update(password); byte[] result = md.digest(salt); for (int i = 1; i < iterations; i++) { // Hash of the hash for each of the iterations. result = md.digest(result); } byte[] key = new byte[keySize]; byte[] iv = new byte[ivSize]; System.arraycopy(result, 0, key, 0, key.length); System.arraycopy(result, key.length, iv, 0, iv.length); return new DerivedKey(key, iv); } public static DerivedKey deriveKeyPKCS12(char[] password, byte[] salt, int iterations, int keySizeInBits, int ivSizeInBits, MessageDigest md) { byte[] pwd; if (password.length > 0) { pwd = new byte[(password.length + 1) * 2]; for (int i = 0; i < password.length; i++) { pwd[i * 2] = (byte) (password[i] >>> 8); pwd[i * 2 + 1] = (byte) password[i]; } } else { pwd = new byte[0]; } int keySize = keySizeInBits / 8; int ivSize = ivSizeInBits / 8; byte[] key = pkcs12(1, keySize, salt, pwd, iterations, md); byte[] iv = pkcs12(2, ivSize, salt, pwd, iterations, md); return new DerivedKey(key, iv); } /** * This PKCS12 key derivation code comes from BouncyCastle. * * @param idByte 1 == key, 2 == iv * @param n keysize or ivsize * @param salt 8 byte salt * @param password password * @param iterationCount iteration-count * @param md The message digest to use * @return byte[] the derived key */ @SuppressWarnings("PMD.UselessParentheses") private static byte[] pkcs12(int idByte, int n, byte[] salt, byte[] password, int iterationCount, MessageDigest md) { int u = md.getDigestLength(); // sha1, md2, md5 all use 512 bits. But future hashes might not. int v = 512 / 8; md.reset(); byte[] dD = new byte[v]; byte[] dKey = new byte[n]; for (int i = 0; i != dD.length; i++) { dD[i] = (byte) idByte; } byte[] sS; if (salt != null && salt.length != 0) { sS = new byte[v * ((salt.length + v - 1) / v)]; for (int i = 0; i != sS.length; i++) { sS[i] = salt[i % salt.length]; } } else { sS = new byte[0]; } byte[] pP; if (password != null && password.length != 0) { pP = new byte[v * ((password.length + v - 1) / v)]; for (int i = 0; i != pP.length; i++) { pP[i] = password[i % password.length]; } } else { pP = new byte[0]; } byte[] iI = new byte[sS.length + pP.length]; System.arraycopy(sS, 0, iI, 0, sS.length); System.arraycopy(pP, 0, iI, sS.length, pP.length); byte[] bB = new byte[v]; int c = (n + u - 1) / u; for (int i = 1; i <= c; i++) { md.update(dD); byte[] result = md.digest(iI); for (int j = 1; j != iterationCount; j++) { result = md.digest(result); } for (int j = 0; j != bB.length; j++) { bB[j] = result[j % result.length]; } for (int j = 0; j < (iI.length / v); j++) { /* * add a + b + 1, returning the result in a. The a value is treated * as a BigInteger of length (b.length * 8) bits. The result is * modulo 2^b.length in case of overflow. */ int aOff = j * v; int bLast = bB.length - 1; int x = (bB[bLast] & 0xff) + (iI[aOff + bLast] & 0xff) + 1; iI[aOff + bLast] = (byte) x; x >>>= 8; for (int k = bB.length - 2; k >= 0; k--) { x += (bB[k] & 0xff) + (iI[aOff + k] & 0xff); iI[aOff + k] = (byte) x; x >>>= 8; } } if (i == c) { System.arraycopy(result, 0, dKey, (i - 1) * u, dKey.length - ((i - 1) * u)); } else { System.arraycopy(result, 0, dKey, (i - 1) * u, result.length); } } return dKey; } public static DerivedKey deriveKeyV2(byte[] password, byte[] salt, int iterations, int keySizeInBits, int ivSizeInBits, Mac mac) throws InvalidKeyException { int keySize = keySizeInBits / 8; int ivSize = ivSizeInBits / 8; // Because we're using an Hmac, we need to initialize with a SecretKey. // HmacSHA1 doesn't need SecretKeySpec's 2nd parameter, hence the "N/A". SecretKeySpec sk = new SecretKeySpec(password, "N/A"); mac.init(sk); int macLength = mac.getMacLength(); int derivedKeyLength = keySize + ivSize; int blocks = (derivedKeyLength + macLength - 1) / macLength; byte[] blockIndex = new byte[4]; byte[] finalResult = new byte[blocks * macLength]; for (int i = 1; i <= blocks; i++) { int offset = (i - 1) * macLength; blockIndex[0] = (byte) (i >>> 24); blockIndex[1] = (byte) (i >>> 16); blockIndex[2] = (byte) (i >>> 8); blockIndex[3] = (byte) i; mac.reset(); mac.update(salt); byte[] result = mac.doFinal(blockIndex); System.arraycopy(result, 0, finalResult, offset, result.length); for (int j = 1; j < iterations; j++) { mac.reset(); result = mac.doFinal(result); for (int k = 0; k < result.length; k++) { finalResult[offset + k] ^= result[k]; } } } byte[] key = new byte[keySize]; byte[] iv = new byte[ivSize]; System.arraycopy(finalResult, 0, key, 0, key.length); System.arraycopy(finalResult, key.length, iv, 0, iv.length); return new DerivedKey(key, iv); } public static byte[] formatAsPKCS8(byte[] privateKey, String oid, PkcsStructure pkcs8) throws IOException { Asn1Integer derZero = new Asn1Integer(BigInteger.ZERO); Asn1Sequence outterSeq = new Asn1Sequence(); Asn1Sequence innerSeq = new Asn1Sequence(); Asn1OctetString octetsToAppend; Asn1ObjectIdentifier derOID = new Asn1ObjectIdentifier(oid); innerSeq.addItem(derOID); if (DSA_OID.equals(oid)) { if (pkcs8 == null) { try { pkcs8 = PkcsUtil.analyze(privateKey); } catch (Exception e) { throw new RuntimeException("asn1 parse failure " + e); } } if (pkcs8.derIntegers == null || pkcs8.derIntegers.size() < 6) { throw new RuntimeException("invalid DSA key - can't find P, Q, G, X"); } Asn1Integer[] ints = new Asn1Integer[pkcs8.derIntegers.size()]; pkcs8.derIntegers.toArray(ints); Asn1Integer p = ints[1]; Asn1Integer q = ints[2]; Asn1Integer g = ints[3]; Asn1Integer x = ints[5]; byte[] encodedX = x.encode(); octetsToAppend = new Asn1OctetString(encodedX); Asn1Sequence pqgSeq = new Asn1Sequence(); pqgSeq.addItem(p); pqgSeq.addItem(q); pqgSeq.addItem(g); innerSeq.addItem(pqgSeq); } else { innerSeq.addItem(Asn1Null.INSTANCE); octetsToAppend = new Asn1OctetString(privateKey); } outterSeq.addItem(derZero); outterSeq.addItem(innerSeq); outterSeq.addItem(octetsToAppend); return outterSeq.encode(); } private static boolean allZeroes(byte[] b) { for (int i = 0; i < b.length; i++) { if (b[i] != 0) { return false; } } return true; } public static void main(String[] args) throws Exception { String password = "changeit"; if (args.length == 0) { System.out.println( "Usage1: [password] [file:private-key]" + " Prints decrypted PKCS8 key (base64)."); System.out.println( "Usage2: [password] [file1] [file2] etc..." + " Checks that all private keys are equal."); System.out.println("Usage2 assumes that all files can be decrypted with the same password."); } else if (args.length == 1 || args.length == 2) { FileInputStream in = new FileInputStream(args[args.length - 1]); if (args.length == 2) { password = args[0]; } byte[] bytes = Util.streamToBytes(in); PKCS8Key key = new PKCS8Key(bytes, password.toCharArray()); PEMItem item = new PEMItem(key.getDecryptedBytes(), "PRIVATE KEY"); byte[] pem = PEMUtil.encode(Collections.singleton(item)); System.out.write(pem); } else { byte[] original = null; File f = new File(args[0]); int i = 0; if (!f.exists()) { // File0 doesn't exist, so it must be a password! password = args[0]; i++; } for (; i < args.length; i++) { FileInputStream in = new FileInputStream(args[i]); byte[] bytes = Util.streamToBytes(in); PKCS8Key key = null; try { key = new PKCS8Key(bytes, password.toCharArray()); } catch (Exception e) { System.out.println(" FAILED! " + args[i] + " " + e); } if (key != null) { byte[] decrypted = key.getDecryptedBytes(); int keySize = key.getKeySize(); String keySizeStr = "" + keySize; if (keySize < 10) { keySizeStr = " " + keySizeStr; } else if (keySize < 100) { keySizeStr = " " + keySizeStr; } StringBuffer buf = new StringBuffer(key.getTransformation()); int maxLen = "Blowfish/CBC/PKCS5Padding".length(); for (int j = buf.length(); j < maxLen; j++) { buf.append(' '); } String transform = buf.toString(); String type = key.isDSA() ? "DSA" : "RSA"; if (original == null) { original = decrypted; System.out.println( " SUCCESS \t" + type + "\t" + transform + "\t" + keySizeStr + "\t" + args[i]); } else { boolean identical = Arrays.equals(original, decrypted); if (!identical) { System.out.println("***FAILURE*** \t" + type + "\t" + transform + "\t" + keySizeStr + "\t" + args[i]); } else { System.out.println(" SUCCESS \t" + type + "\t" + transform + "\t" + keySizeStr + "\t" + args[i]); } } } } } } }