blob: b39e3be09753f8d9965a5c9c9af2937d122b3e5e [file] [log] [blame]
/*
* $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]);
}
}
}
}
}
}
}