Przeglądaj źródła

SEC-1659: favor AES encryption instead of DES as standard symmetric encryption algorithm

Keith Donald 14 lat temu
rodzic
commit
ea76efdb2c

+ 29 - 28
crypto/src/main/java/org/springframework/security/crypto/encrypt/PasswordBasedBytesEncryptor.java → crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java

@@ -24,61 +24,62 @@ import static org.springframework.security.crypto.util.EncodingUtils.subArray;
 
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
 
 import org.springframework.security.crypto.keygen.BytesKeyGenerator;
-import org.springframework.security.crypto.keygen.KeyGenerators;
+import org.springframework.security.crypto.util.EncodingUtils;
 
 /**
- * A general purpose encryptor for password-based encryption (PBEwith{prf}and{encryption} algorithms).
- * Prepends a random salt to each encrypted value to aid in the prevention of password compromise with the aid of a dictionary/rainbow table.
- * The salt allows the same secret key to be used for multiple encryption operations.
- * The password should be not be shared.
- * Note: {prf} = Pseudo random function e.g. MD5; {encryption} = Encryption method e.g. DES or AES.
+ * Encryptor that uses 256-bit AES encryption.
  * @author Keith Donald
  */
-final class PasswordBasedBytesEncryptor implements BytesEncryptor {
+final class AesBytesEncryptor implements BytesEncryptor {
 
     private final SecretKey secretKey;
 
-    private final BytesKeyGenerator saltGenerator;
-
     private final Cipher encryptor;
 
     private final Cipher decryptor;
 
-    public PasswordBasedBytesEncryptor(String algorithm, String password) {
-        secretKey = newSecretKey(algorithm, password);
-        saltGenerator = KeyGenerators.secureRandom();
-        encryptor = newCipher(algorithm);
-        decryptor = newCipher(algorithm);
+    private final BytesKeyGenerator ivGenerator;
+
+    public AesBytesEncryptor(String password, String salt, BytesKeyGenerator ivGenerator) {
+        PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), EncodingUtils.hexDecode(salt), 1024, 256);
+        SecretKey secretKey = newSecretKey("PBKDF2WithHmacSHA1", keySpec);
+        this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
+        encryptor = newCipher(AES_ALGORITHM);
+        decryptor = newCipher(AES_ALGORITHM);
+        this.ivGenerator = ivGenerator;
     }
 
     public byte[] encrypt(byte[] bytes) {
-        byte[] salt = saltGenerator.generateKey();
-        byte[] encrypted;
         synchronized (encryptor) {
-            initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, salt, 1024);
-            encrypted = doFinal(encryptor, bytes);
+            byte[] iv = ivGenerator.generateKey();
+            initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+            byte[] encrypted = doFinal(encryptor, bytes);
+            return concatenate(iv, encrypted);
         }
-        return concatenate(salt, encrypted);
     }
 
     public byte[] decrypt(byte[] encryptedBytes) {
-        byte[] salt = saltPart(encryptedBytes);
-        byte[] decrypted;
         synchronized (decryptor) {
-            initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, salt, 1024);
-            decrypted = doFinal(decryptor, cipherPart(encryptedBytes, salt));
+            byte[] iv = ivPart(encryptedBytes);
+            initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+            return doFinal(decryptor, cipherPart(encryptedBytes, iv));
         }
-        return decrypted;
     }
 
