SEC-1659: favor AES encryption instead of DES as standard symmetric encryption algorithm
This commit is contained in:
parent
ffa7301e7f
commit
ea76efdb2c
|
@ -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";
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 extends AlgorithmParameterSpec> T getParameterSpec(Cipher cipher, Class<T> 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) {
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue