SEC-1659: favor AES encryption instead of DES as standard symmetric encryption algorithm

This commit is contained in:
Keith Donald 2011-01-14 17:05:07 -05:00 committed by Luke Taylor
parent ffa7301e7f
commit ea76efdb2c
7 changed files with 141 additions and 115 deletions

View File

@ -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";
}

View File

@ -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 {

View File

@ -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)));
}
}

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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"));
}
}