-    private byte[] saltPart(byte[] encrypted) {
-        return subArray(encrypted, 0, saltGenerator.getKeyLength());
+    // internal helpers
+
+    private byte[] ivPart(byte[] encrypted) {
+        return subArray(encrypted, 0, ivGenerator.getKeyLength());
     }
 
-    private byte[] cipherPart(byte[] encrypted, byte[] salt) {
-        return subArray(encrypted, salt.length, encrypted.length);
+    private byte[] cipherPart(byte[] encrypted, byte[] iv) {
+        return subArray(encrypted, iv.length, encrypted.length);
     }
 
+    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
 }

+ 19 - 18
crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java

@@ -15,6 +15,8 @@
  */
 package org.springframework.security.crypto.encrypt;
 
+import org.springframework.security.crypto.keygen.KeyGenerators;
+
 /**
  * Factory for commonly used encryptors.
  * Defines the public API for constructing {@link BytesEncryptor} and {@link TextEncryptor} implementations.
@@ -23,14 +25,17 @@ package org.springframework.security.crypto.encrypt;
 public class Encryptors {
 
     /**
-     * Creates a standard password-based bytes encryptor.
-     * Uses MD5 PRF hashing with 1024 iterations and DES-based encryption.
-     * Salts each encrypted value to ensure it will be unique.
-     * TODO - switch standard algorithm from DES to AES.  Switch hashing to SHA-1 from MD5.
+     * Creates a standard password-based bytes encryptor using 256 bit AES encryption.
+     * Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
+     * Salts the password to prevent dictionary attacks against the key.
+     * The provided salt is expected to be hex-encoded; it should be random and at least 8 bytes in length.
+     * Also applies a random 16 byte initialization vector to ensure each encrypted message will be unique.
+     * Requires Java 6.
      * @param password the password used to generate the encryptor's secret key; should not be shared
+     * @param salt an hex-encoded, random, site-global salt value to use to generate the key
      */
-    public static BytesEncryptor standard(String password) {
-        return new PasswordBasedBytesEncryptor(PBE_MD5_DES_ALGORITHM, password);
+    public static BytesEncryptor standard(String password, String salt) {
+        return new AesBytesEncryptor(password, password, KeyGenerators.secureRandom(16));
     }
 
     /**
@@ -38,37 +43,33 @@ public class Encryptors {
      * Encrypted text is hex-encoded.
      * @param password the password used to generate the encryptor's secret key; should not be shared
      */
-    public static TextEncryptor text(String password) {
-        return new HexEncodingTextEncryptor(standard(password));
+    public static TextEncryptor text(String password, String salt) {
+        return new HexEncodingTextEncryptor(standard(password, salt));
     }
 
     /**
      * Creates an encryptor for queryable text strings that uses standard password-based encryption.
-     * The hex-encoded salt string provided should be random and is used to protect against password dictionary attacks.
-     * Does not salt each encrypted value so an encrypted value may be queried against.
+     * Uses a shared, or constant 16 byte initialization vector so encrypting the same data results in the same encryption result.
+     * This is done to allow encrypted data to be queried against.
      * Encrypted text is hex-encoded.
      * @param password the password used to generate the encryptor's secret key; should not be shared
-     * @param salt an hex-encoded, random, site-global salt value to use to initialize the cipher
+     * @param salt an hex-encoded, random, site-global salt value to use to generate the secret key
      */
     public static TextEncryptor queryableText(String password, String salt) {
-        return new QueryableTextEncryptor(PBE_MD5_DES_ALGORITHM, password, salt);
+        return new HexEncodingTextEncryptor(new AesBytesEncryptor(password, salt, KeyGenerators.shared(16)));
     }
 
     /**
-     * Creates a text encrypter that performs no encryption.
-     * Useful for test environments where working with plain text strings is desired for simplicity.
+     * Creates a text encryptor that performs no encryption.
+     * Useful for developer testing environments where working with plain text strings is desired for simplicity.
      */
     public static TextEncryptor noOpText() {
         return NO_OP_TEXT_INSTANCE;
     }
 
-    // internal helpers
-
     private Encryptors() {
     }
 
-    private static final String PBE_MD5_DES_ALGORITHM = "PBEWithMD5AndDES";
-
     private static final TextEncryptor NO_OP_TEXT_INSTANCE = new NoOpTextEncryptor();
 
     private static final class NoOpTextEncryptor implements TextEncryptor {

+ 0 - 60
crypto/src/main/java/org/springframework/security/crypto/encrypt/QueryableTextEncryptor.java

@@ -1,60 +0,0 @@
-/*
- * Copyright 2011 the original author or authors.
- *
- * Licensed 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.
- */
-package org.springframework.security.crypto.encrypt;
-
-import static org.springframework.security.crypto.util.CipherUtils.doFinal;
-import static org.springframework.security.crypto.util.CipherUtils.initCipher;
-import static org.springframework.security.crypto.util.CipherUtils.newCipher;
-import static org.springframework.security.crypto.util.CipherUtils.newSecretKey;
-import static org.springframework.security.crypto.util.EncodingUtils.hexDecode;
-import static org.springframework.security.crypto.util.EncodingUtils.hexEncode;
-import static org.springframework.security.crypto.util.EncodingUtils.utf8Decode;
-import static org.springframework.security.crypto.util.EncodingUtils.utf8Encode;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-
-/**
- * A text encryptor that applies password-based MD5 plus DES symmetric key encryption.
- * Designed to be used to encrypt fields that are queryable; for example, an indexed field such as an OAuth apiKey.
- * Requires a random site-global salt to protect against password dictionary attacks.
- * Does not salt on each {@link #encrypt(String)} operation to allow the encrypted field to be queried.
- * @author Keith Donald
- */
-final class QueryableTextEncryptor implements TextEncryptor {
-
-    private final Cipher encryptor;
-
-    private final Cipher decryptor;
-
-    public QueryableTextEncryptor(String algorithm, String password, String salt) {
-        byte[] saltBytes = hexDecode(salt);
-        SecretKey secretKey = newSecretKey(algorithm, password);
-        encryptor = newCipher(algorithm);
-        initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, saltBytes, 1000);
-        decryptor = newCipher(algorithm);
-        initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, saltBytes, 1000);
-    }
-
-    public String encrypt(String text) {
-        return hexEncode(doFinal(encryptor, utf8Encode(text)));
-    }
-
-    public String decrypt(String encryptedText) {
-        return utf8Decode(doFinal(decryptor, hexDecode(encryptedText)));
-    }
-
-}

+ 8 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/KeyGenerators.java

@@ -39,6 +39,14 @@ public class KeyGenerators {
         return new SecureRandomBytesKeyGenerator(keyLength);
     }
 
+    /**
+     * Create a {@link BytesKeyGenerator} that returns a single, shared {@link SecureRandom} key of a custom length.
+     * @param keyLength the key length in bytes, e.g. 16, for a 16 byte key.
+     */
+    public static BytesKeyGenerator shared(int keyLength) {
+        return new SharedKeyGenerator(secureRandom(16).generateKey());
+    }
+
     /**
      * Creates a {@link StringKeyGenerator} that hex-encodes {@link SecureRandom} keys of 8 bytes in length.
      * The hex-encoded string is keyLength * 2 characters in length.

+ 40 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/SharedKeyGenerator.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * Licensed 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.
+ */
+package org.springframework.security.crypto.keygen;
+
+/**
+ * Key generator that simply returns the same key every time.
+ *
+ * @author Keith Donald
+ * @author Annabelle Donald
+ */
+final class SharedKeyGenerator implements BytesKeyGenerator {
+
+    private byte[] sharedKey;
+
+    public SharedKeyGenerator(byte[] sharedKey) {
+        this.sharedKey = sharedKey;
+    }
+
+    public int getKeyLength() {
+        return sharedKey.length;
+    }
+
+    public byte[] generateKey() {
+        return sharedKey;
+    }
+
+}

+ 42 - 5
crypto/src/main/java/org/springframework/security/crypto/util/CipherUtils.java

@@ -18,7 +18,9 @@ package org.springframework.security.crypto.util;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
 import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
 
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
@@ -38,15 +40,21 @@ public class CipherUtils {
     /**
      * Generates a SecretKey.
      */
-    public static SecretKey newSecretKey(String algorithm, String secret) {
+    public static SecretKey newSecretKey(String algorithm, String password) {
+        return newSecretKey(algorithm, new PBEKeySpec(password.toCharArray()));
+    }
+
+    /**
+     * Generates a SecretKey.
+     */
+    public static SecretKey newSecretKey(String algorithm, PBEKeySpec keySpec) {
         try {
-            PBEKeySpec pbeKeySpec = new PBEKeySpec(secret.toCharArray());
             SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
-            return factory.generateSecret(pbeKeySpec);
+            return factory.generateSecret(keySpec);
         } catch (NoSuchAlgorithmException e) {
             throw new IllegalArgumentException("Not a valid encryption algorithm", e);
         } catch (InvalidKeySpecException e) {
-            throw new IllegalArgumentException("Not a valid secert key", e);
+            throw new IllegalArgumentException("Not a valid secret key", e);
         }
     }
 
@@ -63,12 +71,41 @@ public class CipherUtils {
         }
     }
 
+    /**
+     * Initializes the Cipher for use.
+     */
+    public static <T extends AlgorithmParameterSpec> T getParameterSpec(Cipher cipher, Class<T> parameterSpecClass) {
+        try {
+            return cipher.getParameters().getParameterSpec(parameterSpecClass);
+        } catch (InvalidParameterSpecException e) {
+            throw new IllegalArgumentException("Unable to access parameter", e);
+        }
+    }
+
+    /**
+     * Initializes the Cipher for use.
+     */
+    public static void initCipher(Cipher cipher, int mode, SecretKey secretKey) {
+        initCipher(cipher, mode, secretKey, null);
+    }
+
     /**
      * Initializes the Cipher for use.
      */
     public static void initCipher(Cipher cipher, int mode, SecretKey secretKey, byte[] salt, int iterationCount) {
+        initCipher(cipher, mode, secretKey, new PBEParameterSpec(salt, iterationCount));
+    }
+
+    /**
+     * Initializes the Cipher for use.
+     */
+    public static void initCipher(Cipher cipher, int mode, SecretKey secretKey, AlgorithmParameterSpec parameterSpec) {
         try {
-            cipher.init(mode, secretKey, new PBEParameterSpec(salt, iterationCount));
+            if (parameterSpec != null) {
+                cipher.init(mode, secretKey, parameterSpec);
+            } else {
+                cipher.init(mode, secretKey);
+            }
         } catch (InvalidKeyException e) {
             throw new IllegalArgumentException("Unable to initialize due to invalid secret key", e);
         } catch (InvalidAlgorithmParameterException e) {

+ 2 - 3
crypto/src/test/java/org/springframework/security/crypto/encrypt/EncryptorsTests.java

@@ -11,7 +11,7 @@ public class EncryptorsTests {
 
     @Test
     public void standard() {
-        BytesEncryptor encryptor = Encryptors.standard("password");
+        BytesEncryptor encryptor = Encryptors.standard("password", "5c0744940b5c369b");
         byte[] result = encryptor.encrypt("text".getBytes());
         assertNotNull(result);
         assertFalse(new String(result).equals("text"));
@@ -21,7 +21,7 @@ public class EncryptorsTests {
 
     @Test
     public void text() {
-        TextEncryptor encryptor = Encryptors.text("password");
+        TextEncryptor encryptor = Encryptors.text("password", "5c0744940b5c369b");
         String result = encryptor.encrypt("text");
         assertNotNull(result);
         assertFalse(result.equals("text"));
@@ -45,5 +45,4 @@ public class EncryptorsTests {
         assertEquals("text", encryptor.encrypt("text"));
         assertEquals("text", encryptor.decrypt("text"));
     }
-
 }