SEC-3002: Add new option for AES encryption with GCM

The Galois Counter Mode (GCM) is held to be superior than the current
default CBC. This change adds an extra parameter to the constructor
of AesBytesEncryptor and a new convenience method in Encryptors.
This commit is contained in:
Dave Syer 2015-06-09 15:21:28 +01:00 committed by Rob Winch
parent ca0ffb8b5d
commit a48cc18858
4 changed files with 125 additions and 20 deletions

View File

@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>4.0.1.CI-SNAPSHOT</version>
<version>4.0.2.CI-SNAPSHOT</version>
<name>spring-security-crypto</name>
<description>spring-security-crypto</description>
<url>http://spring.io/spring-security</url>

View File

@ -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,38 +51,83 @@ 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) {
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)
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));
initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey,
this.alg.getParameterSpec(iv));
return doFinal(
decryptor,
ivGenerator != NULL_IV_GENERATOR ? encrypted(encryptedBytes,
this.decryptor,
this.ivGenerator != NULL_IV_GENERATOR ? encrypted(encryptedBytes,
iv.length) : encryptedBytes);
}
}
@ -85,26 +135,24 @@ final class AesBytesEncryptor implements BytesEncryptor {
// 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;
}
};

View File

@ -15,6 +15,7 @@
*/
package org.springframework.security.crypto.encrypt;
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
import org.springframework.security.crypto.keygen.KeyGenerators;
/**
@ -25,6 +26,28 @@ import org.springframework.security.crypto.keygen.KeyGenerators;
*/
public class Encryptors {
/**
* 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
*
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
* (instead of GCM)
*/
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt,
KeyGenerators.secureRandom(16), CipherAlgorithm.GCM);
}
/**
* 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
@ -44,11 +67,24 @@ public class Encryptors {
}
/**
* Creates a text encryptor that uses standard password-based encryption. Encrypted
* 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));

View File

@ -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");
@ -20,6 +31,16 @@ public class EncryptorsTests {
.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");