diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/PasswordBasedBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java similarity index 53% rename from crypto/src/main/java/org/springframework/security/crypto/encrypt/PasswordBasedBytesEncryptor.java rename to crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java index a0b6d2c297..cb9102927c 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/PasswordBasedBytesEncryptor.java +++ b/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"; } \ No newline at end of file diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java index b3d9a7b7c9..fc4222e2d0 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java +++ b/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 { diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/QueryableTextEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/QueryableTextEncryptor.java deleted file mode 100644 index f789463808..0000000000 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/QueryableTextEncryptor.java +++ /dev/null @@ -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))); - } - -} diff --git a/crypto/src/main/java/org/springframework/security/crypto/keygen/KeyGenerators.java b/crypto/src/main/java/org/springframework/security/crypto/keygen/KeyGenerators.java index f12d7f7477..5aaa49167a 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/keygen/KeyGenerators.java +++ b/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. diff --git a/crypto/src/main/java/org/springframework/security/crypto/keygen/SharedKeyGenerator.java b/crypto/src/main/java/org/springframework/security/crypto/keygen/SharedKeyGenerator.java new file mode 100644 index 0000000000..826ffbc31b --- /dev/null +++ b/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; + } + +} \ No newline at end of file diff --git a/crypto/src/main/java/org/springframework/security/crypto/util/CipherUtils.java b/crypto/src/main/java/org/springframework/security/crypto/util/CipherUtils.java index b01e976c39..978fa45208 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/util/CipherUtils.java +++ b/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); } } @@ -66,9 +74,38 @@ public class CipherUtils { /** * Initializes the Cipher for use. */ - public static void initCipher(Cipher cipher, int mode, SecretKey secretKey, byte[] salt, int iterationCount) { + public static T getParameterSpec(Cipher cipher, Class parameterSpecClass) { try { - cipher.init(mode, secretKey, new PBEParameterSpec(salt, iterationCount)); + 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 { + 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) { diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/EncryptorsTests.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/EncryptorsTests.java index c460bf410b..395b1a93a7 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/encrypt/EncryptorsTests.java +++ b/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")); } - }