| /* |
| * $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]); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| } |