diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java index 8d72edea37..a6d61dfe87 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java @@ -22,19 +22,24 @@ import static org.springframework.security.crypto.encrypt.CipherUtils.newSecretK import static org.springframework.security.crypto.util.EncodingUtils.concatenate; import static org.springframework.security.crypto.util.EncodingUtils.subArray; +import java.security.spec.AlgorithmParameterSpec; + import javax.crypto.Cipher; import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; /** * Encryptor that uses 256-bit AES encryption. * * @author Keith Donald + * @author Dave Syer */ final class AesBytesEncryptor implements BytesEncryptor { @@ -46,58 +51,108 @@ final class AesBytesEncryptor implements BytesEncryptor { private final BytesKeyGenerator ivGenerator; + private CipherAlgorithm alg; + + private static final String AES_CBC_ALGORITHM = "AES/CBC/PKCS5Padding"; + + private static final String AES_GCM_ALGORITHM = "AES/GCM/NoPadding"; + + public enum CipherAlgorithm { + + CBC(AES_CBC_ALGORITHM, NULL_IV_GENERATOR), GCM(AES_GCM_ALGORITHM, KeyGenerators + .secureRandom(16)); + + private BytesKeyGenerator ivGenerator; + private String name; + + private CipherAlgorithm(String name, BytesKeyGenerator ivGenerator) { + this.name = name; + this.ivGenerator = ivGenerator; + } + + @Override + public String toString() { + return this.name; + } + + public AlgorithmParameterSpec getParameterSpec(byte[] iv) { + return this == CBC ? new IvParameterSpec(iv) : new GCMParameterSpec(128, iv); + } + + public Cipher createCipher() { + return newCipher(this.toString()); + } + + public BytesKeyGenerator defaultIvGenerator() { + return this.ivGenerator; + } + } + public AesBytesEncryptor(String password, CharSequence salt) { this(password, salt, null); } - public AesBytesEncryptor(String password, CharSequence salt, BytesKeyGenerator ivGenerator) { - PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(salt), 1024, 256); + public AesBytesEncryptor(String password, CharSequence salt, + BytesKeyGenerator ivGenerator) { + this(password, salt, ivGenerator, CipherAlgorithm.CBC); + } + + public AesBytesEncryptor(String password, CharSequence salt, + BytesKeyGenerator ivGenerator, CipherAlgorithm alg) { + PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(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 != null ? ivGenerator : NULL_IV_GENERATOR; + this.alg = alg; + this.encryptor = alg.createCipher(); + this.decryptor = alg.createCipher(); + this.ivGenerator = ivGenerator != null ? ivGenerator : alg.defaultIvGenerator(); } public byte[] encrypt(byte[] bytes) { - synchronized (encryptor) { - byte[] iv = ivGenerator.generateKey(); - initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); - byte[] encrypted = doFinal(encryptor, bytes); - return ivGenerator != NULL_IV_GENERATOR ? concatenate(iv, encrypted) : encrypted; + synchronized (this.encryptor) { + byte[] iv = this.ivGenerator.generateKey(); + initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey, + this.alg.getParameterSpec(iv)); + byte[] encrypted = doFinal(this.encryptor, bytes); + return this.ivGenerator != NULL_IV_GENERATOR ? concatenate(iv, encrypted) + : encrypted; } } public byte[] decrypt(byte[] encryptedBytes) { - synchronized (decryptor) { + synchronized (this.decryptor) { byte[] iv = iv(encryptedBytes); - initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); - return doFinal(decryptor, ivGenerator != NULL_IV_GENERATOR ? encrypted(encryptedBytes, iv.length) : encryptedBytes); + initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey, + this.alg.getParameterSpec(iv)); + return doFinal( + this.decryptor, + this.ivGenerator != NULL_IV_GENERATOR ? encrypted(encryptedBytes, + iv.length) : encryptedBytes); } } // internal helpers private byte[] iv(byte[] encrypted) { - return ivGenerator != NULL_IV_GENERATOR ? subArray(encrypted, 0, ivGenerator.getKeyLength()) : NULL_IV_GENERATOR.generateKey(); + return this.ivGenerator != NULL_IV_GENERATOR ? subArray(encrypted, 0, + this.ivGenerator.getKeyLength()) : NULL_IV_GENERATOR.generateKey(); } private byte[] encrypted(byte[] encryptedBytes, int ivLength) { return subArray(encryptedBytes, ivLength, encryptedBytes.length); } - private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding"; - private static final BytesKeyGenerator NULL_IV_GENERATOR = new BytesKeyGenerator() { private final byte[] VALUE = new byte[16]; public int getKeyLength() { - return VALUE.length; + return this.VALUE.length; } public byte[] generateKey() { - return VALUE; + return this.VALUE; } }; 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 2dbc14e44e..865859453e 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,57 +15,100 @@ */ package org.springframework.security.crypto.encrypt; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm; import org.springframework.security.crypto.keygen.KeyGenerators; /** - * Factory for commonly used encryptors. - * Defines the public API for constructing {@link BytesEncryptor} and {@link TextEncryptor} implementations. + * Factory for commonly used encryptors. Defines the public API for constructing + * {@link BytesEncryptor} and {@link TextEncryptor} implementations. * * @author Keith Donald */ public class Encryptors { /** - * 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. + * Creates a standard password-based bytes encryptor using 256 bit AES encryption with + * Galois Counter Mode (GCM). 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 a hex-encoded, random, site-global salt value to use to generate the key + * @param password the password used to generate the encryptor's secret key; should + * not be shared + * @param salt a hex-encoded, random, site-global salt value to use to generate the + * key + * + * @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode + * (instead of GCM) */ - public static BytesEncryptor standard(CharSequence password, CharSequence salt) { - return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16)); + public static BytesEncryptor stronger(CharSequence password, CharSequence salt) { + return new AesBytesEncryptor(password.toString(), salt, + KeyGenerators.secureRandom(16), CipherAlgorithm.GCM); } /** - * Creates a text encryptor that uses standard password-based encryption. - * Encrypted text is hex-encoded. + * 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 password the password used to generate the encryptor's secret key; should + * not be shared + * @param salt a hex-encoded, random, site-global salt value to use to generate the + * key + */ + public static BytesEncryptor standard(CharSequence password, CharSequence salt) { + return new AesBytesEncryptor(password.toString(), salt, + KeyGenerators.secureRandom(16)); + } + + /** + * Creates a text encryptor that uses "stronger" password-based encryption. Encrypted + * text is hex-encoded. + * + * @param password the password used to generate the encryptor's secret key; should + * not be shared + * @see Encryptors#stronger(CharSequence, CharSequence) + */ + public static TextEncryptor delux(CharSequence password, CharSequence salt) { + return new HexEncodingTextEncryptor(stronger(password, salt)); + } + + /** + * Creates a text encryptor that uses "standard" password-based encryption. Encrypted + * text is hex-encoded. + * + * @param password the password used to generate the encryptor's secret key; should + * not be shared + * @see Encryptors#standard(CharSequence, CharSequence) */ public static TextEncryptor text(CharSequence password, CharSequence salt) { return new HexEncodingTextEncryptor(standard(password, salt)); } /** - * Creates an encryptor for queryable text strings that uses standard password-based encryption. - * Uses a 16-byte all-zero 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. + * Creates an encryptor for queryable text strings that uses standard password-based + * encryption. Uses a 16-byte all-zero 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 a hex-encoded, random, site-global salt value to use to generate the secret key + * @param password the password used to generate the encryptor's secret key; should + * not be shared + * @param salt a hex-encoded, random, site-global salt value to use to generate the + * secret key */ public static TextEncryptor queryableText(CharSequence password, CharSequence salt) { - return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(), salt)); + return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(), + salt)); } /** - * Creates a text encryptor that performs no encryption. - * Useful for developer testing 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; 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 4683c468d0..fc97300fbe 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 @@ -9,6 +9,17 @@ import org.junit.Test; public class EncryptorsTests { + @Test + public void stronger() throws Exception { + BytesEncryptor encryptor = Encryptors.stronger("password", "5c0744940b5c369b"); + byte[] result = encryptor.encrypt("text".getBytes("UTF-8")); + assertNotNull(result); + assertFalse(new String(result).equals("text")); + assertEquals("text", new String(encryptor.decrypt(result))); + assertFalse(new String(result).equals(new String(encryptor.encrypt("text" + .getBytes())))); + } + @Test public void standard() throws Exception { BytesEncryptor encryptor = Encryptors.standard("password", "5c0744940b5c369b"); @@ -19,6 +30,16 @@ public class EncryptorsTests { assertFalse(new String(result).equals(new String(encryptor.encrypt("text".getBytes())))); } + @Test + public void preferred() { + TextEncryptor encryptor = Encryptors.delux("password", "5c0744940b5c369b"); + String result = encryptor.encrypt("text"); + assertNotNull(result); + assertFalse(result.equals("text")); + assertEquals("text", encryptor.decrypt(result)); + assertFalse(result.equals(encryptor.encrypt("text"))); + } + @Test public void text() { TextEncryptor encryptor = Encryptors.text("password", "5c0744940b5c369b");