mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-05 10:12:36 +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.concatenate;
|
||||||
import static org.springframework.security.crypto.util.EncodingUtils.subArray;
|
import static org.springframework.security.crypto.util.EncodingUtils.subArray;
|
||||||
|
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import org.springframework.security.crypto.codec.Hex;
|
import org.springframework.security.crypto.codec.Hex;
|
||||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||||
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encryptor that uses 256-bit AES encryption.
|
* Encryptor that uses 256-bit AES encryption.
|
||||||
*
|
*
|
||||||
* @author Keith Donald
|
* @author Keith Donald
|
||||||
|
* @author Dave Syer
|
||||||
*/
|
*/
|
||||||
final class AesBytesEncryptor implements BytesEncryptor {
|
final class AesBytesEncryptor implements BytesEncryptor {
|
||||||
|
|
||||||
@ -46,58 +51,108 @@ final class AesBytesEncryptor implements BytesEncryptor {
|
|||||||
|
|
||||||
private final BytesKeyGenerator ivGenerator;
|
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) {
|
public AesBytesEncryptor(String password, CharSequence salt) {
|
||||||
this(password, salt, null);
|
this(password, salt, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AesBytesEncryptor(String password, CharSequence salt, BytesKeyGenerator ivGenerator) {
|
public AesBytesEncryptor(String password, CharSequence salt,
|
||||||
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(salt), 1024, 256);
|
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);
|
SecretKey secretKey = newSecretKey("PBKDF2WithHmacSHA1", keySpec);
|
||||||
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
|
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
|
||||||
encryptor = newCipher(AES_ALGORITHM);
|
this.alg = alg;
|
||||||
decryptor = newCipher(AES_ALGORITHM);
|
this.encryptor = alg.createCipher();
|
||||||
this.ivGenerator = ivGenerator != null ? ivGenerator : NULL_IV_GENERATOR;
|
this.decryptor = alg.createCipher();
|
||||||
|
this.ivGenerator = ivGenerator != null ? ivGenerator : alg.defaultIvGenerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] encrypt(byte[] bytes) {
|
public byte[] encrypt(byte[] bytes) {
|
||||||
synchronized (encryptor) {
|
synchronized (this.encryptor) {
|
||||||
byte[] iv = ivGenerator.generateKey();
|
byte[] iv = this.ivGenerator.generateKey();
|
||||||
initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey,
|
||||||
byte[] encrypted = doFinal(encryptor, bytes);
|
this.alg.getParameterSpec(iv));
|
||||||
return ivGenerator != NULL_IV_GENERATOR ? concatenate(iv, encrypted) : encrypted;
|
byte[] encrypted = doFinal(this.encryptor, bytes);
|
||||||
|
return this.ivGenerator != NULL_IV_GENERATOR ? concatenate(iv, encrypted)
|
||||||
|
: encrypted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] decrypt(byte[] encryptedBytes) {
|
public byte[] decrypt(byte[] encryptedBytes) {
|
||||||
synchronized (decryptor) {
|
synchronized (this.decryptor) {
|
||||||
byte[] iv = iv(encryptedBytes);
|
byte[] iv = iv(encryptedBytes);
|
||||||
initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey,
|
||||||
return doFinal(decryptor, ivGenerator != NULL_IV_GENERATOR ? encrypted(encryptedBytes, iv.length) : encryptedBytes);
|
this.alg.getParameterSpec(iv));
|
||||||
|
return doFinal(
|
||||||
|
this.decryptor,
|
||||||
|
this.ivGenerator != NULL_IV_GENERATOR ? encrypted(encryptedBytes,
|
||||||
|
iv.length) : encryptedBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal helpers
|
// internal helpers
|
||||||
|
|
||||||
private byte[] iv(byte[] encrypted) {
|
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) {
|
private byte[] encrypted(byte[] encryptedBytes, int ivLength) {
|
||||||
return subArray(encryptedBytes, ivLength, encryptedBytes.length);
|
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 static final BytesKeyGenerator NULL_IV_GENERATOR = new BytesKeyGenerator() {
|
||||||
|
|
||||||
private final byte[] VALUE = new byte[16];
|
private final byte[] VALUE = new byte[16];
|
||||||
|
|
||||||
public int getKeyLength() {
|
public int getKeyLength() {
|
||||||
return VALUE.length;
|
return this.VALUE.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] generateKey() {
|
public byte[] generateKey() {
|
||||||
return VALUE;
|
return this.VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -15,57 +15,100 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.crypto.encrypt;
|
package org.springframework.security.crypto.encrypt;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
|
||||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for commonly used encryptors.
|
* Factory for commonly used encryptors. Defines the public API for constructing
|
||||||
* Defines the public API for constructing {@link BytesEncryptor} and {@link TextEncryptor} implementations.
|
* {@link BytesEncryptor} and {@link TextEncryptor} implementations.
|
||||||
*
|
*
|
||||||
* @author Keith Donald
|
* @author Keith Donald
|
||||||
*/
|
*/
|
||||||
public class Encryptors {
|
public class Encryptors {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a standard password-based bytes encryptor using 256 bit AES encryption.
|
* Creates a standard password-based bytes encryptor using 256 bit AES encryption with
|
||||||
* Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
* Galois Counter Mode (GCM). Derives the secret key using PKCS #5's PBKDF2
|
||||||
* Salts the password to prevent dictionary attacks against the key.
|
* (Password-Based Key Derivation Function #2). Salts the password to prevent
|
||||||
* The provided salt is expected to be hex-encoded; it should be random and at least 8 bytes in length.
|
* dictionary attacks against the key. The provided salt is expected to be
|
||||||
* Also applies a random 16 byte initialization vector to ensure each encrypted message will be unique.
|
* hex-encoded; it should be random and at least 8 bytes in length. Also applies a
|
||||||
* Requires Java 6.
|
* 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
|
||||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the key
|
* 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) {
|
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
||||||
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16));
|
return new AesBytesEncryptor(password.toString(), salt,
|
||||||
|
KeyGenerators.secureRandom(16), CipherAlgorithm.GCM);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a text encryptor that uses standard password-based encryption.
|
* Creates a standard password-based bytes encryptor using 256 bit AES encryption.
|
||||||
* Encrypted text is hex-encoded.
|
* 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) {
|
public static TextEncryptor text(CharSequence password, CharSequence salt) {
|
||||||
return new HexEncodingTextEncryptor(standard(password, salt));
|
return new HexEncodingTextEncryptor(standard(password, salt));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an encryptor for queryable text strings that uses standard password-based encryption.
|
* Creates an encryptor for queryable text strings that uses standard password-based
|
||||||
* Uses a 16-byte all-zero initialization vector so encrypting the same data results in the same encryption result.
|
* encryption. Uses a 16-byte all-zero initialization vector so encrypting the same
|
||||||
* This is done to allow encrypted data to be queried against.
|
* data results in the same encryption result. This is done to allow encrypted data to
|
||||||
* Encrypted text is hex-encoded.
|
* 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 password the password used to generate the encryptor's secret key; should
|
||||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the secret key
|
* 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) {
|
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.
|
* Creates a text encryptor that performs no encryption. Useful for developer testing
|
||||||
* Useful for developer testing environments where working with plain text strings is desired for simplicity.
|
* environments where working with plain text strings is desired for simplicity.
|
||||||
*/
|
*/
|
||||||
public static TextEncryptor noOpText() {
|
public static TextEncryptor noOpText() {
|
||||||
return NO_OP_TEXT_INSTANCE;
|
return NO_OP_TEXT_INSTANCE;
|
||||||
|
@ -9,6 +9,17 @@ import org.junit.Test;
|
|||||||
|
|
||||||
public class EncryptorsTests {
|
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
|
@Test
|
||||||
public void standard() throws Exception {
|
public void standard() throws Exception {
|
||||||
BytesEncryptor encryptor = Encryptors.standard("password", "5c0744940b5c369b");
|
BytesEncryptor encryptor = Encryptors.standard("password", "5c0744940b5c369b");
|
||||||
@ -19,6 +30,16 @@ public class EncryptorsTests {
|
|||||||
assertFalse(new String(result).equals(new String(encryptor.encrypt("text".getBytes()))));
|
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
|
@Test
|
||||||
public void text() {
|
public void text() {
|
||||||
TextEncryptor encryptor = Encryptors.text("password", "5c0744940b5c369b");
|
TextEncryptor encryptor = Encryptors.text("password", "5c0744940b5c369b");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user