NIFI-3116 This closes #2108. Added initial regression test for StringEncryptor to ensure continued functionality during removal of Jasypt.

Added external compatibility regression test for StringEncryptor to ensure continued functionality during removal of Jasypt.
Documents custom salt lengths and iteration counts for each encryption method.
Added (ignored) failing tests for keyed encryption (Jasypt does not support keyed encryption).
Changed StringEncryptor to non-final class and added protected default constructor.
Added failing test for initialization status.
Added utility methods in CipherUtility.
Moved PBE cipher providers (and tests) from nifi-standard-processors to nifi-security-utils module.
Implemented PBE and keyed encryption/decryption logic.
Moved Scrypt unit test back into scrypt package.
Resolved test failures in limited strength cryptographic environment.
Implemented keyed encryption/decryption and enabled unit tests.
Removed Jasypt dependency from production scope (kept in test scope for backward compatibility tests).

Signed-off-by: joewitt <joewitt@apache.org>
This commit is contained in:
Andy LoPresto 2017-08-15 15:33:19 -04:00 committed by joewitt
parent 3b1b326b13
commit 2c1f5b49e4
33 changed files with 1507 additions and 630 deletions

View File

@ -125,10 +125,6 @@ The following binary components are provided under the Apache Software License v
CurvesAIP is BSD-licensed software (https://github.com/virtuald/curvesapi/)
Copyright (c) 2005, Graph Builder
(ASLv2) Jasypt
The following NOTICE information applies:
Copyright (c) 2007-2010, The JASYPT team (http://www.jasypt.org)
(ASLv2) Apache Commons Codec
The following NOTICE information applies:
Apache Commons Codec

View File

@ -68,6 +68,9 @@
<configuration>
<excludes combine.children="append">
<exclude>src/test/resources/xxe_template.xml</exclude>
<!-- This file is copied from https://github.com/jeremyh/jBCrypt
because the binary is compiled for Java 8 and we must support Java 7 -->
<exclude>src/main/java/org/apache/nifi/security/util/crypto/bcrypt/BCrypt.java</exclude>
</excludes>
</configuration>
</plugin>

View File

@ -94,6 +94,13 @@ public enum EncryptionMethod {
return !algorithm.startsWith("PBE") && !algorithm.startsWith("PGP");
}
/**
* @return true if this algorithm uses its own internal key derivation process from a password
*/
public boolean isPBECipher() {
return algorithm.startsWith("PBE");
}
@Override
public String toString() {
final ToStringBuilder builder = new ToStringBuilder(this);

View File

@ -20,6 +20,11 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -29,6 +34,11 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processor.exception.ProcessException;
@ -41,6 +51,7 @@ public class CipherUtility {
private static final Pattern KEY_LENGTH_PATTERN = Pattern.compile("([\\d]+)BIT");
private static final Map<String, Integer> MAX_PASSWORD_LENGTH_BY_ALGORITHM;
private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128;
static {
Map<String, Integer> aMap = new HashMap<>();
@ -187,6 +198,12 @@ public class CipherUtility {
return getValidKeyLengthsForAlgorithm(algorithm).contains(keyLength);
}
/**
* Returns a list of valid key lengths in bits for this algorithm. If the algorithm cannot be parsed, an empty list is returned.
*
* @param algorithm the name of the algorithm
* @return a list of valid key lengths
*/
public static List<Integer> getValidKeyLengthsForAlgorithm(String algorithm) {
List<Integer> validKeyLengths = new ArrayList<>();
if (StringUtils.isEmpty(algorithm)) {
@ -309,11 +326,84 @@ public class CipherUtility {
if (encryptionMethod == null) {
throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
}
return MAX_PASSWORD_LENGTH_BY_ALGORITHM.getOrDefault(encryptionMethod.getAlgorithm(), -1);
}
if (MAX_PASSWORD_LENGTH_BY_ALGORITHM.containsKey(encryptionMethod.getAlgorithm())) {
return MAX_PASSWORD_LENGTH_BY_ALGORITHM.get(encryptionMethod.getAlgorithm());
} else {
return -1;
public static boolean isUnlimitedStrengthCryptoSupported() {
try {
return (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH);
} catch (NoSuchAlgorithmException e) {
return false;
}
}
public static boolean isPBECipher(String algorithm) {
EncryptionMethod em = EncryptionMethod.forAlgorithm(algorithm);
return em != null && em.isPBECipher();
}
public static boolean isKeyedCipher(String algorithm) {
EncryptionMethod em = EncryptionMethod.forAlgorithm(algorithm);
return em != null && em.isKeyedCipher();
}
/**
* Initializes a {@link Cipher} object with the given PBE parameters.
*
* @param algorithm the algorithm
* @param provider the JCA provider
* @param password the password
* @param salt the salt
* @param iterationCount the KDF iteration count
* @param encryptMode true to encrypt; false to decrypt
* @return the initialized Cipher
* @throws IllegalArgumentException if any parameter is invalid
*/
public static Cipher initPBECipher(String algorithm, String provider, String password, byte[] salt, int iterationCount, boolean encryptMode) throws IllegalArgumentException {
try {
// Initialize secret key from password
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, provider);
SecretKey tempKey = factory.generateSecret(pbeKeySpec);
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, iterationCount);
Cipher cipher = Cipher.getInstance(algorithm, provider);
cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, tempKey, parameterSpec);
return cipher;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException("One or more parameters to initialize the PBE cipher were invalid", e);
}
}
/**
* Returns the KDF iteration count for various PBE algorithms. These values were determined empirically from configured/chosen legacy values from the earlier version of the project.
* Code demonstrating this is available at {@link StringEncryptorTest#testPBEncryptionShouldBeExternallyConsistent}.
*
* @param algorithm the {@link EncryptionMethod#algorithm}
* @return the iteration count. Default is 0.
*/
public static int getIterationCountForAlgorithm(String algorithm) {
int iterationCount = 0;
// DES/RC*/SHA-1/-256 algorithms use custom iteration counts
if (algorithm.matches("DES|RC|SHAA|SHA256")) {
iterationCount = 1000;
}
return iterationCount;
}
/**
* Returns the salt length for various PBE algorithms. These values were determined empirically from configured/chosen legacy values from the earlier version of the project.
* Code demonstrating this is available at {@link StringEncryptorTest#testPBEncryptionShouldBeExternallyConsistent}.
*
* @param algorithm the {@link EncryptionMethod#algorithm}
* @return the salt length in bytes. Default is 16.
*/
public static int getSaltLengthForAlgorithm(String algorithm) {
int saltLength = 16;
// DES/RC* algorithms use custom iteration counts
if (algorithm.contains("DES") || algorithm.contains("RC")) {
saltLength = 8;
}
return saltLength;
}
}

View File

@ -42,7 +42,7 @@ public abstract class KeyedCipherProvider implements CipherProvider {
* @return the initialized cipher
* @throws Exception if there is a problem initializing the cipher
*/
abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws Exception;
public abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws Exception;
/**
* Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception.
@ -53,7 +53,7 @@ public abstract class KeyedCipherProvider implements CipherProvider {
* @return the initialized cipher
* @throws Exception if there is a problem initializing the cipher or if decryption is requested
*/
abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception;
public abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception;
/**
* Generates a new random IV of the correct length.

View File

@ -537,8 +537,8 @@ public class BCrypt {
* Initialise the Blowfish key schedule
*/
private void init_key() {
P = (int[]) P_orig.clone();
S = (int[]) S_orig.clone();
P = P_orig.clone();
S = S_orig.clone();
}
/**
@ -690,7 +690,7 @@ public class BCrypt {
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds,
(int[]) bf_crypt_ciphertext.clone());
bf_crypt_ciphertext.clone());
rs.append("$2");
if (minor >= 'a')

View File

@ -41,18 +41,18 @@ import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.assertTrue
@RunWith(JUnit4.class)
public class BcryptCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProviderGroovyTest.class);
class BcryptCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProviderGroovyTest.class)
private static List<EncryptionMethod> strongKDFEncryptionMethods
private static final int DEFAULT_KEY_LENGTH = 128;
private static final int DEFAULT_KEY_LENGTH = 128
public static final String MICROBENCHMARK = "microbenchmark"
private static ArrayList<Integer> AES_KEY_LENGTHS
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
@ -60,7 +60,7 @@ public class BcryptCipherProviderGroovyTest {
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
if (CipherUtility.isUnlimitedStrengthCryptoSupported()) {
AES_KEY_LENGTHS = [128, 192, 256]
} else {
AES_KEY_LENGTHS = [128]
@ -68,117 +68,117 @@ public class BcryptCipherProviderGroovyTest {
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final byte[] IV = Hex.decodeHex("01" * 16 as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
CipherUtility.isUnlimitedStrengthCryptoSupported())
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final int LONG_KEY_LENGTH = 256
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testHashPWShouldMatchTestVectors() {
void testHashPWShouldMatchTestVectors() {
// Arrange
final String PASSWORD = 'abcdefghijklmnopqrstuvwxyz'
final String SALT = '$2a$10$fVH8e28OQRj9tqiDXs1e1u'
@ -194,16 +194,16 @@ public class BcryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
void testGetCipherShouldSupportExternalCompatibility() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword";
final String PLAINTEXT = "This is a plaintext message."
final String PASSWORD = "thisIsABadPassword"
// These values can be generated by running `$ ./openssl_bcrypt` in the terminal
final byte[] SALT = Hex.decodeHex("81455b915ce9efd1fc61a08eb0255936" as char[]);
final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[]);
final byte[] SALT = Hex.decodeHex("81455b915ce9efd1fc61a08eb0255936" as char[])
final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[])
// $v2$w2$base64_salt_22__base64_hash_31
final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
@ -222,8 +222,8 @@ public class BcryptCipherProviderGroovyTest {
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
// Sanity check
Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC")
@ -245,25 +245,25 @@ public class BcryptCipherProviderGroovyTest {
assert generatedHash == FULL_HASH
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
@Test
public void testGetCipherShouldHandleFullSalt() throws Exception {
void testGetCipherShouldHandleFullSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword";
final String PLAINTEXT = "This is a plaintext message."
final String PASSWORD = "thisIsABadPassword"
// These values can be generated by running `$ ./openssl_bcrypt.rb` in the terminal
final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[]);
final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[])
// $v2$w2$base64_salt_22__base64_hash_31
final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
@ -282,37 +282,37 @@ public class BcryptCipherProviderGroovyTest {
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
@Test
public void testGetCipherShouldHandleUnformedSalt() throws Exception {
void testGetCipherShouldHandleUnformedSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "thisIsABadPassword";
final String PASSWORD = "thisIsABadPassword"
final def INVALID_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', 'bad_salt', '$3a$11$', 'x', '$2a$10$']
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Act
INVALID_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true)
}
// Assert
@ -331,20 +331,20 @@ public class BcryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldRejectEmptySalt() throws Exception {
void testGetCipherShouldRejectEmptySalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "thisIsABadPassword";
final String PASSWORD = "thisIsABadPassword"
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Two different errors -- one explaining the no-salt method is not supported, and the other for an empty byte[] passed
// Act
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true)
}
logger.expected(msg)
@ -353,29 +353,29 @@ public class BcryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final byte[] IV = Hex.decodeHex("00" * 16 as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false)
}
// Assert
@ -384,15 +384,15 @@ public class BcryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final byte[] IV = Hex.decodeHex("01" * 16 as char[])
final String PLAINTEXT = "This is a plaintext message.";
final String PLAINTEXT = "This is a plaintext message."
// Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
@ -403,32 +403,32 @@ public class BcryptCipherProviderGroovyTest {
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
}
@Test
public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final byte[] IV = Hex.decodeHex("00" * 16 as char[])
final String PLAINTEXT = "This is a plaintext message.";
final String PLAINTEXT = "This is a plaintext message."
// Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
@ -440,7 +440,7 @@ public class BcryptCipherProviderGroovyTest {
// Initialize a cipher for encryption
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true)
}
// Assert
@ -449,9 +449,9 @@ public class BcryptCipherProviderGroovyTest {
}
@Test
public void testGenerateSaltShouldUseProvidedWorkFactor() throws Exception {
void testGenerateSaltShouldUseProvidedWorkFactor() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(11);
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(11)
int workFactor = cipherProvider.getWorkFactor()
// Act
@ -466,9 +466,9 @@ public class BcryptCipherProviderGroovyTest {
@Ignore("This test can be run on a specific machine to evaluate if the default work factor is sufficient")
@Test
public void testDefaultConstructorShouldProvideStrongWorkFactor() {
void testDefaultConstructorShouldProvideStrongWorkFactor() {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider();
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider()
// Values taken from http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ and http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt

View File

@ -44,7 +44,7 @@ class CipherProviderFactoryGroovyTest extends GroovyTestCase {
]
@BeforeClass
public static void setUpOnce() throws Exception {
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
@ -53,15 +53,15 @@ class CipherProviderFactoryGroovyTest extends GroovyTestCase {
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
@Test
public void testGetCipherProviderShouldResolveRegisteredKDFs() {
void testGetCipherProviderShouldResolveRegisteredKDFs() {
// Arrange
// Act
@ -77,7 +77,7 @@ class CipherProviderFactoryGroovyTest extends GroovyTestCase {
@Ignore("Cannot mock enum using Groovy map coercion")
@Test
public void testGetCipherProviderShouldHandleUnregisteredKDFs() {
void testGetCipherProviderShouldHandleUnregisteredKDFs() {
// Arrange
// Can't mock this; see http://stackoverflow.com/questions/5323505/mocking-java-enum-to-add-a-value-to-test-fail-case

View File

@ -40,63 +40,63 @@ import java.security.Security
import static org.junit.Assert.fail
@RunWith(JUnit4.class)
public class NiFiLegacyCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProviderGroovyTest.class);
class NiFiLegacyCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProviderGroovyTest.class)
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>();
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>();
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>()
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>()
private static final String PROVIDER_NAME = "BC";
private static final int ITERATION_COUNT = 1000;
private static final String PROVIDER_NAME = "BC"
private static final int ITERATION_COUNT = 1000
private static final byte[] SALT_16_BYTES = Hex.decodeHex("aabbccddeeff00112233445566778899".toCharArray());
private static final byte[] SALT_16_BYTES = Hex.decodeHex("aabbccddeeff00112233445566778899".toCharArray())
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
try {
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
SecretKey tempKey = factory.generateSecret(pbeKeySpec);
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray())
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME)
SecretKey tempKey = factory.generateSecret(pbeKeySpec)
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
return cipher;
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT)
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME)
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec)
return cipher
} catch (Exception e) {
logger.error("Error generating legacy cipher", e);
fail(e.getMessage());
logger.error("Error generating legacy cipher", e)
fail(e.getMessage())
}
return null;
return null
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "shortPassword";
final String plaintext = "This is a plaintext message.";
final String PASSWORD = "shortPassword"
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod encryptionMethod : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm());
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), encryptionMethod)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
@ -107,64 +107,64 @@ public class NiFiLegacyCipherProviderGroovyTest {
logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
CipherUtility.isUnlimitedStrengthCryptoSupported())
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "shortPassword";
final String plaintext = "This is a plaintext message.";
final String PASSWORD = "shortPassword"
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod encryptionMethod : pbeEncryptionMethods) {
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm());
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm())
byte[] salt = cipherProvider.generateSalt(encryptionMethod)
logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldSupportLegacyCode() throws Exception {
void testGetCipherShouldSupportLegacyCode() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "short";
final String plaintext = "This is a plaintext message.";
final String PASSWORD = "short"
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod encryptionMethod : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm());
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), encryptionMethod)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
@ -175,33 +175,33 @@ public class NiFiLegacyCipherProviderGroovyTest {
logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})")
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, salt, encryptionMethod.getAlgorithm());
Cipher legacyCipher = getLegacyCipher(PASSWORD, salt, encryptionMethod.getAlgorithm())
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false)
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "short";
final byte[] SALT = new byte[0];
final String PASSWORD = "short"
final byte[] SALT = new byte[0]
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
logger.info("Using algorithm: {}", em.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
@ -209,48 +209,48 @@ public class NiFiLegacyCipherProviderGroovyTest {
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm())
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false)
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldIgnoreKeyLength() throws Exception {
void testGetCipherShouldIgnoreKeyLength() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = SALT_16_BYTES
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
// Initialize a cipher for encryption
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true);
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true)
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
// Act
KEY_LENGTHS.each { int keyLength ->
logger.info("Decrypting with 'requested' key length: ${keyLength}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@ -262,7 +262,7 @@ public class NiFiLegacyCipherProviderGroovyTest {
*/
@Ignore("Only needed once to determine max supported password lengths")
@Test
public void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() throws IOException {
void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() throws IOException {
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
boolean unlimitedCryptoSupported = PasswordBasedEncryptor.supportsUnlimitedStrength()
@ -277,7 +277,7 @@ public class NiFiLegacyCipherProviderGroovyTest {
String password = "x" * length
try {
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
Cipher cipher = cipherProvider.getCipher(encryptionMethod, password, true)
return false
} catch (Exception e) {

View File

@ -40,63 +40,63 @@ import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.fail
@RunWith(JUnit4.class)
public class OpenSSLPKCS5CipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(OpenSSLPKCS5CipherProviderGroovyTest.class);
class OpenSSLPKCS5CipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(OpenSSLPKCS5CipherProviderGroovyTest.class)
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>();
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>();
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>()
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>()
private static final String PROVIDER_NAME = "BC";
private static final int ITERATION_COUNT = 0;
private static final String PROVIDER_NAME = "BC"
private static final int ITERATION_COUNT = 0
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
try {
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
SecretKey tempKey = factory.generateSecret(pbeKeySpec);
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray())
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME)
SecretKey tempKey = factory.generateSecret(pbeKeySpec)
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
return cipher;
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT)
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME)
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec)
return cipher
} catch (Exception e) {
logger.error("Error generating legacy cipher", e);
fail(e.getMessage());
logger.error("Error generating legacy cipher", e)
fail(e.getMessage())
}
return null;
return null
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "short";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String PASSWORD = "short"
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray())
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
logger.info("Using algorithm: {}", em.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
@ -104,65 +104,65 @@ public class OpenSSLPKCS5CipherProviderGroovyTest {
}
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
CipherUtility.isUnlimitedStrengthCryptoSupported())
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray())
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : pbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
logger.info("Using algorithm: {}", em.getAlgorithm())
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldSupportLegacyCode() throws Exception {
void testGetCipherShouldSupportLegacyCode() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray())
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
logger.info("Using algorithm: {}", em.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
@ -170,33 +170,33 @@ public class OpenSSLPKCS5CipherProviderGroovyTest {
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm())
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, SALT, false)
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "short";
final byte[] SALT = new byte[0];
final String PASSWORD = "short"
final byte[] SALT = new byte[0]
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
logger.info("Using algorithm: {}", em.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
@ -204,64 +204,64 @@ public class OpenSSLPKCS5CipherProviderGroovyTest {
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm())
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false)
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldIgnoreKeyLength() throws Exception {
void testGetCipherShouldIgnoreKeyLength() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray())
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
// Initialize a cipher for encryption
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true);
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true)
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
// Act
KEY_LENGTHS.each { int keyLength ->
logger.info("Decrypting with 'requested' key length: ${keyLength}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldRequireEncryptionMethod() throws Exception {
void testGetCipherShouldRequireEncryptionMethod() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray())
// Act
logger.info("Using algorithm: null");
logger.info("Using algorithm: null")
def msg = shouldFail(IllegalArgumentException) {
Cipher providedCipher = cipherProvider.getCipher(null, PASSWORD, SALT, false);
Cipher providedCipher = cipherProvider.getCipher(null, PASSWORD, SALT, false)
}
// Assert
@ -269,18 +269,18 @@ public class OpenSSLPKCS5CipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldRequirePassword() throws Exception {
void testGetCipherShouldRequirePassword() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray())
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
// Act
logger.info("Using algorithm: ${encryptionMethod}");
logger.info("Using algorithm: ${encryptionMethod}")
def msg = shouldFail(IllegalArgumentException) {
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, "", SALT, false);
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, "", SALT, false)
}
// Assert
@ -288,19 +288,19 @@ public class OpenSSLPKCS5CipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldValidateSaltLength() throws Exception {
void testGetCipherShouldValidateSaltLength() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("00112233445566".toCharArray());
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex("00112233445566".toCharArray())
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
// Act
logger.info("Using algorithm: ${encryptionMethod}");
logger.info("Using algorithm: ${encryptionMethod}")
def msg = shouldFail(IllegalArgumentException) {
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, false);
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, false)
}
// Assert
@ -308,7 +308,7 @@ public class OpenSSLPKCS5CipherProviderGroovyTest {
}
@Test
public void testGenerateSaltShouldProvideValidSalt() throws Exception {
void testGenerateSaltShouldProvideValidSalt() throws Exception {
// Arrange
PBECipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()

View File

@ -37,13 +37,13 @@ import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.assertTrue
@RunWith(JUnit4.class)
public class PBKDF2CipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(PBKDF2CipherProviderGroovyTest.class);
class PBKDF2CipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(PBKDF2CipherProviderGroovyTest.class)
private static List<EncryptionMethod> strongKDFEncryptionMethods
public static final String MICROBENCHMARK = "microbenchmark"
private static final int DEFAULT_KEY_LENGTH = 128;
private static final int DEFAULT_KEY_LENGTH = 128
private static final int TEST_ITERATION_COUNT = 1000
private final String DEFAULT_PRF = "SHA-512"
private final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210"
@ -51,8 +51,8 @@ public class PBKDF2CipherProviderGroovyTest {
private static ArrayList<Integer> AES_KEY_LENGTHS
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
@ -60,7 +60,7 @@ public class PBKDF2CipherProviderGroovyTest {
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
if (CipherUtility.isUnlimitedStrengthCryptoSupported()) {
AES_KEY_LENGTHS = [128, 192, 256]
} else {
AES_KEY_LENGTHS = [128]
@ -68,53 +68,53 @@ public class PBKDF2CipherProviderGroovyTest {
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldRejectInvalidIV() throws Exception {
void testGetCipherShouldRejectInvalidIV() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final def INVALID_IVS = (0..15).collect { int length -> new byte[length] }
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
@ -137,91 +137,91 @@ public class PBKDF2CipherProviderGroovyTest {
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
CipherUtility.isUnlimitedStrengthCryptoSupported())
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final int LONG_KEY_LENGTH = 256
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testShouldRejectEmptyPRF() throws Exception {
void testShouldRejectEmptyPRF() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
String prf = ""
// Act
logger.info("Using PRF ${prf}")
def msg = shouldFail(IllegalArgumentException) {
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT)
}
// Assert
@ -229,15 +229,15 @@ public class PBKDF2CipherProviderGroovyTest {
}
@Test
public void testShouldResolveDefaultPRF() throws Exception {
void testShouldResolveDefaultPRF() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
final PBKDF2CipherProvider SHA512_PROVIDER = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
@ -246,117 +246,117 @@ public class PBKDF2CipherProviderGroovyTest {
logger.info("Using ${prf}")
// Act
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT)
logger.info("Resolved PRF to ${cipherProvider.getPRFName()}")
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = SHA512_PROVIDER.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = SHA512_PROVIDER.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
@Test
public void testShouldResolveVariousPRFs() throws Exception {
void testShouldResolveVariousPRFs() throws Exception {
// Arrange
final List<String> PRFS = ["SHA-1", "MD5", "SHA-256", "SHA-384", "SHA-512"]
RandomIVPBECipherProvider cipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
PRFS.each { String prf ->
logger.info("Using ${prf}")
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT)
logger.info("Resolved PRF to ${cipherProvider.getPRFName()}")
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
void testGetCipherShouldSupportExternalCompatibility() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider("SHA-256", TEST_ITERATION_COUNT);
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider("SHA-256", TEST_ITERATION_COUNT)
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword";
final String PLAINTEXT = "This is a plaintext message."
final String PASSWORD = "thisIsABadPassword"
// These values can be generated by running `$ ./openssl_pbkdf2.rb` in the terminal
final byte[] SALT = Hex.decodeHex("ae2481bee3d8b5d5b732bf464ea2ff01" as char[]);
final byte[] IV = Hex.decodeHex("26db997dcd18472efd74dabe5ff36853" as char[]);
final byte[] SALT = Hex.decodeHex("ae2481bee3d8b5d5b732bf464ea2ff01" as char[])
final byte[] IV = Hex.decodeHex("26db997dcd18472efd74dabe5ff36853" as char[])
final String CIPHER_TEXT = "92edbabae06add6275a1d64815755a9ba52afc96e2c1a316d3abbe1826e96f6c"
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false)
}
// Assert
@ -365,23 +365,23 @@ public class PBKDF2CipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldRejectInvalidSalt() throws Exception {
void testGetCipherShouldRejectInvalidSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "thisIsABadPassword";
final String PASSWORD = "thisIsABadPassword"
final def INVALID_SALTS = ['pbkdf2', '$3a$11$', 'x', '$2a$10$', '', null]
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Act
INVALID_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt?.bytes, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt?.bytes, DEFAULT_KEY_LENGTH, true)
}
// Assert
@ -390,15 +390,15 @@ public class PBKDF2CipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
final String PLAINTEXT = "This is a plaintext message.";
final String PLAINTEXT = "This is a plaintext message."
// Currently only AES ciphers are compatible with PBKDF2, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
@ -409,30 +409,30 @@ public class PBKDF2CipherProviderGroovyTest {
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
}
@Test
public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[])
// Currently only AES ciphers are compatible with PBKDF2, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
@ -444,7 +444,7 @@ public class PBKDF2CipherProviderGroovyTest {
// Initialize a cipher for encryption
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true)
}
// Assert
@ -454,9 +454,9 @@ public class PBKDF2CipherProviderGroovyTest {
@Ignore("This test can be run on a specific machine to evaluate if the default iteration count is sufficient")
@Test
public void testDefaultConstructorShouldProvideStrongIterationCount() {
void testDefaultConstructorShouldProvideStrongIterationCount() {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider();
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider()
// Values taken from http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ and http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
@ -530,7 +530,7 @@ public class PBKDF2CipherProviderGroovyTest {
}
@Test
public void testGenerateSaltShouldProvideValidSalt() throws Exception {
void testGenerateSaltShouldProvideValidSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)

View File

@ -43,20 +43,20 @@ import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.assertTrue
@RunWith(JUnit4.class)
public class ScryptCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(ScryptCipherProviderGroovyTest.class);
class ScryptCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(ScryptCipherProviderGroovyTest.class)
private static List<EncryptionMethod> strongKDFEncryptionMethods
private static final int DEFAULT_KEY_LENGTH = 128;
private static final int DEFAULT_KEY_LENGTH = 128
public static final String MICROBENCHMARK = "microbenchmark"
private static ArrayList<Integer> AES_KEY_LENGTHS
RandomIVPBECipherProvider cipherProvider
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
@ -64,7 +64,7 @@ public class ScryptCipherProviderGroovyTest {
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
if (CipherUtility.isUnlimitedStrengthCryptoSupported()) {
AES_KEY_LENGTHS = [128, 192, 256]
} else {
AES_KEY_LENGTHS = [128]
@ -72,119 +72,119 @@ public class ScryptCipherProviderGroovyTest {
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
// Very fast parameters to test for correctness rather than production values
cipherProvider = new ScryptCipherProvider(4, 1, 1)
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final byte[] IV = Hex.decodeHex("01" * 16 as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
CipherUtility.isUnlimitedStrengthCryptoSupported())
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final int LONG_KEY_LENGTH = 256
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
}
@Test
public void testScryptShouldSupportExternalCompatibility() throws Exception {
void testScryptShouldSupportExternalCompatibility() throws Exception {
// Arrange
// Default values are N=2^14, r=8, p=1, but the provided salt will contain the parameters used
cipherProvider = new ScryptCipherProvider()
final String PLAINTEXT = "This is a plaintext message.";
final String PLAINTEXT = "This is a plaintext message."
final String PASSWORD = "thisIsABadPassword"
final int DK_LEN = 128
@ -237,46 +237,46 @@ public class ScryptCipherProviderGroovyTest {
logger.info("Converted hash from hex ${hashHex} to Base64 ${base64Hash}")
assert Hex.encodeHexString(Base64.decodeBase64(base64Hash)) == hashHex
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("External cipher text: ${CIPHER_TEXT} ${cipherBytes.length}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
logger.info("External cipher text: ${CIPHER_TEXT} ${cipherBytes.length}")
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, javaSalt.bytes, IV, DK_LEN, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, javaSalt.bytes, IV, DK_LEN, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
@Test
public void testGetCipherShouldHandleSaltWithoutParameters() throws Exception {
void testGetCipherShouldHandleSaltWithoutParameters() throws Exception {
// Arrange
// To help Groovy resolve implementation private methods not known at interface level
cipherProvider = cipherProvider as ScryptCipherProvider
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = new byte[cipherProvider.defaultSaltLength]
new SecureRandom().nextBytes(SALT)
final String EXPECTED_FORMATTED_SALT = cipherProvider.formatSaltForScrypt(SALT)
logger.info("Expected salt: ${EXPECTED_FORMATTED_SALT}")
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Act
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
// Manually initialize a cipher for decrypt with the expected salt
byte[] parsedSalt = new byte[cipherProvider.defaultSaltLength]
@ -287,31 +287,31 @@ public class ScryptCipherProviderGroovyTest {
SecretKey key = new SecretKeySpec(keyBytes, "AES")
Cipher manualCipher = Cipher.getInstance(encryptionMethod.algorithm, encryptionMethod.provider)
manualCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv))
byte[] recoveredBytes = manualCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
byte[] recoveredBytes = manualCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
assert plaintext.equals(recovered)
}
@Test
public void testGetCipherShouldNotAcceptInvalidSalts() throws Exception {
void testGetCipherShouldNotAcceptInvalidSalts() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword";
final String PASSWORD = "thisIsABadPassword"
final def INVALID_SALTS = ['bad_sal', '$3a$11$', 'x', '$2a$10$', '$400$1$1$abcdefghijklmnopqrstuvwxyz']
final LENGTH_MESSAGE = "The raw salt must be between 8 and 32 bytes"
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Act
INVALID_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true)
}
logger.expected(msg)
@ -321,20 +321,20 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldHandleUnformattedSalts() throws Exception {
void testGetCipherShouldHandleUnformattedSalts() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword";
final String PASSWORD = "thisIsABadPassword"
final def RECOVERABLE_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', '$4$1$1$0123456789abcdef', '$400$1$1$abcdefghijklmnopqrstuv']
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Act
RECOVERABLE_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true)
// Assert
assert cipher
@ -342,16 +342,16 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldRejectEmptySalt() throws Exception {
void testGetCipherShouldRejectEmptySalt() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword";
final String PASSWORD = "thisIsABadPassword"
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
// Act
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true)
}
logger.expected(msg)
@ -360,27 +360,27 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final byte[] IV = Hex.decodeHex("00" * 16 as char[])
final String plaintext = "This is a plaintext message.";
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false)
}
logger.expected(msg)
@ -390,13 +390,13 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final byte[] IV = Hex.decodeHex("01" * 16 as char[])
final String PLAINTEXT = "This is a plaintext message.";
final String PLAINTEXT = "This is a plaintext message."
// Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
@ -407,30 +407,30 @@ public class ScryptCipherProviderGroovyTest {
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true)
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
assert PLAINTEXT.equals(recovered)
}
}
@Test
public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final String PASSWORD = "shortPassword"
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final byte[] IV = Hex.decodeHex("00" * 16 as char[])
final String PLAINTEXT = "This is a plaintext message.";
final String PLAINTEXT = "This is a plaintext message."
// Even though Scrypt can derive keys of arbitrary length, it will fail to validate if the underlying cipher does not support it
final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
@ -443,7 +443,7 @@ public class ScryptCipherProviderGroovyTest {
// Initialize a cipher for encryption
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true)
}
logger.expected(msg)
@ -453,7 +453,7 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testScryptShouldNotAcceptInvalidPassword() {
void testScryptShouldNotAcceptInvalidPassword() {
// Arrange
String badPassword = ""
byte[] salt = [0x01 as byte] * 16
@ -470,9 +470,9 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testGenerateSaltShouldUseProvidedParameters() throws Exception {
void testGenerateSaltShouldUseProvidedParameters() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new ScryptCipherProvider(8, 2, 2);
RandomIVPBECipherProvider cipherProvider = new ScryptCipherProvider(8, 2, 2)
int n = cipherProvider.getN()
int r = cipherProvider.getR()
int p = cipherProvider.getP()
@ -488,7 +488,7 @@ public class ScryptCipherProviderGroovyTest {
}
@Test
public void testShouldParseSalt() throws Exception {
void testShouldParseSalt() throws Exception {
// Arrange
cipherProvider = cipherProvider as ScryptCipherProvider
@ -498,7 +498,7 @@ public class ScryptCipherProviderGroovyTest {
final int EXPECTED_P = 36
final String FORMATTED_SALT = "\$s0\$a0824\$9bgFbqbmbtuNATrEMquiSg"
logger.info("Using salt: ${FORMATTED_SALT}");
logger.info("Using salt: ${FORMATTED_SALT}")
byte[] rawSalt = new byte[16]
def params = []
@ -515,7 +515,7 @@ public class ScryptCipherProviderGroovyTest {
@Ignore("This test can be run on a specific machine to evaluate if the default parameters are sufficient")
@Test
public void testDefaultConstructorShouldProvideStrongParameters() {
void testDefaultConstructorShouldProvideStrongParameters() {
// Arrange
ScryptCipherProvider testCipherProvider = new ScryptCipherProvider()

View File

@ -14,9 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.util.crypto.scrypt
package org.apache.nifi.security.util.scrypt
import org.apache.commons.codec.binary.Hex
import org.apache.nifi.security.util.crypto.scrypt.Scrypt
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assume
@ -35,7 +36,7 @@ import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
@RunWith(JUnit4.class)
public class ScryptGroovyTest {
class ScryptGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(ScryptGroovyTest.class)
private static final String PASSWORD = "shortPassword"
@ -50,7 +51,7 @@ public class ScryptGroovyTest {
private static final long TWO_GIGABYTES = 2048L * 1024 * 1024
@BeforeClass
public static void setUpOnce() throws Exception {
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
@ -59,16 +60,16 @@ public class ScryptGroovyTest {
}
@Before
public void setUp() throws Exception {
void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
void tearDown() throws Exception {
}
@Test
public void testDeriveScryptKeyShouldBeInternallyConsistent() throws Exception {
void testDeriveScryptKeyShouldBeInternallyConsistent() throws Exception {
// Arrange
def allKeys = []
final int RUNS = 10
@ -91,7 +92,7 @@ public class ScryptGroovyTest {
* This test ensures that the local implementation of Scrypt is compatible with the reference implementation from the Colin Percival paper.
*/
@Test
public void testDeriveScryptKeyShouldMatchTestVectors() {
void testDeriveScryptKeyShouldMatchTestVectors() {
// Arrange
// These values are taken from Colin Percival's scrypt paper: https://www.tarsnap.com/scrypt/scrypt.pdf
@ -144,7 +145,7 @@ public class ScryptGroovyTest {
* to the Java options, this overrides any IDE options. To ensure the heap is properly set, using the {@code groovyUnitTest} profile will re-append {@code -Xmx3072m} to the Java options.
*/
@Test
public void testDeriveScryptKeyShouldMatchExpensiveTestVector() {
void testDeriveScryptKeyShouldMatchExpensiveTestVector() {
// Arrange
long totalMemory = Runtime.getRuntime().totalMemory()
logger.info("Required memory: ${TWO_GIGABYTES} bytes")
@ -194,7 +195,7 @@ public class ScryptGroovyTest {
}
@Test
public void testDeriveScryptKeyShouldSupportExternalCompatibility() {
void testDeriveScryptKeyShouldSupportExternalCompatibility() {
// Arrange
// These values can be generated by running `$ ./openssl_scrypt.rb` in the terminal
@ -220,7 +221,7 @@ public class ScryptGroovyTest {
}
@Test
public void testScryptShouldBeInternallyConsistent() throws Exception {
void testScryptShouldBeInternallyConsistent() throws Exception {
// Arrange
def allHashes = []
final int RUNS = 10
@ -240,7 +241,7 @@ public class ScryptGroovyTest {
}
@Test
public void testScryptShouldGenerateValidSaltIfMissing() {
void testScryptShouldGenerateValidSaltIfMissing() {
// Arrange
// The generated salt should be byte[16], encoded as 22 Base64 chars
@ -255,7 +256,7 @@ public class ScryptGroovyTest {
}
@Test
public void testScryptShouldNotAcceptInvalidN() throws Exception {
void testScryptShouldNotAcceptInvalidN() throws Exception {
// Arrange
final int MAX_N = Integer.MAX_VALUE / 128 / R - 1
@ -277,7 +278,7 @@ public class ScryptGroovyTest {
}
@Test
public void testScryptShouldAcceptValidR() throws Exception {
void testScryptShouldAcceptValidR() throws Exception {
// Arrange
// Use a large p value to allow r to exceed MAX_R without normal N exceeding MAX_N
@ -302,7 +303,7 @@ public class ScryptGroovyTest {
}
@Test
public void testScryptShouldNotAcceptInvalidP() throws Exception {
void testScryptShouldNotAcceptInvalidP() throws Exception {
// Arrange
final int MAX_P = Math.ceil(Integer.MAX_VALUE / 128) - 1
@ -324,7 +325,7 @@ public class ScryptGroovyTest {
}
@Test
public void testCheckShouldValidateCorrectPassword() throws Exception {
void testCheckShouldValidateCorrectPassword() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword"
final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
@ -339,7 +340,7 @@ public class ScryptGroovyTest {
}
@Test
public void testCheckShouldNotValidateIncorrectPassword() throws Exception {
void testCheckShouldNotValidateIncorrectPassword() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword"
final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
@ -354,7 +355,7 @@ public class ScryptGroovyTest {
}
@Test
public void testCheckShouldNotAcceptInvalidPassword() throws Exception {
void testCheckShouldNotAcceptInvalidPassword() throws Exception {
// Arrange
final String HASH = '$s0$a0801$abcdefghijklmnopqrstuv$abcdefghijklmnopqrstuv'
@ -376,7 +377,7 @@ public class ScryptGroovyTest {
}
@Test
public void testCheckShouldNotAcceptInvalidHash() throws Exception {
void testCheckShouldNotAcceptInvalidHash() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword"

View File

@ -40,10 +40,6 @@ The following binary components are provided under the Apache Software License v
The following NOTICE information applies:
Copyright 2006 Envoi Solutions LLC
(ASLv2) Jasypt
The following NOTICE information applies:
Copyright (c) 2007-2010, The JASYPT team (http://www.jasypt.org)
(ASLv2) Apache Commons Codec
The following NOTICE information applies:
Apache Commons Codec

View File

@ -99,10 +99,6 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
@ -166,7 +162,11 @@
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>

View File

@ -48,7 +48,6 @@ import org.apache.nifi.web.api.dto.ProcessorConfigDTO;
import org.apache.nifi.web.api.dto.ProcessorDTO;
import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
import org.apache.nifi.web.api.dto.ReportingTaskDTO;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
@ -508,7 +507,7 @@ public class FlowFromDOMFactory {
if (value != null && value.startsWith(FlowSerializer.ENC_PREFIX) && value.endsWith(FlowSerializer.ENC_SUFFIX)) {
try {
return encryptor.decrypt(value.substring(FlowSerializer.ENC_PREFIX.length(), value.length() - FlowSerializer.ENC_SUFFIX.length()));
} catch (EncryptionException | EncryptionOperationNotPossibleException e) {
} catch (EncryptionException e) {
final String moreDescriptiveMessage = "There was a problem decrypting a sensitive flow configuration value. " +
"Check that the nifi.sensitive.props.key value in nifi.properties matches the value used to encrypt the flow.xml.gz file";
logger.error(moreDescriptiveMessage, e);

View File

@ -16,23 +16,43 @@
*/
package org.apache.nifi.encrypt;
import java.nio.charset.StandardCharsets;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.security.kms.CryptoUtils;
import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.security.util.KeyDerivationFunction;
import org.apache.nifi.security.util.crypto.CipherProvider;
import org.apache.nifi.security.util.crypto.CipherProviderFactory;
import org.apache.nifi.security.util.crypto.CipherUtility;
import org.apache.nifi.security.util.crypto.KeyedCipherProvider;
import org.apache.nifi.security.util.crypto.NiFiLegacyCipherProvider;
import org.apache.nifi.security.util.crypto.PBECipherProvider;
import org.apache.nifi.util.NiFiProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.exceptions.EncryptionInitializationException;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* An application specific string encryptor that collects configuration from the
* application properties, system properties, and/or system environment.
* </p>
*
* <p>
* <p>
* Instance of this class are thread-safe</p>
*
* <p>
* <p>
* The encryption provider and algorithm is configured using the application
* properties:
@ -41,95 +61,298 @@ import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
* <li>nifi.sensitive.props.algorithm</li>
* </ul>
* </p>
*
* <p>
* <p>
* The encryptor's password may be set by configuring the below property:
* <ul>
* <li>nifi.sensitive.props.key</li>
* </ul>
* </p>
*
*/
public final class StringEncryptor {
public class StringEncryptor {
private static final Logger logger = LoggerFactory.getLogger(StringEncryptor.class);
private static final List<String> SUPPORTED_ALGORITHMS = new ArrayList<>();
private static final List<String> SUPPORTED_PROVIDERS = new ArrayList<>();
private final String algorithm;
private final String provider;
private final PBEKeySpec password;
private final SecretKeySpec key;
private String encoding = "HEX";
private CipherProvider cipherProvider;
static {
Security.addProvider(new BouncyCastleProvider());
for (EncryptionMethod em : EncryptionMethod.values()) {
SUPPORTED_ALGORITHMS.add(em.getAlgorithm());
}
logger.debug("Supported encryption algorithms: " + StringUtils.join(SUPPORTED_ALGORITHMS, "\n"));
for (Provider provider : Security.getProviders()) {
SUPPORTED_PROVIDERS.add(provider.getName());
}
logger.debug("Supported providers: " + StringUtils.join(SUPPORTED_PROVIDERS, "\n"));
}
public static final String NF_SENSITIVE_PROPS_KEY = "nifi.sensitive.props.key";
public static final String NF_SENSITIVE_PROPS_ALGORITHM = "nifi.sensitive.props.algorithm";
public static final String NF_SENSITIVE_PROPS_PROVIDER = "nifi.sensitive.props.provider";
private static final String DEFAULT_SENSITIVE_PROPS_KEY = "nififtw!";
private static final String TEST_PLAINTEXT = "this is a test";
private final StandardPBEStringEncryptor encryptor;
private StringEncryptor(final String aglorithm, final String provider, final String key) {
encryptor = new StandardPBEStringEncryptor();
encryptor.setAlgorithm(aglorithm);
encryptor.setProviderName(provider);
encryptor.setPassword(key);
encryptor.setStringOutputType("hexadecimal");
encryptor.initialize();
/**
* This constructor creates an encryptor using <em>Password-Based Encryption</em> (PBE). The <em>key</em> value is the direct value provided in <code>nifi.sensitive.props.key</code> in
* <code>nifi.properties</code>, which is a <em>PASSWORD</em> rather than a <em>KEY</em>, but is named such for backward/legacy logical compatibility throughout the rest of the codebase.
* <p>
* For actual raw key provision, see {@link #StringEncryptor(String, String, byte[])}.
*
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
* @param key the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
*/
protected StringEncryptor(final String algorithm, final String provider, final String key) {
this.algorithm = algorithm;
this.provider = provider;
this.key = null;
this.password = new PBEKeySpec(key == null
? DEFAULT_SENSITIVE_PROPS_KEY.toCharArray()
: key.toCharArray());
initialize();
}
/**
* Creates an instance of the nifi sensitive property encryptor. Validates
* that the encryptor is actually working.
* This constructor creates an encryptor using <em>Keyed Encryption</em>. The <em>key</em> value is the raw byte value of a symmetric encryption key
* (usually expressed for human-readability/transmission in hexadecimal or Base64 encoded format).
*
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
* @param key a raw encryption key in bytes
*/
public StringEncryptor(final String algorithm, final String provider, final byte[] key) {
this.algorithm = algorithm;
this.provider = provider;
this.key = new SecretKeySpec(key, extractKeyTypeFromAlgorithm(algorithm));
this.password = null;
initialize();
}
/**
* A default constructor for mocking during testing.
*/
protected StringEncryptor() {
this.algorithm = null;
this.provider = null;
this.key = null;
this.password = null;
}
/**
* Extracts the cipher "family" (i.e. "AES", "DES", "RC4") from the full algorithm name.
*
* @param algorithm the algorithm ({@link EncryptionMethod#algorithm})
* @return the cipher family
* @throws EncryptionException if the algorithm is null/empty or not supported
*/
private String extractKeyTypeFromAlgorithm(String algorithm) throws EncryptionException {
if (StringUtils.isBlank(algorithm)) {
throw new EncryptionException("The algorithm cannot be null or empty");
}
String parsedCipher = CipherUtility.parseCipherFromAlgorithm(algorithm);
if (parsedCipher.equals(algorithm)) {
throw new EncryptionException("No supported algorithm detected");
} else {
return parsedCipher;
}
}
/**
* Creates an instance of the NiFi sensitive property encryptor.
*
* @param niFiProperties properties
* @return encryptor
* @throws EncryptionException if any issues arise initializing or
* validating the encryptor
* validating the encryptor
* @see #createEncryptor(String, String, String)
* @deprecated as of NiFi 1.4.0 because the entire {@link NiFiProperties} object is not necessary to generate the encryptor.
*/
@Deprecated
public static StringEncryptor createEncryptor(final NiFiProperties niFiProperties) throws EncryptionException {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
// Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
final String sensitivePropAlgorithmVal = niFiProperties.getProperty(NF_SENSITIVE_PROPS_ALGORITHM);
final String sensitivePropProviderVal = niFiProperties.getProperty(NF_SENSITIVE_PROPS_PROVIDER);
final String sensitivePropValueNifiPropVar = niFiProperties.getProperty(NF_SENSITIVE_PROPS_KEY, DEFAULT_SENSITIVE_PROPS_KEY);
if (StringUtils.isBlank(sensitivePropAlgorithmVal)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_ALGORITHM + "must bet set");
return createEncryptor(sensitivePropAlgorithmVal, sensitivePropProviderVal, sensitivePropValueNifiPropVar);
}
/**
* Creates an instance of the NiFi sensitive property encryptor.
*
* @param algorithm the encryption (and key derivation) algorithm ({@link EncryptionMethod#algorithm})
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
* @param password the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
* @return the initialized encryptor
*/
public static StringEncryptor createEncryptor(String algorithm, String provider, String password) {
if (StringUtils.isBlank(algorithm)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_ALGORITHM + " must be set");
}
if (StringUtils.isBlank(sensitivePropProviderVal)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_PROVIDER + "must bet set");
if (StringUtils.isBlank(provider)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_PROVIDER + " must be set");
}
if (StringUtils.isBlank(sensitivePropValueNifiPropVar)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_KEY + "must bet set");
if (StringUtils.isBlank(password)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_KEY + " must be set");
}
final StringEncryptor nifiEncryptor;
try {
nifiEncryptor = new StringEncryptor(sensitivePropAlgorithmVal, sensitivePropProviderVal, sensitivePropValueNifiPropVar);
//test that we can infact encrypt and decrypt something
if (!nifiEncryptor.decrypt(nifiEncryptor.encrypt(TEST_PLAINTEXT)).equals(TEST_PLAINTEXT)) {
throw new EncryptionException("NiFi property encryptor does appear to be working - decrypt/encrypt return invalid results");
return new StringEncryptor(algorithm, provider, password);
}
protected void initialize() {
if (isInitialized()) {
logger.debug("Attempted to initialize an already-initialized StringEncryptor");
return;
}
if (paramsAreValid()) {
if (CipherUtility.isPBECipher(algorithm)) {
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NIFI_LEGACY);
} else {
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
}
} catch (final EncryptionInitializationException | EncryptionOperationNotPossibleException ex) {
throw new EncryptionException("Cannot initialize sensitive property encryptor", ex);
} else {
throw new EncryptionException("Cannot initialize the StringEncryptor because some configuration values are invalid");
}
}
private boolean paramsAreValid() {
boolean algorithmAndProviderValid = algorithmIsValid(algorithm) && providerIsValid(provider);
boolean secretIsValid = false;
if (CipherUtility.isPBECipher(algorithm)) {
secretIsValid = passwordIsValid(password);
} else if (CipherUtility.isKeyedCipher(algorithm)) {
secretIsValid = keyIsValid(key, algorithm);
}
return algorithmAndProviderValid && secretIsValid;
}
private boolean keyIsValid(SecretKeySpec key, String algorithm) {
return key != null && CipherUtility.getValidKeyLengthsForAlgorithm(algorithm).contains(key.getEncoded().length * 8);
}
private boolean passwordIsValid(PBEKeySpec password) {
try {
return password.getPassword() != null;
} catch (IllegalStateException | NullPointerException e) {
return false;
}
}
public void setEncoding(String base) {
if ("HEX".equalsIgnoreCase(base)) {
this.encoding = "HEX";
} else if ("BASE64".equalsIgnoreCase(base)) {
this.encoding = "BASE64";
} else {
throw new IllegalArgumentException("The encoding base must be 'HEX' or 'BASE64'");
}
return nifiEncryptor;
}
/**
* Encrypts the given clear text.
*
* @param clearText the message to encrypt
*
* @return the cipher text
*
* @throws EncryptionException if the encrypt fails
*/
public String encrypt(String clearText) throws EncryptionException {
try {
return encryptor.encrypt(clearText);
} catch (final EncryptionOperationNotPossibleException | EncryptionInitializationException eonpe) {
throw new EncryptionException(eonpe);
if (isInitialized()) {
byte[] rawBytes;
if (CipherUtility.isPBECipher(algorithm)) {
rawBytes = encryptPBE(clearText);
} else {
rawBytes = encryptKeyed(clearText);
}
return encode(rawBytes);
} else {
throw new EncryptionException("The encryptor is not initialized");
}
} catch (final Exception e) {
throw new EncryptionException(e);
}
}
private byte[] encryptPBE(String plaintext) {
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
// Generate salt
byte[] salt;
// NiFi legacy code determined the salt length based on the cipher block size
if (pbecp instanceof NiFiLegacyCipherProvider) {
salt = ((NiFiLegacyCipherProvider) pbecp).generateSalt(encryptionMethod);
} else {
salt = pbecp.generateSalt();
}
// Determine necessary key length
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
// Generate cipher
try {
Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, true);
// Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
// byte[] iv = new byte[0];
// if (cipherProvider instanceof RandomIVPBECipherProvider) {
// iv = cipher.getIV();
// }
// Encrypt the plaintext
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine the output
// byte[] rawBytes = CryptoUtils.concatByteArrays(salt, iv, cipherBytes);
return CryptoUtils.concatByteArrays(salt, cipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not encrypt sensitive value", e);
}
}
private byte[] encryptKeyed(String plaintext) {
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
// Generate cipher
try {
SecureRandom sr = new SecureRandom();
byte[] iv = new byte[16];
sr.nextBytes(iv);
Cipher cipher = keyedcp.getCipher(EncryptionMethod.forAlgorithm(algorithm), key, iv, true);
// Encrypt the plaintext
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine the output
return CryptoUtils.concatByteArrays(iv, cipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not encrypt sensitive value", e);
}
}
private String encode(byte[] rawBytes) {
if (this.encoding.equalsIgnoreCase("HEX")) {
return Hex.encodeHexString(rawBytes);
} else {
return Base64.toBase64String(rawBytes);
}
}
@ -137,17 +360,96 @@ public final class StringEncryptor {
* Decrypts the given cipher text.
*
* @param cipherText the message to decrypt
*
* @return the clear text
*
* @throws EncryptionException if the decrypt fails
*/
public String decrypt(String cipherText) throws EncryptionException {
try {
return encryptor.decrypt(cipherText);
} catch (final EncryptionOperationNotPossibleException | EncryptionInitializationException eonpe) {
throw new EncryptionException(eonpe);
if (isInitialized()) {
byte[] plainBytes;
byte[] cipherBytes = decode(cipherText);
if (CipherUtility.isPBECipher(algorithm)) {
plainBytes = decryptPBE(cipherBytes);
} else {
plainBytes = decryptKeyed(cipherBytes);
}
return new String(plainBytes, StandardCharsets.UTF_8);
} else {
throw new EncryptionException("The encryptor is not initialized");
}
} catch (final Exception e) {
throw new EncryptionException(e);
}
}
private byte[] decryptPBE(byte[] cipherBytes) throws DecoderException {
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
// Extract salt
int saltLength = CipherUtility.getSaltLengthForAlgorithm(algorithm);
byte[] salt = new byte[saltLength];
System.arraycopy(cipherBytes, 0, salt, 0, saltLength);
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, saltLength, cipherBytes.length);
// Determine necessary key length
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
// Generate cipher
try {
Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
// Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
// byte[] iv = new byte[0];
// if (cipherProvider instanceof RandomIVPBECipherProvider) {
// iv = cipher.getIV();
// }
// Decrypt the plaintext
return cipher.doFinal(actualCipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not decrypt sensitive value", e);
}
}
private byte[] decryptKeyed(byte[] cipherBytes) {
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
// Generate cipher
try {
int ivLength = 16;
byte[] iv = new byte[ivLength];
System.arraycopy(cipherBytes, 0, iv, 0, ivLength);
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, ivLength, cipherBytes.length);
Cipher cipher = keyedcp.getCipher(EncryptionMethod.forAlgorithm(algorithm), key, iv, false);
// Encrypt the plaintext
return cipher.doFinal(actualCipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not decrypt sensitive value", e);
}
}
private byte[] decode(String encoded) throws DecoderException {
if (this.encoding.equalsIgnoreCase("HEX")) {
return Hex.decodeHex(encoded.toCharArray());
} else {
return Base64.decode(encoded);
}
}
public boolean isInitialized() {
return this.cipherProvider != null;
}
protected static boolean algorithmIsValid(String algorithm) {
return SUPPORTED_ALGORITHMS.contains(algorithm);
}
protected static boolean providerIsValid(String provider) {
return SUPPORTED_PROVIDERS.contains(provider);
}
}

View File

@ -0,0 +1,482 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.nifi.encrypt
import org.apache.commons.codec.binary.Hex
import org.apache.nifi.properties.StandardNiFiProperties
import org.apache.nifi.security.kms.CryptoUtils
import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider
import org.apache.nifi.security.util.crypto.CipherUtility
import org.apache.nifi.security.util.crypto.KeyedCipherProvider
import org.apache.nifi.util.NiFiProperties
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor
import org.jasypt.encryption.pbe.config.PBEConfig
import org.jasypt.salt.SaltGenerator
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
@RunWith(JUnit4.class)
class StringEncryptorTest {
private static final Logger logger = LoggerFactory.getLogger(StringEncryptorTest.class)
private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final List<EncryptionMethod> keyedEncryptionMethods = EncryptionMethod.values().findAll {
it.keyedCipher
}
private static final List<EncryptionMethod> pbeEncryptionMethods = EncryptionMethod.values().findAll {
it.algorithm =~ "PBE"
}
// Unlimited elements are removed in static initializer
private static final List<EncryptionMethod> limitedPbeEncryptionMethods = pbeEncryptionMethods
private static final SecretKey key = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
private static final String KEY = "nifi.sensitive.props.key"
private static final String ALGORITHM = "nifi.sensitive.props.algorithm"
private static final String PROVIDER = "nifi.sensitive.props.provider"
private static final String DEFAULT_ALGORITHM = "PBEWITHMD5AND128BITAES-CBC-OPENSSL"
private static final String DEFAULT_PROVIDER = "BC"
private static final String DEFAULT_PASSWORD = "nififtw!"
private static final String OTHER_PASSWORD = "thisIsABadPassword"
private static
final Map RAW_PROPERTIES = [(ALGORITHM): DEFAULT_ALGORITHM, (PROVIDER): DEFAULT_PROVIDER, (KEY): DEFAULT_PASSWORD]
private static final NiFiProperties STANDARD_PROPERTIES = new StandardNiFiProperties(new Properties(RAW_PROPERTIES))
private static final byte[] DEFAULT_SALT = new byte[8]
private static final byte[] DEFAULT_IV = new byte[16]
private static final int DEFAULT_ITERATION_COUNT = 0
@BeforeClass
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
limitedPbeEncryptionMethods.removeAll { it.algorithm =~ "SHA.*(CBC)?"}
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
private static boolean isUnlimitedStrengthCryptoAvailable() {
Cipher.getMaxAllowedKeyLength("AES") > 128
}
private
static Cipher generatePBECipher(boolean encryptMode, EncryptionMethod em = EncryptionMethod.MD5_128AES, String password = DEFAULT_PASSWORD, byte[] salt = DEFAULT_SALT, int iterationCount = DEFAULT_ITERATION_COUNT) {
// Initialize secret key from password
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray())
final SecretKeyFactory factory = SecretKeyFactory.getInstance(em.algorithm, em.provider)
SecretKey tempKey = factory.generateSecret(pbeKeySpec)
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, iterationCount)
Cipher cipher = Cipher.getInstance(em.algorithm, em.provider)
cipher.init((encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, tempKey, parameterSpec)
cipher
}
private
static Cipher generateKeyedCipher(boolean encryptMode, EncryptionMethod em = EncryptionMethod.MD5_128AES, String keyHex = KEY_HEX, byte[] iv = DEFAULT_IV) {
SecretKey tempKey = new SecretKeySpec(Hex.decodeHex(keyHex as char[]), CipherUtility.parseCipherFromAlgorithm(em.algorithm))
IvParameterSpec ivSpec = new IvParameterSpec(iv)
Cipher cipher = Cipher.getInstance(em.algorithm, em.provider)
cipher.init((encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, tempKey, ivSpec)
cipher
}
@Test
void testPBEncryptionShouldBeInternallyConsistent() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : limitedPbeEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}")
NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
String cipherText = encryptor.encrypt(plaintext)
logger.info("Cipher text: ${cipherText}")
String recovered = encryptor.decrypt(cipherText)
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext == recovered
}
}
@Test
void testPBEncryptionShouldBeExternallyConsistent() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
for (EncryptionMethod em : pbeEncryptionMethods) {
// Hard-coded 0x00 * 16
byte[] salt = new byte[16]
int iterationCount = DEFAULT_ITERATION_COUNT
// DES/RC* algorithms use 8 byte salts and custom iteration counts
if (em.algorithm =~ "DES|RC") {
salt = new byte[8]
iterationCount = 1000
} else if (em.algorithm =~ "SHAA|SHA256") {
// SHA-1/-256 use 16 byte salts but custom iteration counts
iterationCount = 1000
}
logger.info("Using algorithm: ${em.getAlgorithm()} with ${salt.length} byte salt and ${iterationCount} iterations")
// Encrypt the value manually
Cipher cipher = generatePBECipher(true, em, DEFAULT_PASSWORD, salt, iterationCount)
byte[] cipherBytes = cipher.doFinal(plaintext.bytes)
byte[] saltAndCipherBytes = CryptoUtils.concatByteArrays(salt, cipherBytes)
String cipherTextHex = Hex.encodeHexString(saltAndCipherBytes)
logger.info("Cipher text: ${cipherTextHex}")
NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
// Act
String recovered = encryptor.decrypt(cipherTextHex)
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext == recovered
}
}
/**
* This test uses the Jasypt library {@see StandardPBEStringEncryptor} to encrypt raw messages as the legacy (pre-1.4.0) NiFi application did. Then the messages are decrypted with the "new"/current primitive implementation to ensure backward compatibility. This test method only exercises limited strength key sizes (even this is not technically accurate as the SHA KDF is restricted even when using 128-bit AES).
*
* @throws Exception
*/
@Test
void testLimitedPBEncryptionShouldBeConsistentWithLegacyEncryption() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
for (EncryptionMethod em : limitedPbeEncryptionMethods) {
// Hard-coded 0x00 * 16
byte[] salt = new byte[16]
// DES/RC* algorithms use 8 byte salts
if (em.algorithm =~ "DES|RC") {
salt = new byte[8]
}
logger.info("Using algorithm: ${em.getAlgorithm()} with ${salt.length} byte salt")
StandardPBEStringEncryptor legacyEncryptor = new StandardPBEStringEncryptor()
SaltGenerator mockSaltGenerator = [generateSalt: { int l ->
logger.mock("Generating ${l} byte salt")
new byte[l]
}, includePlainSaltInEncryptionResults : {
-> true
}] as SaltGenerator
PBEConfig mockConfig = [getAlgorithm : { -> em.algorithm },
getPassword : { -> DEFAULT_PASSWORD },
getKeyObtentionIterations: { -> 1000 },
getProviderName : { -> em.provider },
getProvider : { -> new BouncyCastleProvider() },
getSaltGenerator : { -> mockSaltGenerator }
] as PBEConfig
legacyEncryptor.setConfig(mockConfig)
legacyEncryptor.setStringOutputType("hexadecimal")
String cipherText = legacyEncryptor.encrypt(plaintext)
logger.info("Cipher text: ${cipherText}")
NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
// Act
String recovered = encryptor.decrypt(cipherText)
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext == recovered
}
}
/**
* This test uses the Jasypt library {@see StandardPBEStringEncryptor} to encrypt raw messages as the legacy (pre-1.4.0) NiFi application did. Then the messages are decrypted with the "new"/current primitive implementation to ensure backward compatibility. This test method exercises all strength key sizes.
*
* @throws Exception
*/
@Test
void testPBEncryptionShouldBeConsistentWithLegacyEncryption() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", isUnlimitedStrengthCryptoAvailable())
final String plaintext = "This is a plaintext message."
for (EncryptionMethod em : pbeEncryptionMethods) {
// Hard-coded 0x00 * 16
byte[] salt = new byte[16]
// DES/RC* algorithms use 8 byte salts
if (em.algorithm =~ "DES|RC") {
salt = new byte[8]
}
logger.info("Using algorithm: ${em.getAlgorithm()} with ${salt.length} byte salt")
StandardPBEStringEncryptor legacyEncryptor = new StandardPBEStringEncryptor()
SaltGenerator mockSaltGenerator = [generateSalt: { int l ->
logger.mock("Generating ${l} byte salt")
new byte[l]
}, includePlainSaltInEncryptionResults : {
-> true
}] as SaltGenerator
PBEConfig mockConfig = [getAlgorithm : { -> em.algorithm },
getPassword : { -> DEFAULT_PASSWORD },
getKeyObtentionIterations: { -> 1000 },
getProviderName : { -> em.provider },
getProvider : { -> new BouncyCastleProvider() },
getSaltGenerator : { -> mockSaltGenerator }
] as PBEConfig
legacyEncryptor.setConfig(mockConfig)
legacyEncryptor.setStringOutputType("hexadecimal")
String cipherText = legacyEncryptor.encrypt(plaintext)
logger.info("Cipher text: ${cipherText}")
NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
// Act
String recovered = encryptor.decrypt(cipherText)
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext == recovered
}
}
@Test
void testKeyedEncryptionShouldBeInternallyConsistent() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : keyedEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}")
StringEncryptor encryptor = new StringEncryptor(em.algorithm, em.provider, Hex.decodeHex(KEY_HEX as char[]))
String cipherText = encryptor.encrypt(plaintext)
logger.info("Cipher text: ${cipherText}")
String recovered = encryptor.decrypt(cipherText)
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext == recovered
}
}
@Test
void testKeyedEncryptionShouldBeExternallyConsistent() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
for (EncryptionMethod em : keyedEncryptionMethods) {
// IV is actually used for keyed encryption
byte[] iv = Hex.decodeHex(("AA" * 16) as char[])
logger.info("Using algorithm: ${em.getAlgorithm()} with ${iv.length} byte IV")
// Encrypt the value manually
Cipher cipher = generateKeyedCipher(true, em, KEY_HEX, iv)
byte[] cipherBytes = cipher.doFinal(plaintext.bytes)
byte[] ivAndCipherBytes = CryptoUtils.concatByteArrays(iv, cipherBytes)
String cipherTextHex = Hex.encodeHexString(ivAndCipherBytes)
logger.info("Cipher text: ${cipherTextHex}")
StringEncryptor encryptor = new StringEncryptor(em.algorithm, em.provider, Hex.decodeHex(KEY_HEX.chars))
// Act
String recovered = encryptor.decrypt(cipherTextHex)
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext == recovered
}
}
@Test
void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final String plaintext = "This is a plaintext message."
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
logger.info("Using algorithm: ${em.getAlgorithm()}")
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, iv, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, key, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", isUnlimitedStrengthCryptoAvailable())
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final List<Integer> LONG_KEY_LENGTHS = [192, 256]
final String plaintext = "This is a plaintext message."
SecureRandom secureRandom = new SecureRandom()
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
// Re-use the same IV for the different length keys to ensure the encryption is different
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
LONG_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${em.getAlgorithm()} with key length ${keyLength}")
// Generate a key
byte[] keyBytes = new byte[keyLength / 8]
secureRandom.nextBytes(keyBytes)
SecretKey localKey = new SecretKeySpec(keyBytes, "AES")
logger.info("Key: ${Hex.encodeHexString(keyBytes)} ${keyBytes.length}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, localKey, iv, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, localKey, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered)
}
}
}
@Test
void testStringEncryptorShouldNotBeFinal() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
StringEncryptor mockEncryptor = [encrypt: { String pt -> pt.reverse() },
decrypt: { String ct -> ct.reverse() }] as StringEncryptor
// Act
String cipherText = mockEncryptor.encrypt(plaintext)
logger.info("Encrypted ${plaintext} to ${cipherText}")
String recovered = mockEncryptor.decrypt(cipherText)
logger.info("Decrypted ${cipherText} to ${recovered}")
// Assert
assert recovered == plaintext
assert cipherText != plaintext
}
@Test
void testStringEncryptorShouldNotOperateIfNotInitialized() throws Exception {
// Arrange
final String plaintext = "This is a plaintext message."
StringEncryptor uninitializedEncryptor = new StringEncryptor()
// Act
def encryptMsg = shouldFail(EncryptionException) {
String cipherText = uninitializedEncryptor.encrypt(plaintext)
logger.info("Encrypted ${plaintext} to ${cipherText}")
}
def decryptMsg = shouldFail(EncryptionException) {
String recovered = uninitializedEncryptor.decrypt(plaintext)
logger.info("Decrypted ${plaintext} to ${recovered}")
}
// Assert
assert encryptMsg =~ "encryptor is not initialized"
assert decryptMsg =~ "encryptor is not initialized"
}
@Test
void testStringEncryptorShouldDetermineIfInitialized() throws Exception {
// Arrange
StringEncryptor uninitializedEncryptor = new StringEncryptor()
EncryptionMethod em = EncryptionMethod.MD5_128AES
StringEncryptor initializedEncryptor = new StringEncryptor(em.algorithm, em.provider, DEFAULT_PASSWORD)
// Act
boolean uninitializedIsInitialized = uninitializedEncryptor.isInitialized()
logger.info("Uninitialized encryptor is initialized: ${uninitializedIsInitialized}")
boolean initializedIsInitialized = initializedEncryptor.isInitialized()
logger.info("Initialized encryptor is initialized: ${initializedIsInitialized}")
// Assert
assert !uninitializedIsInitialized
assert initializedIsInitialized
}
}

View File

@ -30,6 +30,7 @@
<logger name="org.apache.nifi" level="INFO"/>
<logger name="org.apache.nifi.encrypt" level="DEBUG"/>
<logger name="org.apache.nifi.controller.service.mock" level="ERROR"/>
<logger name="org.apache.nifi.controller.service.StandardControllerServiceProvider" level="WARN" />

View File

@ -508,7 +508,7 @@
<exclude>src/test/resources/TestUpdateRecord/schema/person-with-name-string-fields.avsc</exclude>
<exclude>src/test/resources/TestUpdateRecord/schema/name-fields-only.avsc</exclude>
<exclude>src/test/resources/TestUpdateRecord/schema/person-with-name-and-mother.avsc</exclude>
<!-- This file is copied from https://github.com/jeremyh/jBCrypt
<!-- This file is copied from https://github.com/jeremyh/jBCrypt
because the binary is compiled for Java 8 and we must support Java 7 -->
<exclude>src/main/java/org/apache/nifi/security/util/crypto/bcrypt/BCrypt.java</exclude>
</excludes>