mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-04 09:42:29 +00:00
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:
parent
ae772294cb
commit
4e6b12f8b4
@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user