/* * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ package sun.security.provider; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.Key; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.util.*; import sun.security.pkcs.PKCS8Key; import sun.security.pkcs.EncryptedPrivateKeyInfo; import sun.security.x509.AlgorithmId; import sun.security.util.ObjectIdentifier; import sun.security.util.DerValue; /** * This is an implementation of a Sun proprietary, exportable algorithm * intended for use when protecting (or recovering the cleartext version of) * sensitive keys. * This algorithm is not intended as a general purpose cipher. * * This is how the algorithm works for key protection: * * p - user password * s - random salt * X - xor key * P - to-be-protected key * Y - protected key * R - what gets stored in the keystore * * Step 1: * Take the user's password, append a random salt (of fixed size) to it, * and hash it: d1 = digest(p, s) * Store d1 in X. * * Step 2: * Take the user's password, append the digest result from the previous step, * and hash it: dn = digest(p, dn-1). * Store dn in X (append it to the previously stored digests). * Repeat this step until the length of X matches the length of the private key * P. * * Step 3: * XOR X and P, and store the result in Y: Y = X XOR P. * * Step 4: * Store s, Y, and digest(p, P) in the result buffer R: * R = s + Y + digest(p, P), where "+" denotes concatenation. * (NOTE: digest(p, P) is stored in the result buffer, so that when the key is * recovered, we can check if the recovered key indeed matches the original * key.) R is stored in the keystore. * * The protected key is recovered as follows: * * Step1 and Step2 are the same as above, except that the salt is not randomly * generated, but taken from the result R of step 4 (the first length(s) * bytes). * * Step 3 (XOR operation) yields the plaintext key. * * Then concatenate the password with the recovered key, and compare with the * last length(digest(p, P)) bytes of R. If they match, the recovered key is * indeed the same key as the original key. * * @author Jan Luehe * * * @see java.security.KeyStore * @see JavaKeyStore * @see KeyTool * * @since 1.2 */ final class KeyProtector { private static final int SALT_LEN = 20; // the salt length private static final String DIGEST_ALG = "SHA"; private static final int DIGEST_LEN = 20; // defined by JavaSoft private static final String KEY_PROTECTOR_OID = "1.3.6.1.4.1.42.2.17.1.1"; // The password used for protecting/recovering keys passed through this // key protector. We store it as a byte array, so that we can digest it. private byte[] passwdBytes; private MessageDigest md; /** * Creates an instance of this class, and initializes it with the given * password. * * <p>The password is expected to be in printable ASCII. * Normal rules for good password selection apply: at least * seven characters, mixed case, with punctuation encouraged. * Phrases or words which are easily guessed, for example by * being found in dictionaries, are bad. */ public KeyProtector(char[] password) throws NoSuchAlgorithmException { int i, j; if (password == null) { throw new IllegalArgumentException("password can't be null"); } md = MessageDigest.getInstance(DIGEST_ALG); // Convert password to byte array, so that it can be digested 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]; } } /** * Ensures that the password bytes of this key protector are * set to zero when there are no more references to it. */ protected void finalize() { if (passwdBytes != null) { Arrays.fill(passwdBytes, (byte)0x00); passwdBytes = null; } } /* * Protects the given plaintext key, using the password provided at * construction time. */ public byte[] protect(Key key) throws KeyStoreException { int i; int numRounds; byte[] digest; int xorOffset; // offset in xorKey where next digest will be stored int encrKeyOffset = 0; if (key == null) { throw new IllegalArgumentException("plaintext key can't be null"); } if (!"PKCS#8".equalsIgnoreCase(key.getFormat())) { throw new KeyStoreException( "Cannot get key bytes, not PKCS#8 encoded"); } byte[] plainKey = key.getEncoded(); if (plainKey == null) { throw new KeyStoreException( "Cannot get key bytes, encoding not supported"); } // Determine the number of digest rounds numRounds = plainKey.length / DIGEST_LEN; if ((plainKey.length % DIGEST_LEN) != 0) numRounds++; // Create a random salt byte[] salt = new byte[SALT_LEN]; SecureRandom random = new SecureRandom(); random.nextBytes(salt); // Set up the byte array which will be XORed with "plainKey" byte[] xorKey = new byte[plainKey.length]; // Compute the digests, and store them in "xorKey" for (i = 0, xorOffset = 0, digest = salt; i < numRounds; i++, xorOffset += DIGEST_LEN) { md.update(passwdBytes); md.update(digest); digest = md.digest(); md.reset(); // Copy the digest into "xorKey" if (i < numRounds - 1) { System.arraycopy(digest, 0, xorKey, xorOffset, digest.length); } else { System.arraycopy(digest, 0, xorKey, xorOffset, xorKey.length - xorOffset); } } // XOR "plainKey" with "xorKey", and store the result in "tmpKey" byte[] tmpKey = new byte[plainKey.length]; for (i = 0; i < tmpKey.length; i++) { tmpKey[i] = (byte)(plainKey[i] ^ xorKey[i]); } // Store salt and "tmpKey" in "encrKey" byte[] encrKey = new byte[salt.length + tmpKey.length + DIGEST_LEN]; System.arraycopy(salt, 0, encrKey, encrKeyOffset, salt.length); encrKeyOffset += salt.length; System.arraycopy(tmpKey, 0, encrKey, encrKeyOffset, tmpKey.length); encrKeyOffset += tmpKey.length; // Append digest(password, plainKey) as an integrity check to "encrKey" md.update(passwdBytes); Arrays.fill(passwdBytes, (byte)0x00); passwdBytes = null; md.update(plainKey); digest = md.digest(); md.reset(); System.arraycopy(digest, 0, encrKey, encrKeyOffset, digest.length); // wrap the protected private key in a PKCS#8-style // EncryptedPrivateKeyInfo, and returns its encoding AlgorithmId encrAlg; try { encrAlg = new AlgorithmId(new ObjectIdentifier(KEY_PROTECTOR_OID)); return new EncryptedPrivateKeyInfo(encrAlg,encrKey).getEncoded(); } catch (IOException ioe) { throw new KeyStoreException(ioe.getMessage()); } } /* * Recovers the plaintext version of the given key (in protected format), * using the password provided at construction time. */ public Key recover(EncryptedPrivateKeyInfo encrInfo) throws UnrecoverableKeyException { int i; byte[] digest; int numRounds; int xorOffset; // offset in xorKey where next digest will be stored int encrKeyLen; // the length of the encrpyted key // do we support the algorithm? AlgorithmId encrAlg = encrInfo.getAlgorithm(); if (!(encrAlg.getOID().toString().equals(KEY_PROTECTOR_OID))) { throw new UnrecoverableKeyException("Unsupported key protection " + "algorithm"); } byte[] protectedKey = encrInfo.getEncryptedData(); /* * Get the salt associated with this key (the first SALT_LEN bytes of * <code>protectedKey</code>) */ byte[] salt = new byte[SALT_LEN]; System.arraycopy(protectedKey, 0, salt, 0, SALT_LEN); // Determine the number of digest rounds encrKeyLen = protectedKey.length - SALT_LEN - DIGEST_LEN; numRounds = encrKeyLen / DIGEST_LEN; if ((encrKeyLen % DIGEST_LEN) != 0) numRounds++; // Get the encrypted key portion and store it in "encrKey" byte[] encrKey = new byte[encrKeyLen]; System.arraycopy(protectedKey, SALT_LEN, encrKey, 0, encrKeyLen); // Set up the byte array which will be XORed with "encrKey" byte[] xorKey = new byte[encrKey.length]; // Compute the digests, and store them in "xorKey" for (i = 0, xorOffset = 0, digest = salt; i < numRounds; i++, xorOffset += DIGEST_LEN) { md.update(passwdBytes); md.update(digest); digest = md.digest(); md.reset(); // Copy the digest into "xorKey" if (i < numRounds - 1) { System.arraycopy(digest, 0, xorKey, xorOffset, digest.length); } else { System.arraycopy(digest, 0, xorKey, xorOffset, xorKey.length - xorOffset); } } // XOR "encrKey" with "xorKey", and store the result in "plainKey" 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 * different, throw an exception. */ 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"); } } // The parseKey() method of PKCS8Key parses the key // algorithm and instantiates the appropriate key factory, // which in turn parses the key material. try { return PKCS8Key.parseKey(new DerValue(plainKey)); } catch (IOException ioe) { throw new UnrecoverableKeyException(ioe.getMessage()); } } }