8006591: Protect keystore entries using stronger PBE algorithms
authorvinnie
Wed, 23 Jan 2013 21:25:49 +0000
changeset 15297 eb3d7b36b4c4
parent 15296 e652ed68b1c4
child 15298 06867bfe82ac
8006591: Protect keystore entries using stronger PBE algorithms Reviewed-by: mullan
jdk/src/share/classes/java/security/KeyStore.java
jdk/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java
jdk/test/java/security/KeyStore/PBETest.java
--- a/jdk/src/share/classes/java/security/KeyStore.java	Wed Jan 23 10:31:10 2013 -0800
+++ b/jdk/src/share/classes/java/security/KeyStore.java	Wed Jan 23 21:25:49 2013 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, 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
@@ -29,6 +29,7 @@
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 import java.security.cert.CertificateException;
+import java.security.spec.AlgorithmParameterSpec;
 import java.util.*;
 import javax.crypto.SecretKey;
 
@@ -239,6 +240,8 @@
                 ProtectionParameter, javax.security.auth.Destroyable {
 
         private final char[] password;
+        private final String protectionAlgorithm;
+        private final AlgorithmParameterSpec protectionParameters;
         private volatile boolean destroyed = false;
 
         /**
@@ -251,6 +254,72 @@
          */
         public PasswordProtection(char[] password) {
             this.password = (password == null) ? null : password.clone();
+            this.protectionAlgorithm = null;
+            this.protectionParameters = null;
+        }
+
+        /**
+         * Creates a password parameter and specifies the protection algorithm
+         * and associated parameters to use when encrypting a keystore entry.
+         * <p>
+         * The specified {@code password} is cloned before it is stored in the
+         * new {@code PasswordProtection} object.
+         *
+         * @param password the password, which may be {@code null}
+         * @param protectionAlgorithm the encryption algorithm name, for
+         *     example, {@code PBEWithHmacSHA256AndAES_256}.
+         *     See the Cipher section in the <a href=
+         * "{@docRoot}/../technotes/guides/security/StandardNames.html#Cipher">
+         * Java Cryptography Architecture Standard Algorithm Name
+         * Documentation</a>
+         *     for information about standard encryption algorithm names.
+         * @param protectionParameters the encryption algorithm parameter
+         *     specification, which may be {@code null}
+         * @exception NullPointerException if {@code protectionAlgorithm} is
+         *     {@code null}
+         *
+         * @since 1.8
+         */
+        public PasswordProtection(char[] password, String protectionAlgorithm,
+            AlgorithmParameterSpec protectionParameters) {
+            if (protectionAlgorithm == null) {
+                throw new NullPointerException("invalid null input");
+            }
+            this.password = (password == null) ? null : password.clone();
+            this.protectionAlgorithm = protectionAlgorithm;
+            this.protectionParameters = protectionParameters;
+        }
+
+        /**
+         * Gets the name of the protection algorithm.
+         * If none was set then the keystore provider will use its default
+         * protection algorithm. The name of the default protection algorithm
+         * for a given keystore type is set using the
+         * {@code 'keystore.<type>.keyProtectionAlgorithm'} security property.
+         * For example, the
+         * {@code keystore.PKCS12.keyProtectionAlgorithm} property stores the
+         * name of the default key protection algorithm used for PKCS12
+         * keystores. If the security property is not set, an
+         * implementation-specific algorithm will be used.
+         *
+         * @return the algorithm name, or {@code null} if none was set
+         *
+         * @since 1.8
+         */
+        public String getProtectionAlgorithm() {
+            return protectionAlgorithm;
+        }
+
+        /**
+         * Gets the parameters supplied for the protection algorithm.
+         *
+         * @return the algorithm parameter specification, or {@code  null},
+         *     if none was set
+         *
+         * @since 1.8
+         */
+        public AlgorithmParameterSpec getProtectionParameters() {
+            return protectionParameters;
         }
 
         /**
--- a/jdk/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java	Wed Jan 23 10:31:10 2013 -0800
+++ b/jdk/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java	Wed Jan 23 21:25:49 2013 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2013, 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
@@ -26,19 +26,25 @@
 package sun.security.pkcs12;
 
 import java.io.*;
+import java.security.AccessController;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.Key;
+import java.security.KeyStore;
 import java.security.KeyFactory;
 import java.security.PrivateKey;
+import java.security.PrivilegedAction;
 import java.security.KeyStoreSpi;
 import java.security.KeyStoreException;
+import java.security.UnrecoverableEntryException;
 import java.security.UnrecoverableKeyException;
+import java.security.Security;
 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.AlgorithmParameterSpec;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.*;
 
@@ -49,8 +55,10 @@
 import javax.crypto.SecretKey;
 import javax.crypto.Cipher;
 import javax.crypto.Mac;
+import javax.security.auth.DestroyFailedException;
 import javax.security.auth.x500.X500Principal;
 
+import sun.security.util.Debug;
 import sun.security.util.DerInputStream;
 import sun.security.util.DerOutputStream;
 import sun.security.util.DerValue;
@@ -119,6 +127,13 @@
 
     public static final int VERSION_3 = 3;
 
+    private static final String[] KEY_PROTECTION_ALGORITHM = {
+        "keystore.pkcs12.keyProtectionAlgorithm",
+        "keystore.PKCS12.keyProtectionAlgorithm"
+    };
+
+    private static final Debug debug = Debug.getInstance("pkcs12");
+
     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};
 
@@ -131,6 +146,7 @@
                                         {1, 2, 840, 113549, 1, 12, 1, 6};
     private static final int pbeWithSHAAnd3KeyTripleDESCBC[] =
                                         {1, 2, 840, 113549, 1, 12, 1, 3};
+    private static final int pbes2[] = {1, 2, 840, 113549, 1, 5, 13};
 
     private static ObjectIdentifier PKCS8ShroudedKeyBag_OID;
     private static ObjectIdentifier CertBag_OID;
@@ -139,6 +155,7 @@
     private static ObjectIdentifier PKCS9CertType_OID;
     private static ObjectIdentifier pbeWithSHAAnd40BitRC2CBC_OID;
     private static ObjectIdentifier pbeWithSHAAnd3KeyTripleDESCBC_OID;
+    private static ObjectIdentifier pbes2_OID;
 
     private int counter = 0;
     private static final int iterationCount = 1024;
@@ -163,6 +180,7 @@
                         new ObjectIdentifier(pbeWithSHAAnd40BitRC2CBC);
             pbeWithSHAAnd3KeyTripleDESCBC_OID =
                         new ObjectIdentifier(pbeWithSHAAnd3KeyTripleDESCBC);
+            pbes2_OID = new ObjectIdentifier(pbes2);
         } catch (IOException ioe) {
             // should not happen
         }
@@ -288,6 +306,12 @@
 
             KeyFactory kfac = KeyFactory.getInstance(algName);
             key =  kfac.generatePrivate(kspec);
+
+            if (debug != null) {
+                debug.println("Retrieved a protected private key at alias '" +
+                    alias + "'");
+            }
+
         } catch (Exception e) {
             UnrecoverableKeyException uke =
                 new UnrecoverableKeyException("Get Key failed: " +
@@ -315,6 +339,13 @@
             if (entry.chain == null) {
                 return null;
             } else {
+
+                if (debug != null) {
+                    debug.println("Retrieved a " +
+                        entry.chain.length +
+                        "-certificate chain at alias '" + alias + "'");
+                }
+
                 return entry.chain.clone();
             }
         } else {
@@ -343,6 +374,12 @@
             if (entry.chain == null) {
                 return null;
             } else {
+
+                if (debug != null) {
+                    debug.println("Retrieved a certificate at alias '" + alias +
+                        "'");
+                }
+
                 return entry.chain[0];
             }
         } else {
@@ -393,6 +430,28 @@
                         char[] password, Certificate[] chain)
         throws KeyStoreException
     {
+        KeyStore.PasswordProtection passwordProtection =
+            new KeyStore.PasswordProtection(password);
+
+        try {
+            setKeyEntry(alias, key, passwordProtection, chain);
+
+        } finally {
+            try {
+                passwordProtection.destroy();
+            } catch (DestroyFailedException dfe) {
+                // ignore
+            }
+        }
+    }
+
+    /*
+     * Sets a key entry
+     */
+    private void setKeyEntry(String alias, Key key,
+        KeyStore.PasswordProtection passwordProtection, Certificate[] chain)
+            throws KeyStoreException
+    {
         try {
             KeyEntry entry = new KeyEntry();
             entry.date = new Date();
@@ -401,8 +460,14 @@
                 if ((key.getFormat().equals("PKCS#8")) ||
                     (key.getFormat().equals("PKCS8"))) {
                     // Encrypt the private key
+
+                    if (debug != null) {
+                        debug.println("Setting a protected private key at " +
+                            "alias '" + alias + "'");
+                    }
+
                     entry.protectedPrivKey =
-                        encryptPrivateKey(key.getEncoded(), password);
+                        encryptPrivateKey(key.getEncoded(), passwordProtection);
                 } else {
                     throw new KeyStoreException("Private key is not encoded" +
                                 "as PKCS#8");
@@ -418,6 +483,11 @@
                    throw new KeyStoreException("Certificate chain is " +
                                                 "not validate");
                 entry.chain = chain.clone();
+
+                if (debug != null) {
+                    debug.println("Setting a " + chain.length +
+                        "-certificate chain at alias '" + alias + "'");
+                }
             }
 
             // set the keyId to current date
@@ -472,6 +542,10 @@
         KeyEntry entry = new KeyEntry();
         entry.date = new Date();
 
+        if (debug != null) {
+            debug.println("Setting a protected key at alias '" + alias + "'");
+        }
+
         try {
             // set the keyId to current date
             entry.keyId = ("Time " + (entry.date).getTime()).getBytes("UTF8");
@@ -484,6 +558,11 @@
         entry.protectedPrivKey = key.clone();
         if (chain != null) {
            entry.chain = chain.clone();
+
+            if (debug != null) {
+                debug.println("Setting a " + entry.chain.length +
+                    "-certificate chain at alias '" + alias + "'");
+            }
         }
 
         // add the entry
@@ -576,31 +655,76 @@
      * Encrypt private key using Password-based encryption (PBE)
      * as defined in PKCS#5.
      *
-     * NOTE: Currently pbeWithSHAAnd3-KeyTripleDES-CBC algorithmID is
+     * NOTE: By default, 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)
+    private byte[] encryptPrivateKey(byte[] data,
+        KeyStore.PasswordProtection passwordProtection)
         throws IOException, NoSuchAlgorithmException, UnrecoverableKeyException
     {
         byte[] key = null;
 
         try {
-            // create AlgorithmParameters
-            AlgorithmParameters algParams =
-                getAlgorithmParameters("PBEWithSHA1AndDESede");
+            String algorithm;
+            AlgorithmParameters algParams;
+            AlgorithmId algid;
+
+            // Initialize PBE algorithm and parameters
+            algorithm = passwordProtection.getProtectionAlgorithm();
+            if (algorithm != null) {
+                AlgorithmParameterSpec algParamSpec =
+                    passwordProtection.getProtectionParameters();
+                if (algParamSpec != null) {
+                    algParams = AlgorithmParameters.getInstance(algorithm);
+                    algParams.init(algParamSpec);
+                } else {
+                    algParams = getAlgorithmParameters(algorithm);
+                }
+                ObjectIdentifier pbeOID = mapPBEAlgorithmToOID(algorithm);
+                if (pbeOID != null) {
+                    algid = new AlgorithmId(pbeOID, algParams);
+                } else {
+                    throw new IOException("PBE algorithm '" + algorithm +
+                        " 'is not supported for key entry protection");
+                }
+            } else {
+                // Check default key protection algorithm for PKCS12 keystores
+                algorithm = AccessController.doPrivileged(
+                    new PrivilegedAction<String>() {
+                        public String run() {
+                            String prop =
+                                Security.getProperty(
+                                    KEY_PROTECTION_ALGORITHM[0]);
+                            if (prop == null) {
+                                prop = Security.getProperty(
+                                    KEY_PROTECTION_ALGORITHM[1]);
+                            }
+                            return prop;
+                        }
+                    });
+                if (algorithm == null) {
+                    algorithm = "PBEWithSHA1AndDESede";
+                }
+                algParams = getAlgorithmParameters(algorithm);
+                algid = new AlgorithmId(pbeWithSHAAnd3KeyTripleDESCBC_OID,
+                    algParams);
+            }
 
             // Use JCE
-            SecretKey skey = getPBEKey(password);
-            Cipher cipher = Cipher.getInstance("PBEWithSHA1AndDESede");
+            SecretKey skey = getPBEKey(passwordProtection.getPassword());
+            Cipher cipher = Cipher.getInstance(algorithm);
             cipher.init(Cipher.ENCRYPT_MODE, skey, algParams);
             byte[] encryptedKey = cipher.doFinal(data);
 
+            if (debug != null) {
+                debug.println("  (Cipher algorithm: " + cipher.getAlgorithm() +
+                    ")");
+            }
+
             // 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();
@@ -615,6 +739,18 @@
         return key;
     }
 
+    /*
+     * Map a PBE algorithm name onto its object identifier
+     */
+    private ObjectIdentifier mapPBEAlgorithmToOID(String algorithm) {
+        // Check for PBES2 algorithms
+        if (algorithm.toLowerCase().startsWith("pbewithhmacsha")) {
+            return pbes2_OID;
+        }
+
+        return null;
+    }
+
     /**
      * Assigns the given certificate to the given alias.
      *
@@ -649,6 +785,10 @@
     public synchronized void engineDeleteEntry(String alias)
         throws KeyStoreException
     {
+        if (debug != null) {
+            debug.println("Removing entry at alias '" + alias + "'");
+        }
+
         entries.remove(alias.toLowerCase(Locale.ENGLISH));
     }
 
@@ -778,11 +918,21 @@
         DerOutputStream authSafeContentInfo = new DerOutputStream();
 
         // -- create safeContent Data ContentInfo
+        if (debug != null) {
+            debug.println("Storing " + privateKeyCount +
+                " protected key(s) in a PKCS#7 data content-type");
+        }
+
         byte[] safeContentData = createSafeContent();
         ContentInfo dataContentInfo = new ContentInfo(safeContentData);
         dataContentInfo.encode(authSafeContentInfo);
 
         // -- create EncryptedContentInfo
+        if (debug != null) {
+            debug.println("Storing certificate(s) in a PKCS#7 encryptedData " +
+                "content-type");
+        }
+
         byte[] encrData = createEncryptedData(password);
         ContentInfo encrContentInfo =
                 new ContentInfo(ContentInfo.ENCRYPTED_DATA_OID,
@@ -812,7 +962,6 @@
         stream.flush();
     }
 
-
     /*
      * Generate Hash.
      */
@@ -1143,6 +1292,11 @@
             cipher.init(Cipher.ENCRYPT_MODE, skey, algParams);
             encryptedData = cipher.doFinal(data);
 
+            if (debug != null) {
+                debug.println("  (Cipher algorithm: " + cipher.getAlgorithm() +
+                    ")");
+            }
+
         } catch (Exception e) {
             throw new IOException("Failed to encrypt" +
                     " safe contents entry: " + e, e);
@@ -1240,11 +1394,21 @@
             contentType = safeContents.getContentType();
             safeContentsData = null;
             if (contentType.equals((Object)ContentInfo.DATA_OID)) {
+
+                if (debug != null) {
+                    debug.println("Loading PKCS#7 data content-type");
+                }
+
                 safeContentsData = safeContents.getData();
             } else if (contentType.equals(ContentInfo.ENCRYPTED_DATA_OID)) {
                 if (password == null) {
                    continue;
                 }
+
+                if (debug != null) {
+                    debug.println("Loading PKCS#7 encryptedData content-type");
+                }
+
                 DerInputStream edi =
                                 safeContents.getContent().toDerInputStream();
                 int edVersion = edi.getInteger();
@@ -1312,6 +1476,11 @@
                 m.update(authSafeData);
                 byte[] macResult = m.doFinal();
 
+                if (debug != null) {
+                    debug.println("Checking keystore integrity " +
+                        "(MAC algorithm: " + m.getAlgorithm() + ")");
+                }
+
                 if (!Arrays.equals(macData.getDigest(), macResult)) {
                    throw new SecurityException("Failed PKCS12" +
                                         " integrity checking");
@@ -1417,7 +1586,10 @@
                         (new ByteArrayInputStream(certValue.getOctetString()));
                 bagItem = cert;
             } else {
-                // log error message for "unsupported PKCS12 bag type"
+
+                if (debug != null) {
+                    debug.println("Unsupported PKCS12 bag type: " + bagId);
+                }
             }
 
             DerValue[] attrSet;
@@ -1453,7 +1625,11 @@
                     } else if (attrId.equals((Object)PKCS9LocalKeyId_OID)) {
                         keyId = valSet[0].getOctetString();
                     } else {
-                        // log error message for "unknown attr"
+
+                        if (debug != null) {
+                            debug.println("Unsupported PKCS12 bag attribute: " +
+                                attrId);
+                        }
                     }
                 }
             }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/security/KeyStore/PBETest.java	Wed Jan 23 21:25:49 2013 +0000
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2013, 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.
+ *
+ * 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.
+ */
+
+/*
+ * @test
+ * @bug 8006591
+ * @summary Protect keystore entries using stronger PBE algorithms
+ */
+
+import java.io.*;
+import java.security.*;
+import javax.crypto.spec.*;
+
+// Retrieve a keystore entry, protected by the default encryption algorithm.
+// Set the keystore entry, protected by a stronger encryption algorithm.
+
+public class PBETest {
+    private final static String DIR = System.getProperty("test.src", ".");
+    private static final String PBE_ALGO = "PBEWithHmacSHA1AndAES_128";
+    private static final char[] PASSWORD = "passphrase".toCharArray();
+    private static final String KEYSTORE_TYPE = "JKS";
+    private static final String KEYSTORE = DIR + "/keystore.jks";
+    private static final String NEW_KEYSTORE_TYPE = "PKCS12";
+    private static final String NEW_KEYSTORE = PBE_ALGO + ".p12";
+    private static final String ALIAS = "vajra";
+
+    private static final byte[] IV = {
+        0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,
+        0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20
+    };
+    private static final byte[] SALT = {
+        0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08
+    };
+    private static final int ITERATION_COUNT = 1024;
+
+    public static void main(String[] args) throws Exception {
+
+        new File(NEW_KEYSTORE).delete();
+
+        try {
+            KeyStore keystore = load(KEYSTORE_TYPE, KEYSTORE, PASSWORD);
+            KeyStore.Entry entry =
+                keystore.getEntry(ALIAS,
+                    new KeyStore.PasswordProtection(PASSWORD));
+            System.out.println("Retrieved entry named '" + ALIAS + "'");
+
+            // Set entry
+            KeyStore keystore2 = load(NEW_KEYSTORE_TYPE, null, null);
+            keystore2.setEntry(ALIAS, entry,
+                new KeyStore.PasswordProtection(PASSWORD, PBE_ALGO,
+                    new PBEParameterSpec(SALT, ITERATION_COUNT,
+                        new IvParameterSpec(IV))));
+            System.out.println("Encrypted entry using: " + PBE_ALGO);
+
+            System.out.println("Storing keystore to: " + NEW_KEYSTORE);
+            keystore2.store(new FileOutputStream(NEW_KEYSTORE), PASSWORD);
+
+            keystore2 = load(NEW_KEYSTORE_TYPE, NEW_KEYSTORE, PASSWORD);
+            entry = keystore2.getEntry(ALIAS,
+                new KeyStore.PasswordProtection(PASSWORD));
+            System.out.println("Retrieved entry named '" + ALIAS + "'");
+
+        } finally {
+            new File(NEW_KEYSTORE).delete();
+            System.out.println("Deleted keystore: " + NEW_KEYSTORE);
+        }
+    }
+
+    private static KeyStore load(String type, String path, char[] password)
+        throws Exception {
+
+        FileInputStream stream = null;
+        if (path != null) {
+            stream = new FileInputStream(path);
+        }
+        KeyStore keystore = KeyStore.getInstance(type);
+        System.out.println("Loading keystore from: " + path);
+        keystore.load(stream, password);
+
+        return keystore;
+    }
+}