/* * Copyright (c) 1999, 2010, Oracle and/or its affiliates. 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package sun.security.pkcs12; import java.io.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Key; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.KeyStoreSpi; import java.security.KeyStoreException; import java.security.UnrecoverableKeyException; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.cert.CertificateException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.*; import java.math.*; import java.security.AlgorithmParameters; import java.security.spec.AlgorithmParameterSpec; import javax.crypto.spec.PBEParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKey; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.security.auth.x500.X500Principal; import sun.security.util.DerInputStream; import sun.security.util.DerOutputStream; import sun.security.util.DerValue; import sun.security.util.ObjectIdentifier; import sun.security.pkcs.ContentInfo; import sun.security.x509.AlgorithmId; import sun.security.pkcs.EncryptedPrivateKeyInfo; /** * This class provides the keystore implementation referred to as "PKCS12". * Implements the PKCS#12 PFX protected using the Password privacy mode. * The contents are protected using Password integrity mode. * * Currently we support following PBE algorithms: * - pbeWithSHAAnd3KeyTripleDESCBC to encrypt private keys * - pbeWithSHAAnd40BitRC2CBC to encrypt certificates * * Supported encryption of various implementations : * * Software and mode. Certificate encryption Private key encryption * --------------------------------------------------------------------- * MSIE4 (domestic 40 bit RC2. 40 bit RC2 * and xport versions) * PKCS#12 export. * * MSIE4, 5 (domestic 40 bit RC2, 40 bit RC2, * and export versions) 3 key triple DES 3 key triple DES * PKCS#12 import. * * MSIE5 40 bit RC2 3 key triple DES, * PKCS#12 export. with SHA1 (168 bits) * * Netscape Communicator 40 bit RC2 3 key triple DES, * (domestic and export with SHA1 (168 bits) * versions) PKCS#12 export * * Netscape Communicator 40 bit ciphers only All. * (export version) * PKCS#12 import. * * Netscape Communicator All. All. * (domestic or fortified * version) PKCS#12 import. * * OpenSSL PKCS#12 code. All. All. * --------------------------------------------------------------------- * * NOTE: Currently PKCS12 KeyStore does not support TrustedCertEntries. * PKCS#12 is mainly used to deliver private keys with their associated * certificate chain and aliases. In a PKCS12 keystore, entries are * identified by the alias, and a localKeyId is required to match the * private key with the certificate. * * @author Seema Malkani * @author Jeff Nisewanger * @author Jan Luehe * * @see KeyProtector * @see java.security.KeyStoreSpi * @see KeyTool * * */ public final class PKCS12KeyStore extends KeyStoreSpi { public static final int VERSION_3 = 3; private static final int keyBag[] = {1, 2, 840, 113549, 1, 12, 10, 1, 2}; private static final int certBag[] = {1, 2, 840, 113549, 1, 12, 10, 1, 3}; private static final int pkcs9Name[] = {1, 2, 840, 113549, 1, 9, 20}; private static final int pkcs9KeyId[] = {1, 2, 840, 113549, 1, 9, 21}; private static final int pkcs9certType[] = {1, 2, 840, 113549, 1, 9, 22, 1}; private static final int pbeWithSHAAnd40BitRC2CBC[] = {1, 2, 840, 113549, 1, 12, 1, 6}; private static final int pbeWithSHAAnd3KeyTripleDESCBC[] = {1, 2, 840, 113549, 1, 12, 1, 3}; private static ObjectIdentifier PKCS8ShroudedKeyBag_OID; private static ObjectIdentifier CertBag_OID; private static ObjectIdentifier PKCS9FriendlyName_OID; private static ObjectIdentifier PKCS9LocalKeyId_OID; private static ObjectIdentifier PKCS9CertType_OID; private static ObjectIdentifier pbeWithSHAAnd40BitRC2CBC_OID; private static ObjectIdentifier pbeWithSHAAnd3KeyTripleDESCBC_OID; private int counter = 0; private static final int iterationCount = 1024; private static final int SALT_LEN = 20; // private key count // Note: This is a workaround to allow null localKeyID attribute // in pkcs12 with one private key entry and associated cert-chain private int privateKeyCount = 0; // the source of randomness private SecureRandom random; static { try { PKCS8ShroudedKeyBag_OID = new ObjectIdentifier(keyBag); CertBag_OID = new ObjectIdentifier(certBag); PKCS9FriendlyName_OID = new ObjectIdentifier(pkcs9Name); PKCS9LocalKeyId_OID = new ObjectIdentifier(pkcs9KeyId); PKCS9CertType_OID = new ObjectIdentifier(pkcs9certType); pbeWithSHAAnd40BitRC2CBC_OID = new ObjectIdentifier(pbeWithSHAAnd40BitRC2CBC); pbeWithSHAAnd3KeyTripleDESCBC_OID = new ObjectIdentifier(pbeWithSHAAnd3KeyTripleDESCBC); } catch (IOException ioe) { // should not happen } } // Private keys and their supporting certificate chains private static class KeyEntry { Date date; // the creation date of this entry byte[] protectedPrivKey; Certificate chain[]; byte[] keyId; String alias; }; // A certificate with its PKCS #9 attributes private static class CertEntry { final X509Certificate cert; final byte[] keyId; final String alias; CertEntry(X509Certificate cert, byte[] keyId, String alias) { this.cert = cert; this.keyId = keyId; this.alias = alias; } } /** * Private keys and certificates are stored in a hashtable. * Hash entries are keyed by alias names. */ private Hashtable<String, KeyEntry> entries = new Hashtable<String, KeyEntry>(); private ArrayList<KeyEntry> keyList = new ArrayList<KeyEntry>(); private LinkedHashMap<X500Principal, X509Certificate> certsMap = new LinkedHashMap<X500Principal, X509Certificate>(); private ArrayList<CertEntry> certEntries = new ArrayList<CertEntry>(); /** * Returns the key associated with the given alias, using the given * password to recover it. * * @param alias the alias name * @param password the password for recovering the key * * @return the requested key, or null if the given alias does not exist * or does not identify a <i>key entry</i>. * * @exception NoSuchAlgorithmException if the algorithm for recovering the * key cannot be found * @exception UnrecoverableKeyException if the key cannot be recovered * (e.g., the given password is wrong). */ public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { KeyEntry entry = entries.get(alias.toLowerCase()); Key key = null; if (entry == null) { return null; } // get the encoded private key byte[] encrBytes = entry.protectedPrivKey; byte[] encryptedKey; AlgorithmParameters algParams; ObjectIdentifier algOid; try { // get the encrypted private key EncryptedPrivateKeyInfo encrInfo = new EncryptedPrivateKeyInfo(encrBytes); encryptedKey = encrInfo.getEncryptedData(); // parse Algorithm parameters DerValue val = new DerValue(encrInfo.getAlgorithm().encode()); DerInputStream in = val.toDerInputStream(); algOid = in.getOID(); algParams = parseAlgParameters(in); } catch (IOException ioe) { UnrecoverableKeyException uke = new UnrecoverableKeyException("Private key not stored as " + "PKCS#8 EncryptedPrivateKeyInfo: " + ioe); uke.initCause(ioe); throw uke; } try { byte[] privateKeyInfo; while (true) { try { // Use JCE SecretKey skey = getPBEKey(password); Cipher cipher = Cipher.getInstance(algOid.toString()); cipher.init(Cipher.DECRYPT_MODE, skey, algParams); privateKeyInfo = cipher.doFinal(encryptedKey); break; } catch (Exception e) { if (password.length == 0) { // Retry using an empty password // without a NULL terminator. password = new char[1]; continue; } throw e; } } PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(privateKeyInfo); /* * Parse the key algorithm and then use a JCA key factory * to create the private key. */ DerValue val = new DerValue(privateKeyInfo); DerInputStream in = val.toDerInputStream(); int i = in.getInteger(); DerValue[] value = in.getSequence(2); AlgorithmId algId = new AlgorithmId(value[0].getOID()); String algName = algId.getName(); KeyFactory kfac = KeyFactory.getInstance(algName); key = kfac.generatePrivate(kspec); } catch (Exception e) { UnrecoverableKeyException uke = new UnrecoverableKeyException("Get Key failed: " + e.getMessage()); uke.initCause(e); throw uke; } return key; } /** * Returns the certificate chain associated with the given alias. * * @param alias the alias name * * @return the certificate chain (ordered with the user's certificate first * and the root certificate authority last), or null if the given alias * does not exist or does not contain a certificate chain (i.e., the given * alias identifies either a <i>trusted certificate entry</i> or a * <i>key entry</i> without a certificate chain). */ public Certificate[] engineGetCertificateChain(String alias) { KeyEntry entry = entries.get(alias.toLowerCase()); if (entry != null) { if (entry.chain == null) { return null; } else { return entry.chain.clone(); } } else { return null; } } /** * Returns the certificate associated with the given alias. * * <p>If the given alias name identifies a * <i>trusted certificate entry</i>, the certificate associated with that * entry is returned. If the given alias name identifies a * <i>key entry</i>, the first element of the certificate chain of that * entry is returned, or null if that entry does not have a certificate * chain. * * @param alias the alias name * * @return the certificate, or null if the given alias does not exist or * does not contain a certificate. */ public Certificate engineGetCertificate(String alias) { KeyEntry entry = entries.get(alias.toLowerCase()); if (entry != null) { if (entry.chain == null) { return null; } else { return entry.chain[0]; } } else { return null; } } /** * Returns the creation date of the entry identified by the given alias. * * @param alias the alias name * * @return the creation date of this entry, or null if the given alias does * not exist */ public Date engineGetCreationDate(String alias) { KeyEntry entry = entries.get(alias.toLowerCase()); if (entry != null) { return new Date(entry.date.getTime()); } else { return null; } } /** * Assigns the given key to the given alias, protecting it with the given * password. * * <p>If the given key is of type <code>java.security.PrivateKey</code>, * it must be accompanied by a certificate chain certifying the * corresponding public key. * * <p>If the given alias already exists, the keystore information * associated with it is overridden by the given key (and possibly * certificate chain). * * @param alias the alias name * @param key the key to be associated with the alias * @param password the password to protect the key * @param chain the certificate chain for the corresponding public * key (only required if the given key is of type * <code>java.security.PrivateKey</code>). * * @exception KeyStoreException if the given key cannot be protected, or * this operation fails for some other reason */ public synchronized void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException { try { KeyEntry entry = new KeyEntry(); entry.date = new Date(); if (key instanceof PrivateKey) { if ((key.getFormat().equals("PKCS#8")) || (key.getFormat().equals("PKCS8"))) { // Encrypt the private key entry.protectedPrivKey = encryptPrivateKey(key.getEncoded(), password); } else { throw new KeyStoreException("Private key is not encoded" + "as PKCS#8"); } } else { throw new KeyStoreException("Key is not a PrivateKey"); } // clone the chain if (chain != null) { // validate cert-chain if ((chain.length > 1) && (!validateChain(chain))) throw new KeyStoreException("Certificate chain is " + "not validate"); entry.chain = chain.clone(); } // set the keyId to current date entry.keyId = ("Time " + (entry.date).getTime()).getBytes("UTF8"); // set the alias entry.alias = alias.toLowerCase(); // add the entry entries.put(alias.toLowerCase(), entry); } catch (Exception nsae) { KeyStoreException ke = new KeyStoreException("Key protection " + " algorithm not found: " + nsae); ke.initCause(nsae); throw ke; } } /** * Assigns the given key (that has already been protected) to the given * alias. * * <p>If the protected key is of type * <code>java.security.PrivateKey</code>, it must be accompanied by a * certificate chain certifying the corresponding public key. If the * underlying keystore implementation is of type <code>jks</code>, * <code>key</code> must be encoded as an * <code>EncryptedPrivateKeyInfo</code> as defined in the PKCS #8 standard. * * <p>If the given alias already exists, the keystore information * associated with it is overridden by the given key (and possibly * certificate chain). * * @param alias the alias name * @param key the key (in protected format) to be associated with the alias * @param chain the certificate chain for the corresponding public * key (only useful if the protected key is of type * <code>java.security.PrivateKey</code>). * * @exception KeyStoreException if this operation fails. */ public synchronized void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { // key must be encoded as EncryptedPrivateKeyInfo // as defined in PKCS#8 try { new EncryptedPrivateKeyInfo(key); } catch (IOException ioe) { KeyStoreException ke = new KeyStoreException("Private key is not" + " stored as PKCS#8 EncryptedPrivateKeyInfo: " + ioe); ke.initCause(ioe); throw ke; } KeyEntry entry = new KeyEntry(); entry.date = new Date(); try { // set the keyId to current date entry.keyId = ("Time " + (entry.date).getTime()).getBytes("UTF8"); } catch (UnsupportedEncodingException ex) { // Won't happen } // set the alias entry.alias = alias.toLowerCase(); entry.protectedPrivKey = key.clone(); if (chain != null) { entry.chain = chain.clone(); } // add the entry entries.put(alias.toLowerCase(), entry); } /* * Generate random salt */ private byte[] getSalt() { // Generate a random salt. byte[] salt = new byte[SALT_LEN]; if (random == null) { random = new SecureRandom(); } random.nextBytes(salt); return salt; } /* * Generate PBE Algorithm Parameters */ private AlgorithmParameters getAlgorithmParameters(String algorithm) throws IOException { AlgorithmParameters algParams = null; // create PBE parameters from salt and iteration count PBEParameterSpec paramSpec = new PBEParameterSpec(getSalt(), iterationCount); try { algParams = AlgorithmParameters.getInstance(algorithm); algParams.init(paramSpec); } catch (Exception e) { IOException ioe = new IOException("getAlgorithmParameters failed: " + e.getMessage()); ioe.initCause(e); throw ioe; } return algParams; } /* * parse Algorithm Parameters */ private AlgorithmParameters parseAlgParameters(DerInputStream in) throws IOException { AlgorithmParameters algParams = null; try { DerValue params; if (in.available() == 0) { params = null; } else { params = in.getDerValue(); if (params.tag == DerValue.tag_Null) { params = null; } } if (params != null) { algParams = AlgorithmParameters.getInstance("PBE"); algParams.init(params.toByteArray()); } } catch (Exception e) { IOException ioe = new IOException("parseAlgParameters failed: " + e.getMessage()); ioe.initCause(e); throw ioe; } return algParams; } /* * Generate PBE key */ private SecretKey getPBEKey(char[] password) throws IOException { SecretKey skey = null; try { PBEKeySpec keySpec = new PBEKeySpec(password); SecretKeyFactory skFac = SecretKeyFactory.getInstance("PBE"); skey = skFac.generateSecret(keySpec); } catch (Exception e) { IOException ioe = new IOException("getSecretKey failed: " + e.getMessage()); ioe.initCause(e); throw ioe; } return skey; } /* * Encrypt private key using Password-based encryption (PBE) * as defined in PKCS#5. * * NOTE: Currently pbeWithSHAAnd3-KeyTripleDES-CBC algorithmID is * used to derive the key and IV. * * @return encrypted private key encoded as EncryptedPrivateKeyInfo */ private byte[] encryptPrivateKey(byte[] data, char[] password) throws IOException, NoSuchAlgorithmException, UnrecoverableKeyException { byte[] key = null; try { // create AlgorithmParameters AlgorithmParameters algParams = getAlgorithmParameters("PBEWithSHA1AndDESede"); // Use JCE SecretKey skey = getPBEKey(password); Cipher cipher = Cipher.getInstance("PBEWithSHA1AndDESede"); cipher.init(Cipher.ENCRYPT_MODE, skey, algParams); byte[] encryptedKey = cipher.doFinal(data); // wrap encrypted private key in EncryptedPrivateKeyInfo // as defined in PKCS#8 AlgorithmId algid = new AlgorithmId(pbeWithSHAAnd3KeyTripleDESCBC_OID, algParams); EncryptedPrivateKeyInfo encrInfo = new EncryptedPrivateKeyInfo(algid, encryptedKey); key = encrInfo.getEncoded(); } catch (Exception e) { UnrecoverableKeyException uke = new UnrecoverableKeyException("Encrypt Private Key failed: " + e.getMessage()); uke.initCause(e); throw uke; } return key; } /** * Assigns the given certificate to the given alias. * * <p>If the given alias already exists in this keystore and identifies a * <i>trusted certificate entry</i>, the certificate associated with it is * overridden by the given certificate. * * @param alias the alias name * @param cert the certificate * * @exception KeyStoreException if the given alias already exists and does * identify a <i>key entry</i>, or on an attempt to create a * <i>trusted cert entry</i> which is currently not supported. */ public synchronized void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { KeyEntry entry = entries.get(alias.toLowerCase()); if (entry != null) { throw new KeyStoreException("Cannot overwrite own certificate"); } else throw new KeyStoreException("TrustedCertEntry not supported"); } /** * Deletes the entry identified by the given alias from this keystore. * * @param alias the alias name * * @exception KeyStoreException if the entry cannot be removed. */ public synchronized void engineDeleteEntry(String alias) throws KeyStoreException { entries.remove(alias.toLowerCase()); } /** * Lists all the alias names of this keystore. * * @return enumeration of the alias names */ public Enumeration<String> engineAliases() { return entries.keys(); } /** * Checks if the given alias exists in this keystore. * * @param alias the alias name * * @return true if the alias exists, false otherwise */ public boolean engineContainsAlias(String alias) { return entries.containsKey(alias.toLowerCase()); } /** * Retrieves the number of entries in this keystore. * * @return the number of entries in this keystore */ public int engineSize() { return entries.size(); } /** * Returns true if the entry identified by the given alias is a * <i>key entry</i>, and false otherwise. * * @return true if the entry identified by the given alias is a * <i>key entry</i>, false otherwise. */ public boolean engineIsKeyEntry(String alias) { KeyEntry entry = entries.get(alias.toLowerCase()); if (entry != null) { return true; } else { return false; } } /** * Returns true if the entry identified by the given alias is a * <i>trusted certificate entry</i>, and false otherwise. * * @return true if the entry identified by the given alias is a * <i>trusted certificate entry</i>, false otherwise. */ public boolean engineIsCertificateEntry(String alias) { // TrustedCertEntry is not supported return false; } /** * Returns the (alias) name of the first keystore entry whose certificate * matches the given certificate. * * <p>This method attempts to match the given certificate with each * keystore entry. If the entry being considered * is a <i>trusted certificate entry</i>, the given certificate is * compared to that entry's certificate. If the entry being considered is * a <i>key entry</i>, the given certificate is compared to the first * element of that entry's certificate chain (if a chain exists). * * @param cert the certificate to match with. * * @return the (alias) name of the first entry with matching certificate, * or null if no such entry exists in this keystore. */ public String engineGetCertificateAlias(Certificate cert) { Certificate certElem = null; for (Enumeration<String> e = entries.keys(); e.hasMoreElements(); ) { String alias = e.nextElement(); KeyEntry entry = entries.get(alias); if (entry.chain != null) { certElem = entry.chain[0]; } if (certElem.equals(cert)) { return alias; } } return null; } /** * Stores this keystore to the given output stream, and protects its * integrity with the given password. * * @param stream the output stream to which this keystore is written. * @param password the password to generate the keystore integrity check * * @exception IOException if there was an I/O problem with data * @exception NoSuchAlgorithmException if the appropriate data integrity * algorithm could not be found * @exception CertificateException if any of the certificates included in * the keystore data could not be stored */ public synchronized void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { // password is mandatory when storing if (password == null) { throw new IllegalArgumentException("password can't be null"); } // -- Create PFX DerOutputStream pfx = new DerOutputStream(); // PFX version (always write the latest version) DerOutputStream version = new DerOutputStream(); version.putInteger(VERSION_3); byte[] pfxVersion = version.toByteArray(); pfx.write(pfxVersion); // -- Create AuthSafe DerOutputStream authSafe = new DerOutputStream(); // -- Create ContentInfos DerOutputStream authSafeContentInfo = new DerOutputStream(); // -- create safeContent Data ContentInfo byte[] safeContentData = createSafeContent(); ContentInfo dataContentInfo = new ContentInfo(safeContentData); dataContentInfo.encode(authSafeContentInfo); // -- create EncryptedContentInfo byte[] encrData = createEncryptedData(password); ContentInfo encrContentInfo = new ContentInfo(ContentInfo.ENCRYPTED_DATA_OID, new DerValue(encrData)); encrContentInfo.encode(authSafeContentInfo); // wrap as SequenceOf ContentInfos DerOutputStream cInfo = new DerOutputStream(); cInfo.write(DerValue.tag_SequenceOf, authSafeContentInfo); byte[] authenticatedSafe = cInfo.toByteArray(); // Create Encapsulated ContentInfo ContentInfo contentInfo = new ContentInfo(authenticatedSafe); contentInfo.encode(authSafe); byte[] authSafeData = authSafe.toByteArray(); pfx.write(authSafeData); // -- MAC byte[] macData = calculateMac(password, authenticatedSafe); pfx.write(macData); // write PFX to output stream DerOutputStream pfxout = new DerOutputStream(); pfxout.write(DerValue.tag_Sequence, pfx); byte[] pfxData = pfxout.toByteArray(); stream.write(pfxData); stream.flush(); } /* * Generate Hash. */ private byte[] generateHash(byte[] data) throws IOException { byte[] digest = null; try { MessageDigest md = MessageDigest.getInstance("SHA1"); md.update(data); digest = md.digest(); } catch (Exception e) { IOException ioe = new IOException("generateHash failed: " + e); ioe.initCause(e); throw ioe; } return digest; } /* * Calculate MAC using HMAC algorithm (required for password integrity) * * Hash-based MAC algorithm combines secret key with message digest to * create a message authentication code (MAC) */ private byte[] calculateMac(char[] passwd, byte[] data) throws IOException { byte[] mData = null; String algName = "SHA1"; try { // Generate a random salt. byte[] salt = getSalt(); // generate MAC (MAC key is generated within JCE) Mac m = Mac.getInstance("HmacPBESHA1"); PBEParameterSpec params = new PBEParameterSpec(salt, iterationCount); SecretKey key = getPBEKey(passwd); m.init(key, params); m.update(data); byte[] macResult = m.doFinal(); // encode as MacData MacData macData = new MacData(algName, macResult, salt, iterationCount); DerOutputStream bytes = new DerOutputStream(); bytes.write(macData.getEncoded()); mData = bytes.toByteArray(); } catch (Exception e) { IOException ioe = new IOException("calculateMac failed: " + e); ioe.initCause(e); throw ioe; } return mData; } /* * Validate Certificate Chain */ private boolean validateChain(Certificate[] certChain) { for (int i = 0; i < certChain.length-1; i++) { X500Principal issuerDN = ((X509Certificate)certChain[i]).getIssuerX500Principal(); X500Principal subjectDN = ((X509Certificate)certChain[i+1]).getSubjectX500Principal(); if (!(issuerDN.equals(subjectDN))) return false; } return true; } /* * Create PKCS#12 Attributes, friendlyName and localKeyId. * * Although attributes are optional, they could be required. * For e.g. localKeyId attribute is required to match the * private key with the associated end-entity certificate. * * PKCS8ShroudedKeyBags include unique localKeyID and friendlyName. * CertBags may or may not include attributes depending on the type * of Certificate. In end-entity certificates, localKeyID should be * unique, and the corresponding private key should have the same * localKeyID. For trusted CA certs in the cert-chain, localKeyID * attribute is not required, hence most vendors don't include it. * NSS/Netscape require it to be unique or null, where as IE/OpenSSL * ignore it. * * Here is a list of pkcs12 attribute values in CertBags. * * PKCS12 Attribute NSS/Netscape IE OpenSSL J2SE * -------------------------------------------------------------- * LocalKeyId * (In EE cert only, * NULL in CA certs) true true true true * * friendlyName unique same/ same/ unique * unique unique/ * null * * Note: OpenSSL adds friendlyName for end-entity cert only, and * removes the localKeyID and friendlyName for CA certs. * If the CertBag did not have a friendlyName, most vendors will * add it, and assign it to the DN of the cert. */ private byte[] getBagAttributes(String alias, byte[] keyId) throws IOException { byte[] localKeyID = null; byte[] friendlyName = null; // return null if both attributes are null if ((alias == null) && (keyId == null)) { return null; } // SafeBag Attributes DerOutputStream bagAttrs = new DerOutputStream(); // Encode the friendlyname oid. if (alias != null) { DerOutputStream bagAttr1 = new DerOutputStream(); bagAttr1.putOID(PKCS9FriendlyName_OID); DerOutputStream bagAttrContent1 = new DerOutputStream(); DerOutputStream bagAttrValue1 = new DerOutputStream(); bagAttrContent1.putBMPString(alias); bagAttr1.write(DerValue.tag_Set, bagAttrContent1); bagAttrValue1.write(DerValue.tag_Sequence, bagAttr1); friendlyName = bagAttrValue1.toByteArray(); } // Encode the localkeyId oid. if (keyId != null) { DerOutputStream bagAttr2 = new DerOutputStream(); bagAttr2.putOID(PKCS9LocalKeyId_OID); DerOutputStream bagAttrContent2 = new DerOutputStream(); DerOutputStream bagAttrValue2 = new DerOutputStream(); bagAttrContent2.putOctetString(keyId); bagAttr2.write(DerValue.tag_Set, bagAttrContent2); bagAttrValue2.write(DerValue.tag_Sequence, bagAttr2); localKeyID = bagAttrValue2.toByteArray(); } DerOutputStream attrs = new DerOutputStream(); if (friendlyName != null) { attrs.write(friendlyName); } if (localKeyID != null) { attrs.write(localKeyID); } bagAttrs.write(DerValue.tag_Set, attrs); return bagAttrs.toByteArray(); } /* * Create EncryptedData content type, that contains EncryptedContentInfo. * Includes certificates in individual SafeBags of type CertBag. * Each CertBag may include pkcs12 attributes * (see comments in getBagAttributes) */ private byte[] createEncryptedData(char[] password) throws CertificateException, IOException { DerOutputStream out = new DerOutputStream(); for (Enumeration<String> e = entries.keys(); e.hasMoreElements(); ) { String alias = e.nextElement(); KeyEntry entry = entries.get(alias); // certificate chain int chainLen; if (entry.chain == null) { chainLen = 0; } else { chainLen = entry.chain.length; } for (int i = 0; i < chainLen; i++) { // create SafeBag of Type CertBag DerOutputStream safeBag = new DerOutputStream(); safeBag.putOID(CertBag_OID); // create a CertBag DerOutputStream certBag = new DerOutputStream(); certBag.putOID(PKCS9CertType_OID); // write encoded certs in a context-specific tag DerOutputStream certValue = new DerOutputStream(); X509Certificate cert = (X509Certificate)entry.chain[i]; certValue.putOctetString(cert.getEncoded()); certBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), certValue); // wrap CertBag in a Sequence DerOutputStream certout = new DerOutputStream(); certout.write(DerValue.tag_Sequence, certBag); byte[] certBagValue = certout.toByteArray(); // Wrap the CertBag encoding in a context-specific tag. DerOutputStream bagValue = new DerOutputStream(); bagValue.write(certBagValue); // write SafeBag Value safeBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), bagValue); // write SafeBag Attributes // All Certs should have a unique friendlyName. // This change is made to meet NSS requirements. byte[] bagAttrs = null; if (i == 0) { // Only End-Entity Cert should have a localKeyId. bagAttrs = getBagAttributes(entry.alias, entry.keyId); } else { // Trusted root CA certs and Intermediate CA certs do not // need to have a localKeyId, and hence localKeyId is null // This change is made to meet NSS/Netscape requirements. // NSS pkcs12 library requires trusted CA certs in the // certificate chain to have unique or null localKeyID. // However, IE/OpenSSL do not impose this restriction. bagAttrs = getBagAttributes( cert.getSubjectX500Principal().getName(), null); } if (bagAttrs != null) { safeBag.write(bagAttrs); } // wrap as Sequence out.write(DerValue.tag_Sequence, safeBag); } // for cert-chain } // wrap as SequenceOf SafeBag DerOutputStream safeBagValue = new DerOutputStream(); safeBagValue.write(DerValue.tag_SequenceOf, out); byte[] safeBagData = safeBagValue.toByteArray(); // encrypt the content (EncryptedContentInfo) byte[] encrContentInfo = encryptContent(safeBagData, password); // -- SEQUENCE of EncryptedData DerOutputStream encrData = new DerOutputStream(); DerOutputStream encrDataContent = new DerOutputStream(); encrData.putInteger(0); encrData.write(encrContentInfo); encrDataContent.write(DerValue.tag_Sequence, encrData); return encrDataContent.toByteArray(); } /* * Create SafeContent Data content type. * Includes encrypted private key in a SafeBag of type PKCS8ShroudedKeyBag. * Each PKCS8ShroudedKeyBag includes pkcs12 attributes * (see comments in getBagAttributes) */ private byte[] createSafeContent() throws CertificateException, IOException { DerOutputStream out = new DerOutputStream(); for (Enumeration<String> e = entries.keys(); e.hasMoreElements(); ) { String alias = e.nextElement(); KeyEntry entry = entries.get(alias); // Create SafeBag of type pkcs8ShroudedKeyBag DerOutputStream safeBag = new DerOutputStream(); safeBag.putOID(PKCS8ShroudedKeyBag_OID); // get the encrypted private key byte[] encrBytes = entry.protectedPrivKey; EncryptedPrivateKeyInfo encrInfo = null; try { encrInfo = new EncryptedPrivateKeyInfo(encrBytes); } catch (IOException ioe) { throw new IOException("Private key not stored as " + "PKCS#8 EncryptedPrivateKeyInfo" + ioe.getMessage()); } // Wrap the EncryptedPrivateKeyInfo in a context-specific tag. DerOutputStream bagValue = new DerOutputStream(); bagValue.write(encrInfo.getEncoded()); safeBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), bagValue); // write SafeBag Attributes byte[] bagAttrs = getBagAttributes(alias, entry.keyId); safeBag.write(bagAttrs); // wrap as Sequence out.write(DerValue.tag_Sequence, safeBag); } // wrap as Sequence DerOutputStream safeBagValue = new DerOutputStream(); safeBagValue.write(DerValue.tag_Sequence, out); return safeBagValue.toByteArray(); } /* * Encrypt the contents using Password-based (PBE) encryption * as defined in PKCS #5. * * NOTE: Currently pbeWithSHAAnd40BiteRC2-CBC algorithmID is used * to derive the key and IV. * * @return encrypted contents encoded as EncryptedContentInfo */ private byte[] encryptContent(byte[] data, char[] password) throws IOException { byte[] encryptedData = null; // create AlgorithmParameters AlgorithmParameters algParams = getAlgorithmParameters("PBEWithSHA1AndRC2_40"); DerOutputStream bytes = new DerOutputStream(); AlgorithmId algId = new AlgorithmId(pbeWithSHAAnd40BitRC2CBC_OID, algParams); algId.encode(bytes); byte[] encodedAlgId = bytes.toByteArray(); try { // Use JCE SecretKey skey = getPBEKey(password); Cipher cipher = Cipher.getInstance("PBEWithSHA1AndRC2_40"); cipher.init(Cipher.ENCRYPT_MODE, skey, algParams); encryptedData = cipher.doFinal(data); } catch (Exception e) { IOException ioe = new IOException("Failed to encrypt" + " safe contents entry: " + e); ioe.initCause(e); throw ioe; } // create EncryptedContentInfo DerOutputStream bytes2 = new DerOutputStream(); bytes2.putOID(ContentInfo.DATA_OID); bytes2.write(encodedAlgId); // Wrap encrypted data in a context-specific tag. DerOutputStream tmpout2 = new DerOutputStream(); tmpout2.putOctetString(encryptedData); bytes2.writeImplicit(DerValue.createTag(DerValue.TAG_CONTEXT, false, (byte)0), tmpout2); // wrap EncryptedContentInfo in a Sequence DerOutputStream out = new DerOutputStream(); out.write(DerValue.tag_Sequence, bytes2); return out.toByteArray(); } /** * Loads the keystore from the given input stream. * * <p>If a password is given, it is used to check the integrity of the * keystore data. Otherwise, the integrity of the keystore is not checked. * * @param stream the input stream from which the keystore is loaded * @param password the (optional) password used to check the integrity of * the keystore. * * @exception IOException if there is an I/O or format problem with the * keystore data * @exception NoSuchAlgorithmException if the algorithm used to check * the integrity of the keystore cannot be found * @exception CertificateException if any of the certificates in the * keystore could not be loaded */ public synchronized void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { DataInputStream dis; CertificateFactory cf = null; ByteArrayInputStream bais = null; byte[] encoded = null; if (stream == null) return; // reset the counter counter = 0; DerValue val = new DerValue(stream); DerInputStream s = val.toDerInputStream(); int version = s.getInteger(); if (version != VERSION_3) { throw new IOException("PKCS12 keystore not in version 3 format"); } entries.clear(); /* * Read the authSafe. */ byte[] authSafeData; ContentInfo authSafe = new ContentInfo(s); ObjectIdentifier contentType = authSafe.getContentType(); if (contentType.equals(ContentInfo.DATA_OID)) { authSafeData = authSafe.getData(); } else /* signed data */ { throw new IOException("public key protected PKCS12 not supported"); } DerInputStream as = new DerInputStream(authSafeData); DerValue[] safeContentsArray = as.getSequence(2); int count = safeContentsArray.length; // reset the count at the start privateKeyCount = 0; /* * Spin over the ContentInfos. */ for (int i = 0; i < count; i++) { byte[] safeContentsData; ContentInfo safeContents; DerInputStream sci; byte[] eAlgId = null; sci = new DerInputStream(safeContentsArray[i].toByteArray()); safeContents = new ContentInfo(sci); contentType = safeContents.getContentType(); safeContentsData = null; if (contentType.equals(ContentInfo.DATA_OID)) { safeContentsData = safeContents.getData(); } else if (contentType.equals(ContentInfo.ENCRYPTED_DATA_OID)) { if (password == null) { continue; } DerInputStream edi = safeContents.getContent().toDerInputStream(); int edVersion = edi.getInteger(); DerValue[] seq = edi.getSequence(2); ObjectIdentifier edContentType = seq[0].getOID(); eAlgId = seq[1].toByteArray(); if (!seq[2].isContextSpecific((byte)0)) { throw new IOException("encrypted content not present!"); } byte newTag = DerValue.tag_OctetString; if (seq[2].isConstructed()) newTag |= 0x20; seq[2].resetTag(newTag); safeContentsData = seq[2].getOctetString(); // parse Algorithm parameters DerInputStream in = seq[1].toDerInputStream(); ObjectIdentifier algOid = in.getOID(); AlgorithmParameters algParams = parseAlgParameters(in); while (true) { try { // Use JCE SecretKey skey = getPBEKey(password); Cipher cipher = Cipher.getInstance(algOid.toString()); cipher.init(Cipher.DECRYPT_MODE, skey, algParams); safeContentsData = cipher.doFinal(safeContentsData); break; } catch (Exception e) { if (password.length == 0) { // Retry using an empty password // without a NULL terminator. password = new char[1]; continue; } throw new IOException( "failed to decrypt safe contents entry: " + e, e); } } } else { throw new IOException("public key protected PKCS12" + " not supported"); } DerInputStream sc = new DerInputStream(safeContentsData); loadSafeContents(sc, password); } // The MacData is optional. if (password != null && s.available() > 0) { MacData macData = new MacData(s); try { String algName = macData.getDigestAlgName().toUpperCase(); if (algName.equals("SHA") || algName.equals("SHA1") || algName.equals("SHA-1")) { algName = "SHA1"; } // generate MAC (MAC key is created within JCE) Mac m = Mac.getInstance("HmacPBE" + algName); PBEParameterSpec params = new PBEParameterSpec(macData.getSalt(), macData.getIterations()); SecretKey key = getPBEKey(password); m.init(key, params); m.update(authSafeData); byte[] macResult = m.doFinal(); if (!Arrays.equals(macData.getDigest(), macResult)) { throw new SecurityException("Failed PKCS12" + " integrity checking"); } } catch (Exception e) { IOException ioe = new IOException("Integrity check failed: " + e); ioe.initCause(e); throw ioe; } } /* * Match up private keys with certificate chains. */ KeyEntry[] list = keyList.toArray(new KeyEntry[keyList.size()]); for (int m = 0; m < list.length; m++) { KeyEntry entry = list[m]; if (entry.keyId != null) { ArrayList<X509Certificate> chain = new ArrayList<X509Certificate>(); X509Certificate cert = findMatchedCertificate(entry); while (cert != null) { chain.add(cert); X500Principal issuerDN = cert.getIssuerX500Principal(); if (issuerDN.equals(cert.getSubjectX500Principal())) { break; } cert = certsMap.get(issuerDN); } /* Update existing KeyEntry in entries table */ if (chain.size() > 0) entry.chain = chain.toArray(new Certificate[chain.size()]); } } certEntries.clear(); certsMap.clear(); keyList.clear(); } /** * Locates a matched CertEntry from certEntries, and returns its cert. * @param entry the KeyEntry to match * @return a certificate, null if not found */ private X509Certificate findMatchedCertificate(KeyEntry entry) { CertEntry keyIdMatch = null; CertEntry aliasMatch = null; for (CertEntry ce: certEntries) { if (Arrays.equals(entry.keyId, ce.keyId)) { keyIdMatch = ce; if (entry.alias.equalsIgnoreCase(ce.alias)) { // Full match! return ce.cert; } } else if (entry.alias.equalsIgnoreCase(ce.alias)) { aliasMatch = ce; } } // keyId match first, for compatibility if (keyIdMatch != null) return keyIdMatch.cert; else if (aliasMatch != null) return aliasMatch.cert; else return null; } private void loadSafeContents(DerInputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { DerValue[] safeBags = stream.getSequence(2); int count = safeBags.length; /* * Spin over the SafeBags. */ for (int i = 0; i < count; i++) { ObjectIdentifier bagId; DerInputStream sbi; DerValue bagValue; Object bagItem = null; sbi = safeBags[i].toDerInputStream(); bagId = sbi.getOID(); bagValue = sbi.getDerValue(); if (!bagValue.isContextSpecific((byte)0)) { throw new IOException("unsupported PKCS12 bag value type " + bagValue.tag); } bagValue = bagValue.data.getDerValue(); if (bagId.equals(PKCS8ShroudedKeyBag_OID)) { KeyEntry kEntry = new KeyEntry(); kEntry.protectedPrivKey = bagValue.toByteArray(); bagItem = kEntry; privateKeyCount++; } else if (bagId.equals(CertBag_OID)) { DerInputStream cs = new DerInputStream(bagValue.toByteArray()); DerValue[] certValues = cs.getSequence(2); ObjectIdentifier certId = certValues[0].getOID(); if (!certValues[1].isContextSpecific((byte)0)) { throw new IOException("unsupported PKCS12 cert value type " + certValues[1].tag); } DerValue certValue = certValues[1].data.getDerValue(); CertificateFactory cf = CertificateFactory.getInstance("X509"); X509Certificate cert; cert = (X509Certificate)cf.generateCertificate (new ByteArrayInputStream(certValue.getOctetString())); bagItem = cert; } else { // log error message for "unsupported PKCS12 bag type" } DerValue[] attrSet; try { attrSet = sbi.getSet(2); } catch (IOException e) { // entry does not have attributes // Note: CA certs can have no attributes // OpenSSL generates pkcs12 with no attr for CA certs. attrSet = null; } String alias = null; byte[] keyId = null; if (attrSet != null) { for (int j = 0; j < attrSet.length; j++) { DerInputStream as = new DerInputStream(attrSet[j].toByteArray()); DerValue[] attrSeq = as.getSequence(2); ObjectIdentifier attrId = attrSeq[0].getOID(); DerInputStream vs = new DerInputStream(attrSeq[1].toByteArray()); DerValue[] valSet; try { valSet = vs.getSet(1); } catch (IOException e) { throw new IOException("Attribute " + attrId + " should have a value " + e.getMessage()); } if (attrId.equals(PKCS9FriendlyName_OID)) { alias = valSet[0].getBMPString(); } else if (attrId.equals(PKCS9LocalKeyId_OID)) { keyId = valSet[0].getOctetString(); } else { // log error message for "unknown attr" } } } /* * As per PKCS12 v1.0 friendlyname (alias) and localKeyId (keyId) * are optional PKCS12 bagAttributes. But entries in the keyStore * are identified by their alias. Hence we need to have an * Unfriendlyname in the alias, if alias is null. The keyId * attribute is required to match the private key with the * certificate. If we get a bagItem of type KeyEntry with a * null keyId, we should skip it entirely. */ if (bagItem instanceof KeyEntry) { KeyEntry entry = (KeyEntry)bagItem; if (keyId == null) { // Insert a localKeyID for the privateKey // Note: This is a workaround to allow null localKeyID // attribute in pkcs12 with one private key entry and // associated cert-chain if (privateKeyCount == 1) { keyId = "01".getBytes("UTF8"); } else { continue; } } entry.keyId = keyId; // restore date if it exists String keyIdStr = new String(keyId, "UTF8"); Date date = null; if (keyIdStr.startsWith("Time ")) { try { date = new Date( Long.parseLong(keyIdStr.substring(5))); } catch (Exception e) { date = null; } } if (date == null) { date = new Date(); } entry.date = date; keyList.add(entry); if (alias == null) alias = getUnfriendlyName(); entry.alias = alias; entries.put(alias.toLowerCase(), entry); } else if (bagItem instanceof X509Certificate) { X509Certificate cert = (X509Certificate)bagItem; // Insert a localKeyID for the corresponding cert // Note: This is a workaround to allow null localKeyID // attribute in pkcs12 with one private key entry and // associated cert-chain if ((keyId == null) && (privateKeyCount == 1)) { // insert localKeyID only for EE cert or self-signed cert if (i == 0) { keyId = "01".getBytes("UTF8"); } } certEntries.add(new CertEntry(cert, keyId, alias)); X500Principal subjectDN = cert.getSubjectX500Principal(); if (subjectDN != null) { if (!certsMap.containsKey(subjectDN)) { certsMap.put(subjectDN, cert); } } } } } private String getUnfriendlyName() { counter++; return (String.valueOf(counter)); } }