|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package com.sun.crypto.provider; |
|
|
|
import java.io.IOException; |
|
import java.security.Key; |
|
import java.security.PrivateKey; |
|
import java.security.Provider; |
|
import java.security.KeyFactory; |
|
import java.security.MessageDigest; |
|
import java.security.GeneralSecurityException; |
|
import java.security.NoSuchAlgorithmException; |
|
import java.security.UnrecoverableKeyException; |
|
import java.security.AlgorithmParameters; |
|
import java.security.spec.InvalidParameterSpecException; |
|
import java.security.spec.PKCS8EncodedKeySpec; |
|
import java.util.Arrays; |
|
|
|
import javax.crypto.Cipher; |
|
import javax.crypto.CipherSpi; |
|
import javax.crypto.SecretKey; |
|
import javax.crypto.SealedObject; |
|
import javax.crypto.spec.*; |
|
import javax.security.auth.DestroyFailedException; |
|
|
|
import sun.security.x509.AlgorithmId; |
|
import sun.security.util.ObjectIdentifier; |
|
import sun.security.util.SecurityProperties; |
|
|
|
/** |
|
* This class implements a protection mechanism for private keys. In JCE, we |
|
* use a stronger protection mechanism than in the JDK, because we can use |
|
* the <code>Cipher</code> class. |
|
* Private keys are protected using the JCE mechanism, and are recovered using |
|
* either the JDK or JCE mechanism, depending on how the key has been |
|
* protected. This allows us to parse Sun's keystore implementation that ships |
|
* with JDK 1.2. |
|
* |
|
* @author Jan Luehe |
|
* |
|
* |
|
* @see JceKeyStore |
|
*/ |
|
|
|
final class KeyProtector { |
|
|
|
|
|
private static final String PBE_WITH_MD5_AND_DES3_CBC_OID |
|
= "1.3.6.1.4.1.42.2.19.1"; |
|
|
|
// JavaSoft proprietary key-protection algorithm (used to protect private |
|
|
|
private static final String KEY_PROTECTOR_OID = "1.3.6.1.4.1.42.2.17.1.1"; |
|
|
|
private static final int MAX_ITERATION_COUNT = 5000000; |
|
private static final int MIN_ITERATION_COUNT = 10000; |
|
private static final int DEFAULT_ITERATION_COUNT = 200000; |
|
private static final int SALT_LEN = 20; |
|
private static final int DIGEST_LEN = 20; |
|
private static final int ITERATION_COUNT; |
|
|
|
// the password used for protecting/recovering keys passed through this |
|
|
|
private char[] password; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
static { |
|
int iterationCount = DEFAULT_ITERATION_COUNT; |
|
String ic = SecurityProperties.privilegedGetOverridable( |
|
"jdk.jceks.iterationCount"); |
|
if (ic != null && !ic.isEmpty()) { |
|
try { |
|
iterationCount = Integer.parseInt(ic); |
|
if (iterationCount < MIN_ITERATION_COUNT || |
|
iterationCount > MAX_ITERATION_COUNT) { |
|
iterationCount = DEFAULT_ITERATION_COUNT; |
|
} |
|
} catch (NumberFormatException e) {} |
|
} |
|
ITERATION_COUNT = iterationCount; |
|
} |
|
|
|
KeyProtector(char[] password) { |
|
if (password == null) { |
|
throw new IllegalArgumentException("password can't be null"); |
|
} |
|
this.password = password; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
byte[] protect(PrivateKey key) |
|
throws Exception |
|
{ |
|
|
|
byte[] salt = new byte[8]; |
|
SunJCE.getRandom().nextBytes(salt); |
|
|
|
|
|
PBEParameterSpec pbeSpec = new PBEParameterSpec(salt, ITERATION_COUNT); |
|
|
|
|
|
PBEKeySpec pbeKeySpec = new PBEKeySpec(this.password); |
|
SecretKey sKey = null; |
|
PBEWithMD5AndTripleDESCipher cipher; |
|
try { |
|
sKey = new PBEKey(pbeKeySpec, "PBEWithMD5AndTripleDES"); |
|
|
|
cipher = new PBEWithMD5AndTripleDESCipher(); |
|
cipher.engineInit(Cipher.ENCRYPT_MODE, sKey, pbeSpec, null); |
|
} finally { |
|
pbeKeySpec.clearPassword(); |
|
if (sKey != null) sKey.destroy(); |
|
} |
|
byte[] plain = key.getEncoded(); |
|
byte[] encrKey = cipher.engineDoFinal(plain, 0, plain.length); |
|
Arrays.fill(plain, (byte)0x00); |
|
|
|
// wrap encrypted private key in EncryptedPrivateKeyInfo |
|
|
|
AlgorithmParameters pbeParams = |
|
AlgorithmParameters.getInstance("PBE", SunJCE.getInstance()); |
|
pbeParams.init(pbeSpec); |
|
|
|
AlgorithmId encrAlg = new AlgorithmId |
|
(new ObjectIdentifier(PBE_WITH_MD5_AND_DES3_CBC_OID), pbeParams); |
|
return new EncryptedPrivateKeyInfo(encrAlg,encrKey).getEncoded(); |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
Key recover(EncryptedPrivateKeyInfo encrInfo) |
|
throws UnrecoverableKeyException, NoSuchAlgorithmException |
|
{ |
|
byte[] plain = null; |
|
SecretKey sKey = null; |
|
try { |
|
String encrAlg = encrInfo.getAlgorithm().getOID().toString(); |
|
if (!encrAlg.equals(PBE_WITH_MD5_AND_DES3_CBC_OID) |
|
&& !encrAlg.equals(KEY_PROTECTOR_OID)) { |
|
throw new UnrecoverableKeyException("Unsupported encryption " |
|
+ "algorithm"); |
|
} |
|
|
|
if (encrAlg.equals(KEY_PROTECTOR_OID)) { |
|
|
|
plain = recover(encrInfo.getEncryptedData()); |
|
} else { |
|
byte[] encodedParams = |
|
encrInfo.getAlgorithm().getEncodedParams(); |
|
|
|
|
|
AlgorithmParameters pbeParams = |
|
AlgorithmParameters.getInstance("PBE"); |
|
pbeParams.init(encodedParams); |
|
PBEParameterSpec pbeSpec = |
|
pbeParams.getParameterSpec(PBEParameterSpec.class); |
|
if (pbeSpec.getIterationCount() > MAX_ITERATION_COUNT) { |
|
throw new IOException("PBE iteration count too large"); |
|
} |
|
|
|
|
|
PBEKeySpec pbeKeySpec = new PBEKeySpec(this.password); |
|
sKey = new PBEKey(pbeKeySpec, "PBEWithMD5AndTripleDES"); |
|
pbeKeySpec.clearPassword(); |
|
|
|
|
|
PBEWithMD5AndTripleDESCipher cipher; |
|
cipher = new PBEWithMD5AndTripleDESCipher(); |
|
cipher.engineInit(Cipher.DECRYPT_MODE, sKey, pbeSpec, null); |
|
plain=cipher.engineDoFinal(encrInfo.getEncryptedData(), 0, |
|
encrInfo.getEncryptedData().length); |
|
} |
|
|
|
// determine the private-key algorithm, and parse private key |
|
|
|
String oidName = new AlgorithmId |
|
(new PrivateKeyInfo(plain).getAlgorithm().getOID()).getName(); |
|
KeyFactory kFac = KeyFactory.getInstance(oidName); |
|
return kFac.generatePrivate(new PKCS8EncodedKeySpec(plain)); |
|
} catch (NoSuchAlgorithmException ex) { |
|
// Note: this catch needed to be here because of the |
|
|
|
throw ex; |
|
} catch (IOException ioe) { |
|
throw new UnrecoverableKeyException(ioe.getMessage()); |
|
} catch (GeneralSecurityException gse) { |
|
throw new UnrecoverableKeyException(gse.getMessage()); |
|
} finally { |
|
if (plain != null) Arrays.fill(plain, (byte)0x00); |
|
if (sKey != null) { |
|
try { |
|
sKey.destroy(); |
|
} catch (DestroyFailedException e) { |
|
//shouldn't happen |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private byte[] recover(byte[] protectedKey) |
|
throws UnrecoverableKeyException, NoSuchAlgorithmException |
|
{ |
|
int i, j; |
|
byte[] digest; |
|
int numRounds; |
|
int xorOffset; |
|
int encrKeyLen; |
|
|
|
MessageDigest md = MessageDigest.getInstance("SHA"); |
|
|
|
// Get the salt associated with this key (the first SALT_LEN bytes of |
|
|
|
byte[] salt = new byte[SALT_LEN]; |
|
System.arraycopy(protectedKey, 0, salt, 0, SALT_LEN); |
|
|
|
|
|
encrKeyLen = protectedKey.length - SALT_LEN - DIGEST_LEN; |
|
numRounds = encrKeyLen / DIGEST_LEN; |
|
if ((encrKeyLen % DIGEST_LEN) != 0) |
|
numRounds++; |
|
|
|
|
|
byte[] encrKey = new byte[encrKeyLen]; |
|
System.arraycopy(protectedKey, SALT_LEN, encrKey, 0, encrKeyLen); |
|
|
|
|
|
byte[] xorKey = new byte[encrKey.length]; |
|
|
|
|
|
byte[] passwdBytes = new byte[password.length * 2]; |
|
for (i=0, j=0; i<password.length; i++) { |
|
passwdBytes[j++] = (byte)(password[i] >> 8); |
|
passwdBytes[j++] = (byte)password[i]; |
|
} |
|
|
|
|
|
for (i = 0, xorOffset = 0, digest = salt; |
|
i < numRounds; |
|
i++, xorOffset += DIGEST_LEN) { |
|
md.update(passwdBytes); |
|
md.update(digest); |
|
digest = md.digest(); |
|
md.reset(); |
|
|
|
if (i < numRounds - 1) { |
|
System.arraycopy(digest, 0, xorKey, xorOffset, |
|
digest.length); |
|
} else { |
|
System.arraycopy(digest, 0, xorKey, xorOffset, |
|
xorKey.length - xorOffset); |
|
} |
|
} |
|
|
|
|
|
byte[] plainKey = new byte[encrKey.length]; |
|
for (i = 0; i < plainKey.length; i++) { |
|
plainKey[i] = (byte)(encrKey[i] ^ xorKey[i]); |
|
} |
|
|
|
// Check the integrity of the recovered key by concatenating it with |
|
// the password, digesting the concatenation, and comparing the |
|
// result of the digest operation with the digest provided at the end |
|
// of <code>protectedKey</code>. If the two digest values are |
|
|
|
md.update(passwdBytes); |
|
Arrays.fill(passwdBytes, (byte)0x00); |
|
passwdBytes = null; |
|
md.update(plainKey); |
|
digest = md.digest(); |
|
md.reset(); |
|
for (i = 0; i < digest.length; i++) { |
|
if (digest[i] != protectedKey[SALT_LEN + encrKeyLen + i]) { |
|
throw new UnrecoverableKeyException("Cannot recover key"); |
|
} |
|
} |
|
return plainKey; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
SealedObject seal(Key key) |
|
throws Exception |
|
{ |
|
|
|
byte[] salt = new byte[8]; |
|
SunJCE.getRandom().nextBytes(salt); |
|
|
|
|
|
PBEParameterSpec pbeSpec = new PBEParameterSpec(salt, ITERATION_COUNT); |
|
|
|
|
|
PBEKeySpec pbeKeySpec = new PBEKeySpec(this.password); |
|
SecretKey sKey = null; |
|
Cipher cipher; |
|
try { |
|
sKey = new PBEKey(pbeKeySpec, "PBEWithMD5AndTripleDES"); |
|
pbeKeySpec.clearPassword(); |
|
|
|
|
|
PBEWithMD5AndTripleDESCipher cipherSpi; |
|
cipherSpi = new PBEWithMD5AndTripleDESCipher(); |
|
cipher = new CipherForKeyProtector(cipherSpi, SunJCE.getInstance(), |
|
"PBEWithMD5AndTripleDES"); |
|
cipher.init(Cipher.ENCRYPT_MODE, sKey, pbeSpec); |
|
} finally { |
|
if (sKey != null) sKey.destroy(); |
|
} |
|
return new SealedObjectForKeyProtector(key, cipher); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
Key unseal(SealedObject so, int maxLength) |
|
throws NoSuchAlgorithmException, UnrecoverableKeyException { |
|
SecretKey sKey = null; |
|
try { |
|
|
|
PBEKeySpec pbeKeySpec = new PBEKeySpec(this.password); |
|
sKey = new PBEKey(pbeKeySpec, "PBEWithMD5AndTripleDES"); |
|
pbeKeySpec.clearPassword(); |
|
|
|
SealedObjectForKeyProtector soForKeyProtector = null; |
|
if (!(so instanceof SealedObjectForKeyProtector)) { |
|
soForKeyProtector = new SealedObjectForKeyProtector(so); |
|
} else { |
|
soForKeyProtector = (SealedObjectForKeyProtector)so; |
|
} |
|
AlgorithmParameters params = soForKeyProtector.getParameters(); |
|
if (params == null) { |
|
throw new UnrecoverableKeyException("Cannot get " + |
|
"algorithm parameters"); |
|
} |
|
PBEParameterSpec pbeSpec; |
|
try { |
|
pbeSpec = params.getParameterSpec(PBEParameterSpec.class); |
|
} catch (InvalidParameterSpecException ipse) { |
|
throw new IOException("Invalid PBE algorithm parameters"); |
|
} |
|
if (pbeSpec.getIterationCount() > MAX_ITERATION_COUNT) { |
|
throw new IOException("PBE iteration count too large"); |
|
} |
|
PBEWithMD5AndTripleDESCipher cipherSpi; |
|
cipherSpi = new PBEWithMD5AndTripleDESCipher(); |
|
Cipher cipher = new CipherForKeyProtector(cipherSpi, |
|
SunJCE.getInstance(), |
|
"PBEWithMD5AndTripleDES"); |
|
cipher.init(Cipher.DECRYPT_MODE, sKey, params); |
|
return soForKeyProtector.getKey(cipher, maxLength); |
|
} catch (NoSuchAlgorithmException ex) { |
|
// Note: this catch needed to be here because of the |
|
|
|
throw ex; |
|
} catch (IOException ioe) { |
|
throw new UnrecoverableKeyException(ioe.getMessage()); |
|
} catch (ClassNotFoundException cnfe) { |
|
throw new UnrecoverableKeyException(cnfe.getMessage()); |
|
} catch (GeneralSecurityException gse) { |
|
throw new UnrecoverableKeyException(gse.getMessage()); |
|
} finally { |
|
if (sKey != null) { |
|
try { |
|
sKey.destroy(); |
|
} catch (DestroyFailedException e) { |
|
//shouldn't happen |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
final class CipherForKeyProtector extends javax.crypto.Cipher { |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected CipherForKeyProtector(CipherSpi cipherSpi, |
|
Provider provider, |
|
String transformation) { |
|
super(cipherSpi, provider, transformation); |
|
} |
|
} |