http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy deleted file mode 100644 index c154a1f..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy +++ /dev/null @@ -1,399 +0,0 @@ -/* - * 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.processors.standard.util.crypto.scrypt - -import org.apache.commons.codec.binary.Hex -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.After -import org.junit.Assume -import org.junit.Before -import org.junit.BeforeClass -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import java.security.SecureRandom -import java.security.Security - -import static groovy.test.GroovyAssert.shouldFail - -@RunWith(JUnit4.class) -public class ScryptGroovyTest { - private static final Logger logger = LoggerFactory.getLogger(ScryptGroovyTest.class) - - private static final String PASSWORD = "shortPassword" - private static final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210" - private static final byte[] SALT_BYTES = Hex.decodeHex(SALT_HEX as char[]) - - // Small values to test for correctness, not timing - private static final int N = 2**4 - private static final int R = 1 - private static final int P = 1 - private static final int DK_LEN = 128 - private static final long TWO_GIGABYTES = 2048L * 1024 * 1024 - - @BeforeClass - public static void setUpOnce() throws Exception { - Security.addProvider(new BouncyCastleProvider()) - - logger.metaClass.methodMissing = { String name, args -> - logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") - } - } - - @Before - public void setUp() throws Exception { - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testDeriveScryptKeyShouldBeInternallyConsistent() throws Exception { - // Arrange - def allKeys = [] - final int RUNS = 10 - - logger.info("Running with '${PASSWORD}', '${SALT_HEX}', $N, $R, $P, $DK_LEN") - - // Act - RUNS.times { - byte[] keyBytes = Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, N, R, P, DK_LEN) - logger.info("Derived key: ${Hex.encodeHexString(keyBytes)}") - allKeys << keyBytes - } - - // Assert - assert allKeys.size() == RUNS - assert allKeys.every { it == allKeys.first() } - } - - /** - * This test ensures that the local implementation of Scrypt is compatible with the reference implementation from the Colin Percival paper. - */ - @Test - public void testDeriveScryptKeyShouldMatchTestVectors() { - // Arrange - - // These values are taken from Colin Percival's scrypt paper: https://www.tarsnap.com/scrypt/scrypt.pdf - final byte[] HASH_2 = Hex.decodeHex("fdbabe1c9d3472007856e7190d01e9fe" + - "7c6ad7cbc8237830e77376634b373162" + - "2eaf30d92e22a3886ff109279d9830da" + - "c727afb94a83ee6d8360cbdfa2cc0640" as char[]) - - final byte[] HASH_3 = Hex.decodeHex("7023bdcb3afd7348461c06cd81fd38eb" + - "fda8fbba904f8e3ea9b543f6545da1f2" + - "d5432955613f0fcf62d49705242a9af9" + - "e61e85dc0d651e40dfcf017b45575887" as char[]) - - final def TEST_VECTORS = [ - // Empty password is not supported by JCE - [password: "password", - salt : "NaCl", - n : 1024, - r : 8, - p : 16, - dkLen : 64 * 8, - hash : HASH_2], - [password: "pleaseletmein", - salt : "SodiumChloride", - n : 16384, - r : 8, - p : 1, - dkLen : 64 * 8, - hash : HASH_3], - ] - - // Act - TEST_VECTORS.each { Map params -> - logger.info("Running with '${params.password}', '${params.salt}', ${params.n}, ${params.r}, ${params.p}, ${params.dkLen}") - long memoryInBytes = Scrypt.calculateExpectedMemory(params.n, params.r, params.p) - logger.info("Expected memory usage: (128 * r * N + 128 * r * p) ${memoryInBytes} bytes") - logger.info(" Expected ${Hex.encodeHexString(params.hash)}") - - byte[] calculatedHash = Scrypt.deriveScryptKey(params.password.bytes, params.salt.bytes, params.n, params.r, params.p, params.dkLen) - logger.info("Generated ${Hex.encodeHexString(calculatedHash)}") - - // Assert - assert calculatedHash == params.hash - } - } - - /** - * This test ensures that the local implementation of Scrypt is compatible with the reference implementation from the Colin Percival paper. The test vector requires ~1GB {@code byte[]} - * and therefore the Java heap must be at least 1GB. Because {@link nifi/pom.xml} has a {@code surefire} rule which appends {@code -Xmx1G} - * 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() { - // Arrange - long totalMemory = Runtime.getRuntime().totalMemory() - logger.info("Required memory: ${TWO_GIGABYTES} bytes") - logger.info("Max heap memory: ${totalMemory} bytes") - Assume.assumeTrue("Test is being skipped due to JVM heap size. Please run with -Xmx3072m to set sufficient heap size", - totalMemory >= TWO_GIGABYTES) - - // These values are taken from Colin Percival's scrypt paper: https://www.tarsnap.com/scrypt/scrypt.pdf - final byte[] HASH = Hex.decodeHex("2101cb9b6a511aaeaddbbe09cf70f881" + - "ec568d574a2ffd4dabe5ee9820adaa47" + - "8e56fd8f4ba5d09ffa1c6d927c40f4c3" + - "37304049e8a952fbcbf45c6fa77a41a4" as char[]) - - // This test vector requires 2GB heap space and approximately 10 seconds on a consumer machine - String password = "pleaseletmein" - String salt = "SodiumChloride" - int n = 1048576 - int r = 8 - int p = 1 - int dkLen = 64 * 8 - - // Act - logger.info("Running with '${password}', '${salt}', ${n}, ${r}, ${p}, ${dkLen}") - long memoryInBytes = Scrypt.calculateExpectedMemory(n, r, p) - logger.info("Expected memory usage: (128 * r * N + 128 * r * p) ${memoryInBytes} bytes") - logger.info(" Expected ${Hex.encodeHexString(HASH)}") - - byte[] calculatedHash = Scrypt.deriveScryptKey(password.bytes, salt.bytes, n, r, p, dkLen) - logger.info("Generated ${Hex.encodeHexString(calculatedHash)}") - - // Assert - assert calculatedHash == HASH - } - - @Ignore("This test was just to exercise the heap and debug OOME issues") - @Test - void testShouldCauseOutOfMemoryError() { - SecureRandom secureRandom = new SecureRandom() -// int i = 29 - (10..31).each { int i -> - int length = 2**i - byte[] bytes = new byte[length] - secureRandom.nextBytes(bytes) - logger.info("Successfully ran with byte[] of length ${length}") - logger.info("${Hex.encodeHexString(bytes[0..<16] as byte[])}...") - } - } - - @Test - public void testDeriveScryptKeyShouldSupportExternalCompatibility() { - // Arrange - - // These values can be generated by running `$ ./openssl_scrypt.rb` in the terminal - final String EXPECTED_KEY_HEX = "a8efbc0a709d3f89b6bb35b05fc8edf5" - String password = "thisIsABadPassword" - String saltHex = "f5b8056ea6e66edb8d013ac432aba24a" - int n = 1024 - int r = 8 - int p = 36 - int dkLen = 16 * 8 - - // Act - logger.info("Running with '${password}', ${saltHex}, ${n}, ${r}, ${p}, ${dkLen}") - long memoryInBytes = Scrypt.calculateExpectedMemory(n, r, p) - logger.info("Expected memory usage: (128 * r * N + 128 * r * p) ${memoryInBytes} bytes") - logger.info(" Expected ${EXPECTED_KEY_HEX}") - - byte[] calculatedHash = Scrypt.deriveScryptKey(password.bytes, Hex.decodeHex(saltHex as char[]), n, r, p, dkLen) - logger.info("Generated ${Hex.encodeHexString(calculatedHash)}") - - // Assert - assert calculatedHash == Hex.decodeHex(EXPECTED_KEY_HEX as char[]) - } - - @Test - public void testScryptShouldBeInternallyConsistent() throws Exception { - // Arrange - def allHashes = [] - final int RUNS = 10 - - logger.info("Running with '${PASSWORD}', '${SALT_HEX}', $N, $R, $P") - - // Act - RUNS.times { - String hash = Scrypt.scrypt(PASSWORD, SALT_BYTES, N, R, P, DK_LEN) - logger.info("Hash: ${hash}") - allHashes << hash - } - - // Assert - assert allHashes.size() == RUNS - assert allHashes.every { it == allHashes.first() } - } - - @Test - public void testScryptShouldGenerateValidSaltIfMissing() { - // Arrange - - // The generated salt should be byte[16], encoded as 22 Base64 chars - final def EXPECTED_SALT_PATTERN = /\$.+\$[0-9a-zA-Z\/\+]{22}\$.+/ - - // Act - String calculatedHash = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN) - logger.info("Generated ${calculatedHash}") - - // Assert - assert calculatedHash =~ EXPECTED_SALT_PATTERN - } - - @Test - public void testScryptShouldNotAcceptInvalidN() throws Exception { - // Arrange - - final int MAX_N = Integer.MAX_VALUE / 128 / R - 1 - - // N must be a power of 2 > 1 and < Integer.MAX_VALUE / 128 / r - final def INVALID_NS = [-2, 0, 1, 3, 4096 - 1, MAX_N + 1] - - // Act - INVALID_NS.each { int invalidN -> - logger.info("Using N: ${invalidN}") - - def msg = shouldFail(IllegalArgumentException) { - Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, invalidN, R, P, DK_LEN) - } - - // Assert - assert msg =~ "N must be a power of 2 greater than 1|Parameter N is too large" - } - } - - @Test - public void testScryptShouldAcceptValidR() throws Exception { - // Arrange - - // Use a large p value to allow r to exceed MAX_R without normal N exceeding MAX_N - int largeP = 2**10 - final int MAX_R = Math.ceil(Integer.MAX_VALUE / 128 / largeP) - 1 - - // r must be in (0..Integer.MAX_VALUE / 128 / p) - final def INVALID_RS = [0, MAX_R + 1] - - // Act - INVALID_RS.each { int invalidR -> - logger.info("Using r: ${invalidR}") - - def msg = shouldFail(IllegalArgumentException) { - byte[] hash = Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, N, invalidR, largeP, DK_LEN) - logger.info("Generated hash: ${Hex.encodeHexString(hash)}") - } - - // Assert - assert msg =~ "Parameter r must be 1 or greater|Parameter r is too large" - } - } - - @Test - public void testScryptShouldNotAcceptInvalidP() throws Exception { - // Arrange - final int MAX_P = Math.ceil(Integer.MAX_VALUE / 128) - 1 - - // p must be in (0..Integer.MAX_VALUE / 128) - final def INVALID_PS = [0, MAX_P + 1] - - // Act - INVALID_PS.each { int invalidP -> - logger.info("Using p: ${invalidP}") - - def msg = shouldFail(IllegalArgumentException) { - byte[] hash = Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, N, R, invalidP, DK_LEN) - logger.info("Generated hash: ${Hex.encodeHexString(hash)}") - } - - // Assert - assert msg =~ "Parameter p must be 1 or greater|Parameter p is too large" - } - } - - @Test - public void testCheckShouldValidateCorrectPassword() throws Exception { - // Arrange - final String PASSWORD = "thisIsABadPassword" - final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN) - logger.info("Password: ${PASSWORD} -> Hash: ${EXPECTED_HASH}") - - // Act - boolean matches = Scrypt.check(PASSWORD, EXPECTED_HASH) - logger.info("Check matches: ${matches}") - - // Assert - assert matches - } - - @Test - public void testCheckShouldNotValidateIncorrectPassword() throws Exception { - // Arrange - final String PASSWORD = "thisIsABadPassword" - final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN) - logger.info("Password: ${PASSWORD} -> Hash: ${EXPECTED_HASH}") - - // Act - boolean matches = Scrypt.check(PASSWORD.reverse(), EXPECTED_HASH) - logger.info("Check matches: ${matches}") - - // Assert - assert !matches - } - - @Test - public void testCheckShouldNotAcceptInvalidPassword() throws Exception { - // Arrange - final String HASH = '$s0$a0801$abcdefghijklmnopqrstuv$abcdefghijklmnopqrstuv' - - // Even though the spec allows for empty passwords, the JCE does not, so extend enforcement of that to the user boundary - final def INVALID_PASSWORDS = ['', null] - - // Act - INVALID_PASSWORDS.each { String invalidPassword -> - logger.info("Using password: ${invalidPassword}") - - def msg = shouldFail(IllegalArgumentException) { - boolean matches = Scrypt.check(invalidPassword, HASH) - } - logger.expected(msg) - - // Assert - assert msg =~ "Password cannot be empty" - } - } - - @Test - public void testCheckShouldNotAcceptInvalidHash() throws Exception { - // Arrange - final String PASSWORD = "thisIsABadPassword" - - // Even though the spec allows for empty salts, the JCE does not, so extend enforcement of that to the user boundary - final def INVALID_HASHES = ['', null, '$s0$a0801$', '$s0$a0801$abcdefghijklmnopqrstuv$'] - - // Act - INVALID_HASHES.each { String invalidHash -> - logger.info("Using hash: ${invalidHash}") - - def msg = shouldFail(IllegalArgumentException) { - boolean matches = Scrypt.check(PASSWORD, invalidHash) - } - logger.expected(msg) - - // Assert - assert msg =~ "Hash cannot be empty|Hash is not properly formatted" - } - } -} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy new file mode 100644 index 0000000..e5e001f --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy @@ -0,0 +1,538 @@ +/* + * 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.security.util.crypto + +import org.apache.commons.codec.binary.Base64 +import org.apache.commons.codec.binary.Hex +import org.apache.nifi.security.util.EncryptionMethod +import org.apache.nifi.security.util.crypto.bcrypt.BCrypt +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Ignore +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.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import java.security.Security + +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); + + private static List<EncryptionMethod> strongKDFEncryptionMethods + + 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()); + + strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() } + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + + if (PasswordBasedEncryptor.supportsUnlimitedStrength()) { + AES_KEY_LENGTHS = [128, 192, 256] + } else { + AES_KEY_LENGTHS = [128] + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testGetCipherShouldBeInternallyConsistent() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + final String PASSWORD = "shortPassword"; + final byte[] SALT = cipherProvider.generateSalt() + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : strongKDFEncryptionMethods) { + 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(); + logger.info("IV: ${Hex.encodeHexString(iv)}") + + 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"); + logger.info("Recovered: ${recovered}") + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + final String PASSWORD = "shortPassword"; + final byte[] SALT = cipherProvider.generateSalt() + final byte[] IV = Hex.decodeHex("01" * 16 as char[]); + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : strongKDFEncryptionMethods) { + logger.info("Using algorithm: ${em.getAlgorithm()}"); + + // Initialize a cipher for encryption + 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}"); + + 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); + } + } + + @Test + public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception { + // Arrange + Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", + PasswordBasedEncryptor.supportsUnlimitedStrength()); + + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + final String PASSWORD = "shortPassword"; + final byte[] SALT = cipherProvider.generateSalt() + + final int LONG_KEY_LENGTH = 256 + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : strongKDFEncryptionMethods) { + 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(); + logger.info("IV: ${Hex.encodeHexString(iv)}") + + 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"); + logger.info("Recovered: ${recovered}") + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testHashPWShouldMatchTestVectors() { + // Arrange + final String PASSWORD = 'abcdefghijklmnopqrstuvwxyz' + final String SALT = '$2a$10$fVH8e28OQRj9tqiDXs1e1u' + final String EXPECTED_HASH = '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq' +// final int WORK_FACTOR = 10 + + // Act + String calculatedHash = BCrypt.hashpw(PASSWORD, SALT) + logger.info("Generated ${calculatedHash}") + + // Assert + assert calculatedHash == EXPECTED_HASH + } + + @Test + public void testGetCipherShouldSupportExternalCompatibility() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + 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[]); + + // $v2$w2$base64_salt_22__base64_hash_31 + final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi" + logger.info("Full Hash: ${FULL_HASH}") + final String HASH = FULL_HASH[-31..-1] + logger.info(" Hash: ${HASH.padLeft(60, " ")}") + logger.info(" B64 Salt: ${CipherUtility.encodeBase64NoPadding(SALT).padLeft(29, " ")}") + + String extractedSalt = FULL_HASH[7..<29] + logger.info("Extracted Salt: ${extractedSalt}") + String extractedSaltHex = Hex.encodeHexString(Base64.decodeBase64(extractedSalt)) + logger.info("Extracted Salt (hex): ${extractedSaltHex}") + logger.info(" Expected Salt (hex): ${Hex.encodeHexString(SALT)}") + + final String CIPHER_TEXT = "3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15" + 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}"); + + // Sanity check + Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC") + def rubyKey = new SecretKeySpec(Hex.decodeHex("724cd9e1b0b9e87c7f7e7d7b270bca07" as char[]), "AES") + def ivSpec = new IvParameterSpec(IV) + rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec) + byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes) + logger.info("Expected cipher text: ${Hex.encodeHexString(rubyCipherBytes)}") + rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec) + assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes + assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes + logger.sanity("Decrypted external cipher text and generated cipher text successfully") + + // Sanity for hash generation + final String FULL_SALT = FULL_HASH[0..<29] + logger.sanity("Salt from external: ${FULL_SALT}") + String generatedHash = BCrypt.hashpw(PASSWORD, FULL_SALT) + logger.sanity("Generated hash: ${generatedHash}") + 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"); + logger.info("Recovered: ${recovered}") + + // Assert + assert PLAINTEXT.equals(recovered); + } + + @Test + public void testGetCipherShouldHandleFullSalt() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + 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[]); + + // $v2$w2$base64_salt_22__base64_hash_31 + final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi" + logger.info("Full Hash: ${FULL_HASH}") + final String FULL_SALT = FULL_HASH[0..<29] + logger.info(" Salt: ${FULL_SALT}") + final String HASH = FULL_HASH[-31..-1] + logger.info(" Hash: ${HASH.padLeft(60, " ")}") + + String extractedSalt = FULL_HASH[7..<29] + logger.info("Extracted Salt: ${extractedSalt}") + String extractedSaltHex = Hex.encodeHexString(Base64.decodeBase64(extractedSalt)) + logger.info("Extracted Salt (hex): ${extractedSaltHex}") + + final String CIPHER_TEXT = "3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15" + 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}"); + + // 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"); + logger.info("Recovered: ${recovered}") + + // Assert + assert PLAINTEXT.equals(recovered); + } + + @Test + public void testGetCipherShouldHandleUnformedSalt() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + 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()}"); + + // 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); + } + + // Assert + assert msg =~ "The salt must be of the format \\\$2a\\\$10\\\$gUVbkVzp79H8YaCOsCVZNu\\. To generate a salt, use BcryptCipherProvider#generateSalt" + } + } + + String bytesToBitString(byte[] bytes) { + bytes.collect { + String.format("%8s", Integer.toBinaryString(it & 0xFF)).replace(' ', '0') + }.join("") + } + + String spaceString(String input, int blockSize = 4) { + input.collect { it.padLeft(blockSize, " ") }.join("") + } + + @Test + public void testGetCipherShouldRejectEmptySalt() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + final String PASSWORD = "thisIsABadPassword"; + + EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC + 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); + } + logger.expected(msg) + + // Assert + assert msg =~ "The salt cannot be empty\\. To generate a salt, use BcryptCipherProvider#generateSalt" + } + + @Test + public void testGetCipherForDecryptShouldRequireIV() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4) + + final String PASSWORD = "shortPassword"; + final byte[] SALT = cipherProvider.generateSalt() + final byte[] IV = Hex.decodeHex("00" * 16 as char[]); + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : strongKDFEncryptionMethods) { + logger.info("Using algorithm: ${em.getAlgorithm()}"); + + // Initialize a cipher for encryption + 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}"); + + def msg = shouldFail(IllegalArgumentException) { + cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false); + } + + // Assert + assert msg =~ "Cannot decrypt without a valid IV" + } + } + + @Test + public void testGetCipherShouldAcceptValidKeyLengths() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = cipherProvider.generateSalt() + final byte[] IV = Hex.decodeHex("01" * 16 as char[]); + + 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 + EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC + + // Act + VALID_KEY_LENGTHS.each { int keyLength -> + 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); + logger.info("IV: ${Hex.encodeHexString(IV)}") + + 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"); + logger.info("Recovered: ${recovered}") + + // Assert + assert PLAINTEXT.equals(recovered); + } + } + + @Test + public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = cipherProvider.generateSalt() + final byte[] IV = Hex.decodeHex("00" * 16 as char[]); + + 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] + EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC + + // Act + INVALID_KEY_LENGTHS.each { int keyLength -> + logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}") + + // Initialize a cipher for encryption + def msg = shouldFail(IllegalArgumentException) { + Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true); + } + + // Assert + assert msg =~ "${keyLength} is not a valid key length for AES" + } + } + + @Test + public void testGenerateSaltShouldUseProvidedWorkFactor() throws Exception { + // Arrange + RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(11); + int workFactor = cipherProvider.getWorkFactor() + + // Act + final byte[] saltBytes = cipherProvider.generateSalt() + String salt = new String(saltBytes) + logger.info("Salt: ${salt}") + + // Assert + assert salt =~ /^\$2[axy]\$\d{2}\$/ + assert salt.contains("\$${workFactor}\$") + } + + @Ignore("This test can be run on a specific machine to evaluate if the default work factor is sufficient") + @Test + public void testDefaultConstructorShouldProvideStrongWorkFactor() { + // Arrange + 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 + + // Calculate the work factor to reach 500 ms + int minimumWorkFactor = calculateMinimumWorkFactor() + logger.info("Determined minimum safe work factor to be ${minimumWorkFactor}") + + // Act + int workFactor = cipherProvider.getWorkFactor() + logger.info("Default work factor ${workFactor}") + + // Assert + assertTrue("The default work factor for BcryptCipherProvider is too weak. Please update the default value to a stronger level.", workFactor >= minimumWorkFactor) + } + + /** + * Returns the work factor required for a derivation to exceed 500 ms on this machine. Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt + * + * @return the minimum bcrypt work factor + */ + private static int calculateMinimumWorkFactor() { + // High start-up cost, so run multiple times for better benchmarking + final int RUNS = 10 + + // Benchmark using a work factor of 5 (the second-lowest allowed) + int workFactor = 5 + + String salt = BCrypt.gensalt(workFactor) + + // Run once to prime the system + double duration = time { + BCrypt.hashpw(MICROBENCHMARK, salt) + } + logger.info("First run of work factor ${workFactor} took ${duration} ms (ignored)") + + def durations = [] + + RUNS.times { int i -> + duration = time { + BCrypt.hashpw(MICROBENCHMARK, salt) + } + logger.info("Work factor ${workFactor} took ${duration} ms") + durations << duration + } + + duration = durations.sum() / durations.size() + logger.info("Work factor ${workFactor} averaged ${duration} ms") + + // Increasing the work factor by 1 would double the run time + // Keep increasing N until the estimated duration is over 500 ms + while (duration < 500) { + workFactor += 1 + duration *= 2 + } + + logger.info("Returning work factor ${workFactor} for ${duration} ms") + + return workFactor + } + + private static double time(Closure c) { + long start = System.nanoTime() + c.call() + long end = System.nanoTime() + return (end - start) / 1_000_000.0 + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy new file mode 100644 index 0000000..28da9d1 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy @@ -0,0 +1,97 @@ +/* + * 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.security.util.crypto + +import org.apache.nifi.security.util.KeyDerivationFunction +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.security.Security + +@RunWith(JUnit4.class) +class CipherProviderFactoryGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactoryGroovyTest.class) + + private static final Map<KeyDerivationFunction, Class> EXPECTED_CIPHER_PROVIDERS = [ + (KeyDerivationFunction.BCRYPT) : BcryptCipherProvider.class, + (KeyDerivationFunction.NIFI_LEGACY) : NiFiLegacyCipherProvider.class, + (KeyDerivationFunction.NONE) : AESKeyedCipherProvider.class, + (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): OpenSSLPKCS5CipherProvider.class, + (KeyDerivationFunction.PBKDF2) : PBKDF2CipherProvider.class, + (KeyDerivationFunction.SCRYPT) : ScryptCipherProvider.class + ] + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testGetCipherProviderShouldResolveRegisteredKDFs() { + // Arrange + + // Act + KeyDerivationFunction.values().each { KeyDerivationFunction kdf -> + logger.info("Expected: ${kdf.name} -> ${EXPECTED_CIPHER_PROVIDERS.get(kdf).simpleName}") + CipherProvider cp = CipherProviderFactory.getCipherProvider(kdf) + logger.info("Resolved: ${kdf.name} -> ${cp.class.simpleName}") + + // Assert + assert cp.class == (EXPECTED_CIPHER_PROVIDERS.get(kdf)) + } + } + + @Ignore("Cannot mock enum using Groovy map coercion") + @Test + public void testGetCipherProviderShouldHandleUnregisteredKDFs() { + // Arrange + + // Can't mock this; see http://stackoverflow.com/questions/5323505/mocking-java-enum-to-add-a-value-to-test-fail-case + KeyDerivationFunction invalidKDF = [name: "Unregistered", description: "Not a registered KDF"] as KeyDerivationFunction + logger.info("Expected: ${invalidKDF.name} -> error") + + // Act + def msg = shouldFail(IllegalArgumentException) { + CipherProvider cp = CipherProviderFactory.getCipherProvider(invalidKDF) + logger.info("Resolved: ${invalidKDF.name} -> ${cp.class.simpleName}") + } + logger.expected(msg) + + // Assert + assert msg =~ "No cipher provider registered for ${invalidKDF.name}" + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy new file mode 100644 index 0000000..0487af3 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy @@ -0,0 +1,122 @@ +/* + * 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.security.util.crypto + +import org.apache.commons.codec.binary.Hex +import org.apache.nifi.processor.io.StreamCallback +import org.apache.nifi.security.util.EncryptionMethod +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.security.Security + +public class KeyedEncryptorGroovyTest { + private static final Logger logger = LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class) + + private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/" + private static final File plainFile = new File("${TEST_RESOURCES_PREFIX}/plain.txt") + private static final File encryptedFile = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.asc") + + private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" + private static final SecretKey KEY = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES") + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testShouldEncryptAndDecrypt() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext message." + logger.info("Plaintext: {}", PLAINTEXT) + InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) + + OutputStream cipherStream = new ByteArrayOutputStream() + OutputStream recoveredStream = new ByteArrayOutputStream() + + EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC + logger.info("Using ${encryptionMethod.name()}") + + // Act + KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY) + + StreamCallback encryptionCallback = encryptor.getEncryptionCallback() + StreamCallback decryptionCallback = encryptor.getDecryptionCallback() + + encryptionCallback.process(plainStream, cipherStream) + + final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() + logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes)) + InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) + decryptionCallback.process(cipherInputStream, recoveredStream) + + // Assert + byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() + String recovered = new String(recoveredBytes, "UTF-8") + logger.info("Recovered: {}\n\n", recovered) + assert PLAINTEXT.equals(recovered) + } + + @Test + public void testShouldDecryptOpenSSLUnsaltedCipherTextWithKnownIV() throws Exception { + // Arrange + final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text + logger.info("Plaintext: {}", PLAINTEXT) + byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes + + final String keyHex = "711E85689CE7AFF6F410AEA43ABC5446" + final String ivHex = "842F685B84879B2E00F977C22B9E9A7D" + + InputStream cipherStream = new ByteArrayInputStream(cipherBytes) + OutputStream recoveredStream = new ByteArrayOutputStream() + + final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC + KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, new SecretKeySpec(Hex.decodeHex(keyHex as char[]), "AES"), Hex.decodeHex(ivHex as char[])) + + StreamCallback decryptionCallback = encryptor.getDecryptionCallback() + logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") + + // Act + decryptionCallback.process(cipherStream, recoveredStream) + + // Assert + byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() + String recovered = new String(recoveredBytes, "UTF-8") + logger.info("Recovered: {}", recovered) + assert PLAINTEXT.equals(recovered) + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy new file mode 100644 index 0000000..2a3d456 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy @@ -0,0 +1,299 @@ +/* + * 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.security.util.crypto + +import org.apache.commons.codec.binary.Hex +import org.apache.nifi.security.util.EncryptionMethod +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Ignore +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.PBEKeySpec +import javax.crypto.spec.PBEParameterSpec +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); + + 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 byte[] SALT_16_BYTES = Hex.decodeHex("aabbccddeeff00112233445566778899".toCharArray()); + + @BeforeClass + public 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 { + } + + @After + public 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 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()); + } + + return null; + } + + @Test + public void testGetCipherShouldBeInternallyConsistent() throws Exception { + // Arrange + NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider(); + + final String PASSWORD = "shortPassword"; + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod encryptionMethod : limitedStrengthPbeEncryptionMethods) { + 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") + continue + } + + 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); + + 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception { + // Arrange + Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", + PasswordBasedEncryptor.supportsUnlimitedStrength()); + + NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider(); + + final String PASSWORD = "shortPassword"; + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod encryptionMethod : pbeEncryptionMethods) { + 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); + + 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherShouldSupportLegacyCode() throws Exception { + // Arrange + NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider(); + + final String PASSWORD = "short"; + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod encryptionMethod : limitedStrengthPbeEncryptionMethods) { + 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") + continue + } + + byte[] salt = cipherProvider.generateSalt(encryptionMethod) + logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})") + + // Initialize a legacy cipher for encryption + Cipher legacyCipher = getLegacyCipher(PASSWORD, salt, encryptionMethod.getAlgorithm()); + + 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception { + // Arrange + NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider(); + + final String PASSWORD = "short"; + final byte[] SALT = new byte[0]; + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) { + 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") + continue + } + + // Initialize a legacy cipher for encryption + Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm()); + + 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherShouldIgnoreKeyLength() throws Exception { + // Arrange + NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider(); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = SALT_16_BYTES + + 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); + + // 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + /** + * This test determines for each PBE encryption algorithm if it actually requires the JCE unlimited strength jurisdiction policies to be installed. + * Even some algorithms that use 128-bit keys (which should be allowed on all systems) throw exceptions because BouncyCastle derives the key + * from the password using a long digest result at the time of key length checking. + * @throws IOException + */ + @Ignore("Only needed once to determine max supported password lengths") + @Test + public void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() throws IOException { + def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") } + + boolean unlimitedCryptoSupported = PasswordBasedEncryptor.supportsUnlimitedStrength() + logger.info("This JVM supports unlimited strength crypto: ${unlimitedCryptoSupported}") + + def longestSupportedPasswordByEM = [:] + + encryptionMethods.each { EncryptionMethod encryptionMethod -> + logger.info("Attempting ${encryptionMethod.name()} (${encryptionMethod.algorithm}) which claims unlimited strength required: ${encryptionMethod.unlimitedStrength}") + + (1..20).find { int length -> + String password = "x" * length + + try { + NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider(); + Cipher cipher = cipherProvider.getCipher(encryptionMethod, password, true) + return false + } catch (Exception e) { + logger.error("Unable to create the cipher with ${encryptionMethod.algorithm} and password ${password} (${password.length()}) due to ${e.getMessage()}") + if (!longestSupportedPasswordByEM.containsKey(encryptionMethod)) { + longestSupportedPasswordByEM.put(encryptionMethod, password.length() - 1) + } + return true + } + } + logger.info("\n") + } + + logger.info("Longest supported password by encryption method:") + longestSupportedPasswordByEM.each { EncryptionMethod encryptionMethod, int length -> + logger.info("\t${encryptionMethod.algorithm}\t${length}") + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy new file mode 100644 index 0000000..62b7970 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy @@ -0,0 +1,323 @@ +/* + * 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.security.util.crypto + +import org.apache.commons.codec.binary.Hex +import org.apache.nifi.security.util.EncryptionMethod +import org.bouncycastle.jce.provider.BouncyCastleProvider +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.PBEKeySpec +import javax.crypto.spec.PBEParameterSpec +import java.security.Security + +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); + + 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; + + @BeforeClass + public 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 { + } + + @After + public 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 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()); + } + + return null; + } + + @Test + public void testGetCipherShouldBeInternallyConsistent() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "short"; + final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray()); + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) { + 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") + continue + } + + // Initialize a cipher for encryption + 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); + + cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false); + byte[] recoveredBytes = cipher.doFinal(cipherBytes); + String recovered = new String(recoveredBytes, "UTF-8"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception { + // Arrange + Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", + PasswordBasedEncryptor.supportsUnlimitedStrength()); + + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray()); + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : pbeEncryptionMethods) { + logger.info("Using algorithm: {}", em.getAlgorithm()); + + // Initialize a cipher for encryption + 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); + + cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false); + byte[] recoveredBytes = cipher.doFinal(cipherBytes); + String recovered = new String(recoveredBytes, "UTF-8"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherShouldSupportLegacyCode() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray()); + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) { + 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") + continue + } + + // Initialize a legacy cipher for encryption + Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm()); + + 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "short"; + final byte[] SALT = new byte[0]; + + final String plaintext = "This is a plaintext message."; + + // Act + for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) { + 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") + continue + } + + // Initialize a legacy cipher for encryption + Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm()); + + 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherShouldIgnoreKeyLength() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray()); + + 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); + + // 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"); + + // Assert + assert plaintext.equals(recovered); + } + } + + @Test + public void testGetCipherShouldRequireEncryptionMethod() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray()); + + // Act + logger.info("Using algorithm: null"); + + def msg = shouldFail(IllegalArgumentException) { + Cipher providedCipher = cipherProvider.getCipher(null, PASSWORD, SALT, false); + } + + // Assert + assert msg =~ "The encryption method must be specified" + } + + @Test + public void testGetCipherShouldRequirePassword() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray()); + EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES + + // Act + logger.info("Using algorithm: ${encryptionMethod}"); + + def msg = shouldFail(IllegalArgumentException) { + Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, "", SALT, false); + } + + // Assert + assert msg =~ "Encryption with an empty password is not supported" + } + + @Test + public void testGetCipherShouldValidateSaltLength() throws Exception { + // Arrange + OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider(); + + final String PASSWORD = "shortPassword"; + final byte[] SALT = Hex.decodeHex("00112233445566".toCharArray()); + EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES + + // Act + logger.info("Using algorithm: ${encryptionMethod}"); + + def msg = shouldFail(IllegalArgumentException) { + Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, false); + } + + // Assert + assert msg =~ "Salt must be 8 bytes US-ASCII encoded" + } + + @Test + public void testGenerateSaltShouldProvideValidSalt() throws Exception { + // Arrange + PBECipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider() + + // Act + byte[] salt = cipherProvider.generateSalt() + logger.info("Checking salt ${Hex.encodeHexString(salt)}") + + // Assert + assert salt.length == cipherProvider.getDefaultSaltLength() + assert salt != [(0x00 as byte) * cipherProvider.defaultSaltLength] + } +} \ No newline at end of file
