http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/scrypt/Scrypt.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/scrypt/Scrypt.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/scrypt/Scrypt.java new file mode 100644 index 0000000..2aeae3d --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/scrypt/Scrypt.java @@ -0,0 +1,510 @@ +/* + * 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.scrypt; + +import static java.lang.Integer.MAX_VALUE; +import static java.lang.System.arraycopy; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.security.util.crypto.CipherUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Copyright (C) 2011 - Will Glozer. All rights reserved. + *

+ * Taken from Will Glozer's port of Colin Percival's C implementation. Glozer's project located at https://github.com/wg/scrypt was released under the ASF + * 2.0 license and has not been updated since May 25, 2013 and there are outstanding issues which have been patched in this version. + *

+ * An implementation of the scrypt + * key derivation function. + *

+ * Allows for hashing passwords using the + * scrypt key derivation function + * and comparing a plain text password to a hashed one. + */ +public class Scrypt { + private static final Logger logger = LoggerFactory.getLogger(Scrypt.class); + + private static final int DEFAULT_SALT_LENGTH = 16; + + /** + * Hash the supplied plaintext password and generate output in the format described + * below: + *

+ * The hashed output is an + * extended implementation of the Modular Crypt Format that also includes the scrypt + * algorithm parameters. + *

+ * Format: $s0$PARAMS$SALT$KEY. + *

+ *

+ *
PARAMS
32-bit hex integer containing log2(N) (16 bits), r (8 bits), and p (8 bits)
+ *
SALT
base64-encoded salt
+ *
KEY
base64-encoded derived key
+ *
+ *

+ * s0 identifies version 0 of the scrypt format, using a 128-bit salt and 256-bit derived key. + *

+ * This method generates a 16 byte random salt internally. + * + * @param password password + * @param n CPU cost parameter + * @param r memory cost parameter + * @param p parallelization parameter + * @param dkLen the desired key length in bits + * @return the hashed password + */ + public static String scrypt(String password, int n, int r, int p, int dkLen) { + byte[] salt = new byte[DEFAULT_SALT_LENGTH]; + new SecureRandom().nextBytes(salt); + + return scrypt(password, salt, n, r, p, dkLen); + } + + /** + * Hash the supplied plaintext password and generate output in the format described + * in {@link Scrypt#scrypt(String, int, int, int, int)}. + * + * @param password password + * @param salt the raw salt (16 bytes) + * @param n CPU cost parameter + * @param r memory cost parameter + * @param p parallelization parameter + * @param dkLen the desired key length in bits + * @return the hashed password + */ + public static String scrypt(String password, byte[] salt, int n, int r, int p, int dkLen) { + try { + byte[] derived = deriveScryptKey(password.getBytes(StandardCharsets.UTF_8), salt, n, r, p, dkLen); + + return formatHash(salt, n, r, p, derived); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?"); + } + } + + public static String formatSalt(byte[] salt, int n, int r, int p) { + String params = encodeParams(n, r, p); + + StringBuilder sb = new StringBuilder((salt.length) * 2); + sb.append("$s0$").append(params).append('$'); + sb.append(CipherUtility.encodeBase64NoPadding(salt)); + + return sb.toString(); + } + + private static String encodeParams(int n, int r, int p) { + return Long.toString(log2(n) << 16L | r << 8 | p, 16); + } + + private static String formatHash(byte[] salt, int n, int r, int p, byte[] derived) { + StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2); + sb.append(formatSalt(salt, n, r, p)).append('$'); + sb.append(CipherUtility.encodeBase64NoPadding(derived)); + + return sb.toString(); + } + + /** + * Returns the expected memory cost of the provided parameters in bytes. + * + * @param n the N value, iterations >= 2 + * @param r the r value, block size >= 1 + * @param p the p value, parallelization factor >= 1 + * @return the memory cost in bytes + */ + public static int calculateExpectedMemory(int n, int r, int p) { + return 128 * r * n + 128 * r * p; + } + + /** + * Compare the supplied plaintext password to a hashed password. + * + * @param password plaintext password + * @param hashed scrypt hashed password + * @return true if password matches hashed value + */ + public static boolean check(String password, String hashed) { + try { + if (StringUtils.isEmpty(password)) { + throw new IllegalArgumentException("Password cannot be empty"); + } + + if (StringUtils.isEmpty(hashed)) { + throw new IllegalArgumentException("Hash cannot be empty"); + } + + String[] parts = hashed.split("\\$"); + + if (parts.length != 5 || !parts[1].equals("s0")) { + throw new IllegalArgumentException("Hash is not properly formatted"); + } + + List splitParams = parseParameters(parts[2]); + int n = splitParams.get(0); + int r = splitParams.get(1); + int p = splitParams.get(2); + + byte[] salt = Base64.decodeBase64(parts[3]); + byte[] derived0 = Base64.decodeBase64(parts[4]); + + // Previously this was hard-coded to 32 bits but the publicly-available scrypt methods accept arbitrary bit lengths + int hashLength = derived0.length * 8; + byte[] derived1 = deriveScryptKey(password.getBytes(StandardCharsets.UTF_8), salt, n, r, p, hashLength); + + if (derived0.length != derived1.length) return false; + + int result = 0; + for (int i = 0; i < derived0.length; i++) { + result |= derived0[i] ^ derived1[i]; + } + return result == 0; + } catch (GeneralSecurityException e) { + throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?"); + } + } + + /** + * Parses the individual values from the encoded params value in the modified-mcrypt format for the salt & hash. + *

+ * Example: + *

+ * Hash: $s0$e0801$epIxT/h6HbbwHaehFnh/bw$7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0 + * Params: e0801 + *

+ * N = 16384 + * r = 8 + * p = 1 + * + * @param encodedParams the String representation of the second section of the mcrypt format hash + * @return a list containing N, r, p + */ + public static List parseParameters(String encodedParams) { + long params = Long.parseLong(encodedParams, 16); + + List paramsList = new ArrayList<>(3); + + // Parse N, r, p from encoded value and add to return list + paramsList.add((int) Math.pow(2, params >> 16 & 0xffff)); + paramsList.add((int) params >> 8 & 0xff); + paramsList.add((int) params & 0xff); + + return paramsList; + } + + private static int log2(int n) { + int log = 0; + if ((n & 0xffff0000) != 0) { + n >>>= 16; + log = 16; + } + if (n >= 256) { + n >>>= 8; + log += 8; + } + if (n >= 16) { + n >>>= 4; + log += 4; + } + if (n >= 4) { + n >>>= 2; + log += 2; + } + return log + (n >>> 1); + } + + /** + * Implementation of the scrypt KDF. + * + * @param password password + * @param salt salt + * @param n CPU cost parameter + * @param r memory cost parameter + * @param p parallelization parameter + * @param dkLen intended length of the derived key in bits + * @return the derived key + * @throws GeneralSecurityException when HMAC_SHA256 is not available + */ + protected static byte[] deriveScryptKey(byte[] password, byte[] salt, int n, int r, int p, int dkLen) throws GeneralSecurityException { + if (n < 2 || (n & (n - 1)) != 0) { + throw new IllegalArgumentException("N must be a power of 2 greater than 1"); + } + + if (r < 1) { + throw new IllegalArgumentException("Parameter r must be 1 or greater"); + } + + if (p < 1) { + throw new IllegalArgumentException("Parameter p must be 1 or greater"); + } + + if (n > MAX_VALUE / 128 / r) { + throw new IllegalArgumentException("Parameter N is too large"); + } + + // Must be enforced before r check + if (p > MAX_VALUE / 128) { + throw new IllegalArgumentException("Parameter p is too large"); + } + + if (r > MAX_VALUE / 128 / p) { + throw new IllegalArgumentException("Parameter r is too large"); + } + + if (password == null || password.length == 0) { + throw new IllegalArgumentException("Password cannot be empty"); + } + + int saltLength = salt == null ? 0 : salt.length; + if (salt == null || saltLength == 0) { + // Do not enforce this check here. According to the scrypt spec, the salt can be empty. However, in the user-facing ScryptCipherProvider, enforce an arbitrary check to avoid empty salts + logger.warn("An empty salt was used for scrypt key derivation"); +// throw new IllegalArgumentException("Salt cannot be empty"); + // as the Exception is not being thrown, prevent NPE if salt is null by setting it to empty array + if( salt == null ) salt = new byte[]{}; + } + + if (saltLength < 8 || saltLength > 32) { + // Do not enforce this check here. According to the scrypt spec, the salt can be empty. However, in the user-facing ScryptCipherProvider, enforce an arbitrary check of [8..32] bytes + logger.warn("A salt of length {} was used for scrypt key derivation", saltLength); +// throw new IllegalArgumentException("Salt must be between 8 and 32 bytes"); + } + + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(password, "HmacSHA256")); + + byte[] b = new byte[128 * r * p]; + byte[] xy = new byte[256 * r]; + byte[] v = new byte[128 * r * n]; + int i; + + pbkdf2(mac, salt, 1, b, p * 128 * r); + + for (i = 0; i < p; i++) { + smix(b, i * 128 * r, r, n, v, xy); + } + + byte[] dk = new byte[dkLen / 8]; + pbkdf2(mac, b, 1, dk, dkLen / 8); + return dk; + } + + /** + * Implementation of PBKDF2 (RFC2898). + * + * @param alg the HMAC algorithm to use + * @param p the password + * @param s the salt + * @param c the iteration count + * @param dkLen the intended length, in octets, of the derived key + * @return The derived key + */ + private static byte[] pbkdf2(String alg, byte[] p, byte[] s, int c, int dkLen) throws GeneralSecurityException { + Mac mac = Mac.getInstance(alg); + mac.init(new SecretKeySpec(p, alg)); + byte[] dk = new byte[dkLen]; + pbkdf2(mac, s, c, dk, dkLen); + return dk; + } + + /** + * Implementation of PBKDF2 (RFC2898). + * + * @param mac the pre-initialized {@link Mac} instance to use + * @param s the salt + * @param c the iteration count + * @param dk the byte array that derived key will be placed in + * @param dkLen the intended length, in octets, of the derived key + * @throws GeneralSecurityException if the key length is too long + */ + private static void pbkdf2(Mac mac, byte[] s, int c, byte[] dk, int dkLen) throws GeneralSecurityException { + int hLen = mac.getMacLength(); + + if (dkLen > (Math.pow(2, 32) - 1) * hLen) { + throw new GeneralSecurityException("Requested key length too long"); + } + + byte[] U = new byte[hLen]; + byte[] T = new byte[hLen]; + byte[] block1 = new byte[s.length + 4]; + + int l = (int) Math.ceil((double) dkLen / hLen); + int r = dkLen - (l - 1) * hLen; + + arraycopy(s, 0, block1, 0, s.length); + + for (int i = 1; i <= l; i++) { + block1[s.length + 0] = (byte) (i >> 24 & 0xff); + block1[s.length + 1] = (byte) (i >> 16 & 0xff); + block1[s.length + 2] = (byte) (i >> 8 & 0xff); + block1[s.length + 3] = (byte) (i >> 0 & 0xff); + + mac.update(block1); + mac.doFinal(U, 0); + arraycopy(U, 0, T, 0, hLen); + + for (int j = 1; j < c; j++) { + mac.update(U); + mac.doFinal(U, 0); + + for (int k = 0; k < hLen; k++) { + T[k] ^= U[k]; + } + } + + arraycopy(T, 0, dk, (i - 1) * hLen, (i == l ? r : hLen)); + } + } + + private static void smix(byte[] b, int bi, int r, int n, byte[] v, byte[] xy) { + int xi = 0; + int yi = 128 * r; + int i; + + arraycopy(b, bi, xy, xi, 128 * r); + + for (i = 0; i < n; i++) { + arraycopy(xy, xi, v, i * (128 * r), 128 * r); + blockmix_salsa8(xy, xi, yi, r); + } + + for (i = 0; i < n; i++) { + int j = integerify(xy, xi, r) & (n - 1); + blockxor(v, j * (128 * r), xy, xi, 128 * r); + blockmix_salsa8(xy, xi, yi, r); + } + + arraycopy(xy, xi, b, bi, 128 * r); + } + + private static void blockmix_salsa8(byte[] by, int bi, int yi, int r) { + byte[] X = new byte[64]; + int i; + + arraycopy(by, bi + (2 * r - 1) * 64, X, 0, 64); + + for (i = 0; i < 2 * r; i++) { + blockxor(by, i * 64, X, 0, 64); + salsa20_8(X); + arraycopy(X, 0, by, yi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy(by, yi + (i * 2) * 64, by, bi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy(by, yi + (i * 2 + 1) * 64, by, bi + (i + r) * 64, 64); + } + } + + private static int r(int a, int b) { + return (a << b) | (a >>> (32 - b)); + } + + private static void salsa20_8(byte[] b) { + int[] b32 = new int[16]; + int[] x = new int[16]; + int i; + + for (i = 0; i < 16; i++) { + b32[i] = (b[i * 4 + 0] & 0xff) << 0; + b32[i] |= (b[i * 4 + 1] & 0xff) << 8; + b32[i] |= (b[i * 4 + 2] & 0xff) << 16; + b32[i] |= (b[i * 4 + 3] & 0xff) << 24; + } + + arraycopy(b32, 0, x, 0, 16); + + for (i = 8; i > 0; i -= 2) { + x[4] ^= r(x[0] + x[12], 7); + x[8] ^= r(x[4] + x[0], 9); + x[12] ^= r(x[8] + x[4], 13); + x[0] ^= r(x[12] + x[8], 18); + x[9] ^= r(x[5] + x[1], 7); + x[13] ^= r(x[9] + x[5], 9); + x[1] ^= r(x[13] + x[9], 13); + x[5] ^= r(x[1] + x[13], 18); + x[14] ^= r(x[10] + x[6], 7); + x[2] ^= r(x[14] + x[10], 9); + x[6] ^= r(x[2] + x[14], 13); + x[10] ^= r(x[6] + x[2], 18); + x[3] ^= r(x[15] + x[11], 7); + x[7] ^= r(x[3] + x[15], 9); + x[11] ^= r(x[7] + x[3], 13); + x[15] ^= r(x[11] + x[7], 18); + x[1] ^= r(x[0] + x[3], 7); + x[2] ^= r(x[1] + x[0], 9); + x[3] ^= r(x[2] + x[1], 13); + x[0] ^= r(x[3] + x[2], 18); + x[6] ^= r(x[5] + x[4], 7); + x[7] ^= r(x[6] + x[5], 9); + x[4] ^= r(x[7] + x[6], 13); + x[5] ^= r(x[4] + x[7], 18); + x[11] ^= r(x[10] + x[9], 7); + x[8] ^= r(x[11] + x[10], 9); + x[9] ^= r(x[8] + x[11], 13); + x[10] ^= r(x[9] + x[8], 18); + x[12] ^= r(x[15] + x[14], 7); + x[13] ^= r(x[12] + x[15], 9); + x[14] ^= r(x[13] + x[12], 13); + x[15] ^= r(x[14] + x[13], 18); + } + + for (i = 0; i < 16; ++i) b32[i] = x[i] + b32[i]; + + for (i = 0; i < 16; i++) { + b[i * 4 + 0] = (byte) (b32[i] >> 0 & 0xff); + b[i * 4 + 1] = (byte) (b32[i] >> 8 & 0xff); + b[i * 4 + 2] = (byte) (b32[i] >> 16 & 0xff); + b[i * 4 + 3] = (byte) (b32[i] >> 24 & 0xff); + } + } + + private static void blockxor(byte[] s, int si, byte[] d, int di, int len) { + for (int i = 0; i < len; i++) { + d[di + i] ^= s[si + i]; + } + } + + private static int integerify(byte[] b, int bi, int r) { + int n; + + bi += (2 * r - 1) * 64; + + n = (b[bi + 0] & 0xff) << 0; + n |= (b[bi + 1] & 0xff) << 8; + n |= (b[bi + 2] & 0xff) << 16; + n |= (b[bi + 3] & 0xff) << 24; + + return n; + } + + public static int getDefaultSaltLength() { + return DEFAULT_SALT_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/TestEncryptContentGroovy.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestEncryptContentGroovy.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestEncryptContentGroovy.groovy index e3de6b0..f940640 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestEncryptContentGroovy.groovy +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestEncryptContentGroovy.groovy @@ -17,10 +17,10 @@ package org.apache.nifi.processors.standard import org.apache.nifi.components.ValidationResult -import org.apache.nifi.processors.standard.util.crypto.CipherUtility -import org.apache.nifi.processors.standard.util.crypto.PasswordBasedEncryptor import org.apache.nifi.security.util.EncryptionMethod import org.apache.nifi.security.util.KeyDerivationFunction +import org.apache.nifi.security.util.crypto.CipherUtility +import org.apache.nifi.security.util.crypto.PasswordBasedEncryptor import org.apache.nifi.util.MockFlowFile import org.apache.nifi.util.MockProcessContext import org.apache.nifi.util.TestRunner 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/AESKeyedCipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy deleted file mode 100644 index 0596d7d..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy +++ /dev/null @@ -1,340 +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.spec.SecretKeySpec -import java.security.SecureRandom -import java.security.Security - -import static groovy.test.GroovyAssert.shouldFail - -@RunWith(JUnit4.class) -public class AESKeyedCipherProviderGroovyTest { - private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProviderGroovyTest.class) - - private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" - - private static final List keyedEncryptionMethods = EncryptionMethod.values().findAll { it.keyedCipher } - - 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 testGetCipherShouldBeInternallyConsistent() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - final String plaintext = "This is a plaintext message." - - // Act - for (EncryptionMethod em : keyedEncryptionMethods) { - logger.info("Using algorithm: ${em.getAlgorithm()}") - - // Initialize a cipher for encryption - Cipher cipher = cipherProvider.getCipher(em, key, 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, key, iv, false) - byte[] recoveredBytes = cipher.doFinal(cipherBytes) - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: ${recovered}") - - // Assert - assert plaintext.equals(recovered) - } - } - - @Test - public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - final String plaintext = "This is a plaintext message." - - // Act - keyedEncryptionMethods.each { EncryptionMethod em -> - logger.info("Using algorithm: ${em.getAlgorithm()}") - byte[] iv = cipherProvider.generateIV() - logger.info("IV: ${Hex.encodeHexString(iv)}") - - // Initialize a cipher for encryption - Cipher cipher = cipherProvider.getCipher(em, key, iv, true) - - byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8")) - logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}") - - cipher = cipherProvider.getCipher(em, key, iv, false) - byte[] recoveredBytes = cipher.doFinal(cipherBytes) - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: ${recovered}") - - // Assert - assert plaintext.equals(recovered) - } - } - - @Test - 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()) - - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - final List LONG_KEY_LENGTHS = [192, 256] - - final String plaintext = "This is a plaintext message." - - SecureRandom secureRandom = new SecureRandom() - - // Act - keyedEncryptionMethods.each { EncryptionMethod em -> - // Re-use the same IV for the different length keys to ensure the encryption is different - byte[] iv = cipherProvider.generateIV() - logger.info("IV: ${Hex.encodeHexString(iv)}") - - LONG_KEY_LENGTHS.each { int keyLength -> - logger.info("Using algorithm: ${em.getAlgorithm()} with key length ${keyLength}") - - // Generate a key - byte[] keyBytes = new byte[keyLength / 8] - secureRandom.nextBytes(keyBytes) - SecretKey localKey = new SecretKeySpec(keyBytes, "AES") - logger.info("Key: ${Hex.encodeHexString(keyBytes)} ${keyBytes.length}") - - // Initialize a cipher for encryption - Cipher cipher = cipherProvider.getCipher(em, localKey, iv, true) - - byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8")) - logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}") - - cipher = cipherProvider.getCipher(em, localKey, iv, false) - byte[] recoveredBytes = cipher.doFinal(cipherBytes) - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: ${recovered}") - - // Assert - assert plaintext.equals(recovered) - } - } - } - - @Test - public void testShouldRejectEmptyKey() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Act - def msg = shouldFail(IllegalArgumentException) { - cipherProvider.getCipher(encryptionMethod, null, true) - } - - // Assert - assert msg =~ "The key must be specified" - } - - @Test - public void testShouldRejectIncorrectLengthKey() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - SecretKey localKey = new SecretKeySpec(Hex.decodeHex("0123456789ABCDEF" as char[]), "AES") - assert ![128, 192, 256].contains(localKey.encoded.length) - - final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Act - def msg = shouldFail(IllegalArgumentException) { - cipherProvider.getCipher(encryptionMethod, localKey, true) - } - - // Assert - assert msg =~ "The key must be of length \\[128, 192, 256\\]" - } - - @Test - public void testShouldRejectEmptyEncryptionMethod() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - // Act - def msg = shouldFail(IllegalArgumentException) { - cipherProvider.getCipher(null, key, true) - } - - // Assert - assert msg =~ "The encryption method must be specified" - } - - @Test - public void testShouldRejectUnsupportedEncryptionMethod() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES - - // Act - def msg = shouldFail(IllegalArgumentException) { - cipherProvider.getCipher(encryptionMethod, key, true) - } - - // Assert - assert msg =~ " requires a PBECipherProvider" - } - - @Test - public void testGetCipherShouldSupportExternalCompatibility() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - final String PLAINTEXT = "This is a plaintext message." - - // These values can be generated by running `$ ./openssl_aes.rb` in the terminal - final byte[] IV = Hex.decodeHex("e0bc8cc7fbc0bdfdc184dc22ce2fcb5b" as char[]) - final byte[] LOCAL_KEY = Hex.decodeHex("c72943d27c3e5a276169c5998a779117" as char[]) - final String CIPHER_TEXT = "a2725ea55c7dd717664d044cab0f0b5f763653e322c27df21954f5be394efb1b" - byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[]) - - SecretKey localKey = new SecretKeySpec(LOCAL_KEY, "AES") - - 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, localKey, IV, false) - byte[] recoveredBytes = cipher.doFinal(cipherBytes) - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: ${recovered}") - - // Assert - assert PLAINTEXT.equals(recovered) - } - - @Test - public void testGetCipherForDecryptShouldRequireIV() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - final String plaintext = "This is a plaintext message." - - // Act - keyedEncryptionMethods.each { EncryptionMethod em -> - logger.info("Using algorithm: ${em.getAlgorithm()}") - byte[] iv = cipherProvider.generateIV() - logger.info("IV: ${Hex.encodeHexString(iv)}") - - // Initialize a cipher for encryption - Cipher cipher = cipherProvider.getCipher(em, key, iv, true) - - byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8")) - logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}") - - def msg = shouldFail(IllegalArgumentException) { - cipher = cipherProvider.getCipher(em, key, false) - } - - // Assert - assert msg =~ "Cannot decrypt without a valid IV" - } - } - - @Test - public void testGetCipherShouldRejectInvalidIVLengths() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - 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, key, badIV, true) - - // Decrypt should fail - def msg = shouldFail(IllegalArgumentException) { - cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false) - } - logger.expected(msg) - - // Assert - assert msg =~ "Cannot decrypt without a valid IV" - } - } - - @Test - public void testGetCipherShouldRejectEmptyIV() throws Exception { - // Arrange - KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider() - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - byte[] badIV = [0x00 as byte] * 16 as byte[] - - // Act - 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, key, badIV, true) - logger.info("IV after encrypt: ${Hex.encodeHexString(cipher.getIV())}") - - // Decrypt should fail - def msg = shouldFail(IllegalArgumentException) { - cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false) - } - logger.expected(msg) - - // Assert - assert msg =~ "Cannot decrypt without a valid IV" - } -} \ 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/BcryptCipherProviderGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy deleted file mode 100644 index 396e3b2..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy +++ /dev/null @@ -1,539 +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.bcrypt.BCrypt -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.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 strongKDFEncryptionMethods - - private static final int DEFAULT_KEY_LENGTH = 128; - public static final String MICROBENCHMARK = "microbenchmark" - 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 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/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy deleted file mode 100644 index be8d5f4..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy +++ /dev/null @@ -1,97 +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.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 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/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy deleted file mode 100644 index 6a6a958..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy +++ /dev/null @@ -1,251 +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.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.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import java.security.Security - -@RunWith(JUnit4.class) -class CipherUtilityGroovyTest extends GroovyTestCase { - private static final Logger logger = LoggerFactory.getLogger(CipherUtilityGroovyTest.class) - - // TripleDES must precede DES for automatic grouping precedence - private static final List CIPHERS = ["AES", "TRIPLEDES", "DES", "RC2", "RC4", "RC5", "TWOFISH"] - private static final List SYMMETRIC_ALGORITHMS = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") || it.algorithm.startsWith("AES") }*.algorithm - private static final Map> ALGORITHMS_MAPPED_BY_CIPHER = SYMMETRIC_ALGORITHMS.groupBy { String algorithm -> CIPHERS.find { algorithm.contains(it) } } - - // Manually mapped as of 01/19/16 0.5.0 - private static final Map> ALGORITHMS_MAPPED_BY_KEY_LENGTH = [ - (40) : ["PBEWITHSHAAND40BITRC2-CBC", - "PBEWITHSHAAND40BITRC4"], - (64) : ["PBEWITHMD5ANDDES", - "PBEWITHSHA1ANDDES"], - (112): ["PBEWITHSHAAND2-KEYTRIPLEDES-CBC", - "PBEWITHSHAAND3-KEYTRIPLEDES-CBC"], - (128): ["PBEWITHMD5AND128BITAES-CBC-OPENSSL", - "PBEWITHMD5ANDRC2", - "PBEWITHSHA1ANDRC2", - "PBEWITHSHA256AND128BITAES-CBC-BC", - "PBEWITHSHAAND128BITAES-CBC-BC", - "PBEWITHSHAAND128BITRC2-CBC", - "PBEWITHSHAAND128BITRC4", - "PBEWITHSHAANDTWOFISH-CBC", - "AES/CBC/PKCS7Padding", - "AES/CTR/NoPadding", - "AES/GCM/NoPadding"], - (192): ["PBEWITHMD5AND192BITAES-CBC-OPENSSL", - "PBEWITHSHA256AND192BITAES-CBC-BC", - "PBEWITHSHAAND192BITAES-CBC-BC", - "AES/CBC/PKCS7Padding", - "AES/CTR/NoPadding", - "AES/GCM/NoPadding"], - (256): ["PBEWITHMD5AND256BITAES-CBC-OPENSSL", - "PBEWITHSHA256AND256BITAES-CBC-BC", - "PBEWITHSHAAND256BITAES-CBC-BC", - "AES/CBC/PKCS7Padding", - "AES/CTR/NoPadding", - "AES/GCM/NoPadding"] - ] - - @BeforeClass - static void setUpOnce() { - Security.addProvider(new BouncyCastleProvider()); - - // Fix because TRIPLEDES -> DESede - def tripleDESAlgorithms = ALGORITHMS_MAPPED_BY_CIPHER.remove("TRIPLEDES") - ALGORITHMS_MAPPED_BY_CIPHER.put("DESede", tripleDESAlgorithms) - - logger.info("Mapped algorithms: ${ALGORITHMS_MAPPED_BY_CIPHER}") - } - - @Before - void setUp() throws Exception { - - } - - @After - void tearDown() throws Exception { - - } - - @Test - void testShouldParseCipherFromAlgorithm() { - // Arrange - final def EXPECTED_ALGORITHMS = ALGORITHMS_MAPPED_BY_CIPHER - - // Act - SYMMETRIC_ALGORITHMS.each { String algorithm -> - String cipher = CipherUtility.parseCipherFromAlgorithm(algorithm) - logger.info("Extracted ${cipher} from ${algorithm}") - - // Assert - assert EXPECTED_ALGORITHMS.get(cipher).contains(algorithm) - } - } - - @Test - void testShouldParseKeyLengthFromAlgorithm() { - // Arrange - final def EXPECTED_ALGORITHMS = ALGORITHMS_MAPPED_BY_KEY_LENGTH - - // Act - SYMMETRIC_ALGORITHMS.each { String algorithm -> - int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm) - logger.info("Extracted ${keyLength} from ${algorithm}") - - // Assert - assert EXPECTED_ALGORITHMS.get(keyLength).contains(algorithm) - } - } - - @Test - void testShouldDetermineValidKeyLength() { - // Arrange - - // Act - ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List algorithms -> - algorithms.each { String algorithm -> - logger.info("Checking ${keyLength} for ${algorithm}") - - // Assert - assert CipherUtility.isValidKeyLength(keyLength, CipherUtility.parseCipherFromAlgorithm(algorithm)) - } - } - } - - @Test - void testShouldDetermineInvalidKeyLength() { - // Arrange - - // Act - ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List algorithms -> - algorithms.each { String algorithm -> - def invalidKeyLengths = [-1, 0, 1] - if (algorithm =~ "RC\\d") { - invalidKeyLengths += [39, 2049] - } else { - invalidKeyLengths += keyLength + 1 - } - logger.info("Checking ${invalidKeyLengths.join(", ")} for ${algorithm}") - - // Assert - invalidKeyLengths.each { int invalidKeyLength -> - assert !CipherUtility.isValidKeyLength(invalidKeyLength, CipherUtility.parseCipherFromAlgorithm(algorithm)) - } - } - } - } - - @Test - void testShouldDetermineValidKeyLengthForAlgorithm() { - // Arrange - - // Act - ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List algorithms -> - algorithms.each { String algorithm -> - logger.info("Checking ${keyLength} for ${algorithm}") - - // Assert - assert CipherUtility.isValidKeyLengthForAlgorithm(keyLength, algorithm) - } - } - } - - @Test - void testShouldDetermineInvalidKeyLengthForAlgorithm() { - // Arrange - - // Act - ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List algorithms -> - algorithms.each { String algorithm -> - def invalidKeyLengths = [-1, 0, 1] - if (algorithm =~ "RC\\d") { - invalidKeyLengths += [39, 2049] - } else { - invalidKeyLengths += keyLength + 1 - } - logger.info("Checking ${invalidKeyLengths.join(", ")} for ${algorithm}") - - // Assert - invalidKeyLengths.each { int invalidKeyLength -> - assert !CipherUtility.isValidKeyLengthForAlgorithm(invalidKeyLength, algorithm) - } - } - } - - // Extra hard-coded checks - String algorithm = "PBEWITHSHA256AND256BITAES-CBC-BC" - int invalidKeyLength = 192 - logger.info("Checking ${invalidKeyLength} for ${algorithm}") - assert !CipherUtility.isValidKeyLengthForAlgorithm(invalidKeyLength, algorithm) - } - - @Test - void testShouldGetValidKeyLengthsForAlgorithm() { - // Arrange - - def rcKeyLengths = (40..2048).asList() - def CIPHER_KEY_SIZES = [ - AES : [128, 192, 256], - DES : [56, 64], - DESede : [56, 64, 112, 128, 168, 192], - RC2 : rcKeyLengths, - RC4 : rcKeyLengths, - RC5 : rcKeyLengths, - TWOFISH: [128, 192, 256] - ] - - def SINGLE_KEY_SIZE_ALGORITHMS = EncryptionMethod.values()*.algorithm.findAll { CipherUtility.parseActualKeyLengthFromAlgorithm(it) != -1 } - logger.info("Single key size algorithms: ${SINGLE_KEY_SIZE_ALGORITHMS}") - def MULTIPLE_KEY_SIZE_ALGORITHMS = EncryptionMethod.values()*.algorithm - SINGLE_KEY_SIZE_ALGORITHMS - MULTIPLE_KEY_SIZE_ALGORITHMS.removeAll { it.contains("PGP") } - logger.info("Multiple key size algorithms: ${MULTIPLE_KEY_SIZE_ALGORITHMS}") - - // Act - SINGLE_KEY_SIZE_ALGORITHMS.each { String algorithm -> - def EXPECTED_KEY_SIZES = [CipherUtility.parseKeyLengthFromAlgorithm(algorithm)] - - def validKeySizes = CipherUtility.getValidKeyLengthsForAlgorithm(algorithm) - logger.info("Checking ${algorithm} ${validKeySizes} against expected ${EXPECTED_KEY_SIZES}") - - // Assert - assert validKeySizes == EXPECTED_KEY_SIZES - } - - // Act - MULTIPLE_KEY_SIZE_ALGORITHMS.each { String algorithm -> - String cipher = CipherUtility.parseCipherFromAlgorithm(algorithm) - def EXPECTED_KEY_SIZES = CIPHER_KEY_SIZES[cipher] - - def validKeySizes = CipherUtility.getValidKeyLengthsForAlgorithm(algorithm) - logger.info("Checking ${algorithm} ${validKeySizes} against expected ${EXPECTED_KEY_SIZES}") - - // Assert - assert validKeySizes == EXPECTED_KEY_SIZES - } - } -} 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/KeyedEncryptorGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy deleted file mode 100644 index 8e78778..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy +++ /dev/null @@ -1,122 +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.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