Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id D10BF200CA3 for ; Tue, 2 May 2017 19:27:29 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id CFA55160BAB; Tue, 2 May 2017 17:27:29 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 59F14160BBF for ; Tue, 2 May 2017 19:27:27 +0200 (CEST) Received: (qmail 29811 invoked by uid 500); 2 May 2017 17:27:26 -0000 Mailing-List: contact commits-help@nifi.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@nifi.apache.org Delivered-To: mailing list commits@nifi.apache.org Received: (qmail 29613 invoked by uid 99); 2 May 2017 17:27:26 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 02 May 2017 17:27:26 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 455D4E1794; Tue, 2 May 2017 17:27:26 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: alopresto@apache.org To: commits@nifi.apache.org Date: Tue, 02 May 2017 17:27:29 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [04/13] nifi git commit: NIFI-3594 Implemented encrypted provenance repository. Added src/test/resources/logback-test.xml files resetting log level from DEBUG (in nifi-data-provenance-utils) to WARN because later tests depend on MockComponentLog recordin archived-at: Tue, 02 May 2017 17:27:30 -0000 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/NiFiLegacyCipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy deleted file mode 100644 index 176e61b..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy +++ /dev/null @@ -1,294 +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 - -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.security.util.EncryptionMethod -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.* -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 pbeEncryptionMethods = new ArrayList<>(); - private static List 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/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy deleted file mode 100644 index 31cbd5a..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy +++ /dev/null @@ -1,319 +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 - -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.security.util.EncryptionMethod -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.* -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 pbeEncryptionMethods = new ArrayList<>(); - private static List 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 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/PBKDF2CipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProviderGroovyTest.groovy deleted file mode 100644 index dfcd0da..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProviderGroovyTest.groovy +++ /dev/null @@ -1,540 +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 - -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.security.util.EncryptionMethod -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.* -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.crypto.Cipher -import java.security.Security - -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); - - private static List strongKDFEncryptionMethods - - public static final String MICROBENCHMARK = "microbenchmark" - 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" - private final String IV_HEX = "01" * 16 - private static ArrayList 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 PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT); - - final String PASSWORD = "shortPassword"; - final byte[] SALT = Hex.decodeHex(SALT_HEX 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, 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 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 def INVALID_IVS = (0..15).collect { int length -> new byte[length] } - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Act - INVALID_IVS.each { byte[] badIV -> - logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}") - - // Encrypt should print a warning about the bad IV but overwrite it - Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, true) - - // Decrypt should fail - def msg = shouldFail(IllegalArgumentException) { - cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, false) - } - - // Assert - assert msg =~ "Cannot decrypt without a valid IV" - } - } - - @Test - public void testGetCipherWithExternalIVShouldBeInternallyConsistent() 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 byte[] IV = Hex.decodeHex(IV_HEX 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 PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT); - - 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."; - - // 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 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 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); - } - - // Assert - assert msg =~ "Cannot resolve empty PRF" - } - - @Test - public 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 plaintext = "This is a plaintext message."; - final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - final PBKDF2CipherProvider SHA512_PROVIDER = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT) - - String prf = "sha768" - logger.info("Using ${prf}") - - // Act - cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT); - logger.info("Resolved PRF to ${cipherProvider.getPRFName()}") - logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}"); - - // Initialize a cipher for encryption - 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}"); - - 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); - } - - @Test - public void testShouldResolveVariousPRFs() throws Exception { - // Arrange - final List 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 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); - logger.info("Resolved PRF to ${cipherProvider.getPRFName()}") - - logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}"); - - // Initialize a cipher for encryption - 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}"); - - 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); - } - } - - @Test - public void testGetCipherShouldSupportExternalCompatibility() throws Exception { - // Arrange - RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider("SHA-256", TEST_ITERATION_COUNT); - - 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 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}"); - - // 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"); - logger.info("Recovered: ${recovered}") - - // Assert - assert PLAINTEXT.equals(recovered); - } - - @Test - public void testGetCipherForDecryptShouldRequireIV() 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 byte[] IV = Hex.decodeHex(IV_HEX 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 testGetCipherShouldRejectInvalidSalt() throws Exception { - // Arrange - RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT) - - 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()}"); - - // 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 at least 16 bytes\\. To generate a salt, use PBKDF2CipherProvider#generateSalt" - } - } - - @Test - public void testGetCipherShouldAcceptValidKeyLengths() 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 byte[] IV = Hex.decodeHex(IV_HEX as char[]); - - 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 - 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 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[]); - - // Currently only AES ciphers are compatible with PBKDF2, so redundant to test all algorithms - final def VALID_KEY_LENGTHS = [-1, 40, 64, 112, 512] - 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 - 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" - } - } - - @Ignore("This test can be run on a specific machine to evaluate if the default iteration count is sufficient") - @Test - public void testDefaultConstructorShouldProvideStrongIterationCount() { - // Arrange - 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 - - // Calculate the iteration count to reach 500 ms - int minimumIterationCount = calculateMinimumIterationCount() - logger.info("Determined minimum safe iteration count to be ${minimumIterationCount}") - - // Act - int iterationCount = cipherProvider.getIterationCount() - logger.info("Default iteration count ${iterationCount}") - - // Assert - assertTrue("The default iteration count for PBKDF2CipherProvider is too weak. Please update the default value to a stronger level.", iterationCount >= minimumIterationCount) - } - - /** - * Returns the iteration count required for a derivation to exceed 500 ms on this machine using the default PRF. - * Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt - * - * @return the minimum iteration count - */ - private static int calculateMinimumIterationCount() { - // High start-up cost, so run multiple times for better benchmarking - final int RUNS = 10 - - // Benchmark using an iteration count of 10k - int iterationCount = 10_000 - - final byte[] SALT = [0x00 as byte] * 16 - final byte[] IV = [0x01 as byte] * 16 - - String defaultPrf = new PBKDF2CipherProvider().getPRFName() - RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(defaultPrf, iterationCount) - - // Run once to prime the system - double duration = time { - Cipher cipher = cipherProvider.getCipher(EncryptionMethod.AES_CBC, MICROBENCHMARK, SALT, IV, DEFAULT_KEY_LENGTH, false) - } - logger.info("First run of iteration count ${iterationCount} took ${duration} ms (ignored)") - - def durations = [] - - RUNS.times { int i -> - duration = time { - // Use encrypt mode with provided salt and IV to minimize overhead during benchmark call - Cipher cipher = cipherProvider.getCipher(EncryptionMethod.AES_CBC, "${MICROBENCHMARK}${i}", SALT, IV, DEFAULT_KEY_LENGTH, false) - } - logger.info("Iteration count ${iterationCount} took ${duration} ms") - durations << duration - } - - duration = durations.sum() / durations.size() - logger.info("Iteration count ${iterationCount} averaged ${duration} ms") - - // Keep increasing iteration count until the estimated duration is over 500 ms - while (duration < 500) { - iterationCount *= 2 - duration *= 2 - } - - logger.info("Returning iteration count ${iterationCount} for ${duration} ms") - - return iterationCount - } - - private static double time(Closure c) { - long start = System.nanoTime() - c.call() - long end = System.nanoTime() - return (end - start) / 1_000_000.0 - } - - @Test - public void testGenerateSaltShouldProvideValidSalt() throws Exception { - // Arrange - RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT) - - // Act - byte[] salt = cipherProvider.generateSalt() - logger.info("Checking salt ${Hex.encodeHexString(salt)}") - - // Assert - assert salt.length == 16 - assert salt != [(0x00 as byte) * 16] - } -} \ 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/processors/standard/util/crypto/PasswordBasedEncryptorGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptorGroovyTest.groovy deleted file mode 100644 index 35c20e5..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptorGroovyTest.groovy +++ /dev/null @@ -1,225 +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 - -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.processor.io.StreamCallback -import org.apache.nifi.security.util.EncryptionMethod -import org.apache.nifi.security.util.KeyDerivationFunction -import org.apache.nifi.stream.io.ByteArrayOutputStream -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.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.crypto.Cipher -import java.security.Security - -public class PasswordBasedEncryptorGroovyTest { - private static final Logger logger = LoggerFactory.getLogger(PasswordBasedEncryptorGroovyTest.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}/salted_128_raw.asc") - - private static final String PASSWORD = "thisIsABadPassword" - private static final String LEGACY_PASSWORD = "Hello, World!" - - @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")) - - String shortPassword = "short" - - def encryptionMethodsAndKdfs = [ - (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): EncryptionMethod.MD5_128AES, - (KeyDerivationFunction.NIFI_LEGACY) : EncryptionMethod.MD5_128AES, - (KeyDerivationFunction.BCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.SCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.PBKDF2) : EncryptionMethod.AES_CBC - ] - - // Act - encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod -> - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - logger.info("Using ${kdf.name} and ${encryptionMethod.name()}") - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, shortPassword.toCharArray(), kdf) - - 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) - - // This is necessary to run multiple iterations - plainStream.reset() - } - } - - @Test - public void testShouldDecryptLegacyOpenSSLSaltedCipherText() throws Exception { - // Arrange - Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength()) - - final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text - logger.info("Plaintext: {}", PLAINTEXT) - byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/salted_128_raw.enc").bytes - InputStream cipherStream = new ByteArrayInputStream(cipherBytes) - OutputStream recoveredStream = new ByteArrayOutputStream() - - final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES - final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - 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) - } - - @Test - public void testShouldDecryptLegacyOpenSSLUnsaltedCipherText() throws Exception { - // Arrange - Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength()) - - 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 - InputStream cipherStream = new ByteArrayInputStream(cipherBytes) - OutputStream recoveredStream = new ByteArrayOutputStream() - - final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES - final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - 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) - } - - @Test - public void testShouldDecryptNiFiLegacySaltedCipherTextWithVariableSaltLength() throws Exception { - // Arrange - final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text - logger.info("Plaintext: {}", PLAINTEXT) - - final String PASSWORD = "short" - logger.info("Password: ${PASSWORD}") - - /* The old NiFi legacy KDF code checked the algorithm block size and used it for the salt length. - If the block size was not available, it defaulted to 8 bytes based on the default salt size. */ - - def pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") } - def encryptionMethodsByBlockSize = pbeEncryptionMethods.groupBy { - Cipher cipher = Cipher.getInstance(it.algorithm, it.provider) - cipher.getBlockSize() - } - - logger.info("Grouped algorithms by block size: ${encryptionMethodsByBlockSize.collectEntries { k, v -> [k, v*.algorithm] }}") - - encryptionMethodsByBlockSize.each { int blockSize, List encryptionMethods -> - encryptionMethods.each { EncryptionMethod encryptionMethod -> - final int EXPECTED_SALT_SIZE = (blockSize > 0) ? blockSize : 8 - logger.info("Testing ${encryptionMethod.algorithm} with expected salt size ${EXPECTED_SALT_SIZE}") - - def legacySaltHex = "aa" * EXPECTED_SALT_SIZE - byte[] legacySalt = Hex.decodeHex(legacySaltHex as char[]) - logger.info("Generated legacy salt ${legacySaltHex} (${legacySalt.length})") - - // Act - - // Encrypt using the raw legacy code - NiFiLegacyCipherProvider legacyCipherProvider = new NiFiLegacyCipherProvider() - Cipher legacyCipher = legacyCipherProvider.getCipher(encryptionMethod, PASSWORD, legacySalt, true) - byte[] cipherBytes = legacyCipher.doFinal(PLAINTEXT.bytes) - logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") - - byte[] completeCipherStreamBytes = CipherUtility.concatBytes(legacySalt, cipherBytes) - logger.info("Complete cipher stream: ${Hex.encodeHexString(completeCipherStreamBytes)}") - - InputStream cipherStream = new ByteArrayInputStream(completeCipherStreamBytes) - OutputStream resultStream = new ByteArrayOutputStream() - - // Now parse and decrypt using PBE encryptor - PasswordBasedEncryptor decryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD as char[], KeyDerivationFunction.NIFI_LEGACY) - - StreamCallback decryptCallback = decryptor.decryptionCallback - decryptCallback.process(cipherStream, resultStream) - - logger.info("Decrypted: ${Hex.encodeHexString(resultStream.toByteArray())}") - String recovered = new String(resultStream.toByteArray()) - logger.info("Recovered: ${recovered}") - - // Assert - assert recovered == PLAINTEXT - } - } - } -} \ 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/processors/standard/util/crypto/ScryptCipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProviderGroovyTest.groovy deleted file mode 100644 index 8fd0455..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProviderGroovyTest.groovy +++ /dev/null @@ -1,597 +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 - -import org.apache.commons.codec.binary.Base64 -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.processors.standard.util.crypto.scrypt.Scrypt -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.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec -import java.security.SecureRandom -import java.security.Security - -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); - - private static List strongKDFEncryptionMethods - - private static final int DEFAULT_KEY_LENGTH = 128; - public static final String MICROBENCHMARK = "microbenchmark" - private static ArrayList AES_KEY_LENGTHS - - RandomIVPBECipherProvider cipherProvider - - @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 { - // Very fast parameters to test for correctness rather than production values - cipherProvider = new ScryptCipherProvider(4, 1, 1) - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testGetCipherShouldBeInternallyConsistent() throws Exception { - // Arrange - 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 - 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()); - - 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 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 PASSWORD = "thisIsABadPassword" - final int DK_LEN = 128 - - // These values can be generated by running `$ ./openssl_scrypt.rb` in the terminal - final byte[] SALT = Hex.decodeHex("f5b8056ea6e66edb8d013ac432aba24a" as char[]) - logger.info("Expected salt: ${Hex.encodeHexString(SALT)}") - final byte[] IV = Hex.decodeHex("76a00f00878b8c3db314ae67804c00a1" as char[]) - - final String CIPHER_TEXT = "604188bf8e9137bc1b24a0ab01973024bc5935e9ae5fedf617bdca028c63c261" - logger.sanity("Ruby cipher text: ${CIPHER_TEXT}") - byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[]) - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Sanity check - String rubyKeyHex = "a8efbc0a709d3f89b6bb35b05fc8edf5" - logger.sanity("Using key: ${rubyKeyHex}") - logger.sanity("Using IV: ${Hex.encodeHexString(IV)}") - Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC") - def rubyKey = new SecretKeySpec(Hex.decodeHex(rubyKeyHex as char[]), "AES") - def ivSpec = new IvParameterSpec(IV) - rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec) - byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes) - logger.sanity("Created cipher text: ${Hex.encodeHexString(rubyCipherBytes)}") - rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec) - assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes - logger.sanity("Decrypted generated cipher text successfully") - assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes - logger.sanity("Decrypted external cipher text successfully") - - // n$r$p$hex_salt_SL$hex_hash_HL - final String FULL_HASH = "400\$8\$24\$f5b8056ea6e66edb8d013ac432aba24a\$a8efbc0a709d3f89b6bb35b05fc8edf5" - logger.info("Full Hash: ${FULL_HASH}") - - def (String nStr, String rStr, String pStr, String saltHex, String hashHex) = FULL_HASH.split("\\\$") - def (n, r, p) = [nStr, rStr, pStr].collect { Integer.valueOf(it, 16) } - - logger.info("N: Hex ${nStr} -> ${n}") - logger.info("r: Hex ${rStr} -> ${r}") - logger.info("p: Hex ${pStr} -> ${p}") - logger.info("Salt: ${saltHex}") - logger.info("Hash: ${hashHex}") - - // Form Java-style salt with cost params from Ruby-style - String javaSalt = Scrypt.formatSalt(Hex.decodeHex(saltHex as char[]), n, r, p) - logger.info("Formed Java-style salt: ${javaSalt}") - - // Convert hash from hex to Base64 - String base64Hash = CipherUtility.encodeBase64NoPadding(Hex.decodeHex(hashHex as char[])) - 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}"); - - // 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"); - logger.info("Recovered: ${recovered}") - - // Assert - assert PLAINTEXT.equals(recovered); - } - - @Test - public 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 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."; - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - 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(); - logger.info("IV: ${Hex.encodeHexString(iv)}") - - 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] - def params = [] - cipherProvider.parseSalt(EXPECTED_FORMATTED_SALT, parsedSalt, params) - def (int n, int r, int p) = params - byte[] keyBytes = Scrypt.deriveScryptKey(PASSWORD.bytes, parsedSalt, n, r, p, DEFAULT_KEY_LENGTH) - 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"); - logger.info("Recovered: ${recovered}") - - // Assert - assert plaintext.equals(recovered); - } - - @Test - public void testGetCipherShouldNotAcceptInvalidSalts() throws Exception { - // Arrange - 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()}"); - - // 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); - } - logger.expected(msg) - - // Assert - assert msg =~ LENGTH_MESSAGE - } - } - - @Test - public void testGetCipherShouldHandleUnformattedSalts() throws Exception { - // Arrange - 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()}"); - - // Act - RECOVERABLE_SALTS.each { String salt -> - logger.info("Checking salt ${salt}") - - Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true); - - // Assert - assert cipher - } - } - - @Test - public void testGetCipherShouldRejectEmptySalt() throws Exception { - // Arrange - final String PASSWORD = "thisIsABadPassword"; - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}"); - - // 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 ScryptCipherProvider#generateSalt" - } - - @Test - public void testGetCipherForDecryptShouldRequireIV() throws Exception { - // Arrange - 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); - } - logger.expected(msg) - - // Assert - assert msg =~ "Cannot decrypt without a valid IV" - } - } - - @Test - public void testGetCipherShouldAcceptValidKeyLengths() throws Exception { - // Arrange - 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 - 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."; - - // 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] - // Currently only AES ciphers are compatible with Scrypt, so redundant to test all algorithms - 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); - } - logger.expected(msg) - - // Assert - assert msg =~ "${keyLength} is not a valid key length for AES" - } - } - - @Test - public void testScryptShouldNotAcceptInvalidPassword() { - // Arrange - String badPassword = "" - byte[] salt = [0x01 as byte] * 16 - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Act - def msg = shouldFail(IllegalArgumentException) { - cipherProvider.getCipher(encryptionMethod, badPassword, salt, DEFAULT_KEY_LENGTH, true) - } - - // Assert - assert msg =~ "Encryption with an empty password is not supported" - } - - @Test - public void testGenerateSaltShouldUseProvidedParameters() throws Exception { - // Arrange - RandomIVPBECipherProvider cipherProvider = new ScryptCipherProvider(8, 2, 2); - int n = cipherProvider.getN() - int r = cipherProvider.getR() - int p = cipherProvider.getP() - - // Act - final String salt = new String(cipherProvider.generateSalt()) - logger.info("Salt: ${salt}") - - // Assert - assert salt =~ "^(?i)\\\$s0\\\$[a-f0-9]{5,16}\\\$" - String params = Scrypt.encodeParams(n, r, p) - assert salt.contains("\$${params}\$") - } - - @Test - public void testShouldParseSalt() throws Exception { - // Arrange - cipherProvider = cipherProvider as ScryptCipherProvider - - final byte[] EXPECTED_RAW_SALT = Hex.decodeHex("f5b8056ea6e66edb8d013ac432aba24a" as char[]) - final int EXPECTED_N = 1024 - final int EXPECTED_R = 8 - final int EXPECTED_P = 36 - - final String FORMATTED_SALT = "\$s0\$a0824\$9bgFbqbmbtuNATrEMquiSg" - logger.info("Using salt: ${FORMATTED_SALT}"); - - byte[] rawSalt = new byte[16] - def params = [] - - // Act - cipherProvider.parseSalt(FORMATTED_SALT, rawSalt, params) - - // Assert - assert rawSalt == EXPECTED_RAW_SALT - assert params[0] == EXPECTED_N - assert params[1] == EXPECTED_R - assert params[2] == EXPECTED_P - } - - @Ignore("This test can be run on a specific machine to evaluate if the default parameters are sufficient") - @Test - public void testDefaultConstructorShouldProvideStrongParameters() { - // Arrange - ScryptCipherProvider testCipherProvider = new ScryptCipherProvider() - - /** See this Stack Overflow answer for a good visualization of the interplay between N, r, p http://stackoverflow.com/a/30308723 */ - - // Act - int n = testCipherProvider.getN() - int r = testCipherProvider.getR() - int p = testCipherProvider.getP() - logger.info("Default parameters N=${n}, r=${r}, p=${p}") - - // Calculate the parameters to reach 500 ms - def (int minimumN, int minimumR, int minimumP) = calculateMinimumParameters(r, p) - logger.info("Determined minimum safe parameters to be N=${minimumN}, r=${minimumR}, p=${minimumP}") - - // Assert - assertTrue("The default parameters for ScryptCipherProvider are too weak. Please update the default values to a stronger level.", n >= minimumN) - } - - /** - * Returns the parameters 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 - * - * @param r the block size in bytes (defaults to 8) - * @param p the parallelization factor (defaults to 1) - * @param maxHeapSize the maximum heap size to use in bytes (defaults to 1 GB) - * - * @return the minimum scrypt parameters as [N, r, p] - */ - private static List calculateMinimumParameters(int r = 8, int p = 1, int maxHeapSize = 1024 * 1024 * 1024) { - // High start-up cost, so run multiple times for better benchmarking - final int RUNS = 10 - - // Benchmark using N=2^4 - int n = 2**4 - int dkLen = 128 - - assert Scrypt.calculateExpectedMemory(n, r, p) <= maxHeapSize - - byte[] salt = new byte[Scrypt.defaultSaltLength] - new SecureRandom().nextBytes(salt) - - // Run once to prime the system - double duration = time { - Scrypt.scrypt(MICROBENCHMARK, salt, n, r, p, dkLen) - } - logger.info("First run of N=${n}, r=${r}, p=${p} took ${duration} ms (ignored)") - - def durations = [] - - RUNS.times { int i -> - duration = time { - Scrypt.scrypt(MICROBENCHMARK, salt, n, r, p, dkLen) - } - logger.info("N=${n}, r=${r}, p=${p} took ${duration} ms") - durations << duration - } - - duration = durations.sum() / durations.size() - logger.info("N=${n}, r=${r}, p=${p} averaged ${duration} ms") - - // Doubling N would double the run time - // Keep increasing N until the estimated duration is over 500 ms - while (duration < 500) { - n *= 2 - duration *= 2 - } - - logger.info("Returning N=${n}, r=${r}, p=${p} for ${duration} ms") - - return [n, r, p] - } - - 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