You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by al...@apache.org on 2017/05/02 17:27:32 UTC
[07/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
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/processors/standard/util/crypto/scrypt/Scrypt.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/scrypt/Scrypt.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/scrypt/Scrypt.java
deleted file mode 100644
index 7785e9e..0000000
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/scrypt/Scrypt.java
+++ /dev/null
@@ -1,511 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.processors.standard.util.crypto.scrypt;
-
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.processors.standard.util.crypto.CipherUtility;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.lang.Integer.MAX_VALUE;
-import static java.lang.System.arraycopy;
-
-
-/**
- * Copyright (C) 2011 - Will Glozer. All rights reserved.
- * <p/>
- * Taken from Will Glozer's port of Colin Percival's C implementation. Glozer's project located at <a href="https://github.com/wg/scrypt">https://github.com/wg/scrypt</a> 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.
- * <p/>
- * An implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf">scrypt</a>
- * key derivation function.
- * <p/>
- * Allows for hashing passwords using the
- * <a href="http://www.tarsnap.com/scrypt.html">scrypt</a> 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:
- * <p/>
- * The hashed output is an
- * extended implementation of the Modular Crypt Format that also includes the scrypt
- * algorithm parameters.
- * <p/>
- * Format: <code>$s0$PARAMS$SALT$KEY</code>.
- * <p/>
- * <dl>
- * <dd>PARAMS</dd><dt>32-bit hex integer containing log2(N) (16 bits), r (8 bits), and p (8 bits)</dt>
- * <dd>SALT</dd><dt>base64-encoded salt</dt>
- * <dd>KEY</dd><dt>base64-encoded derived key</dt>
- * </dl>
- * <p/>
- * <code>s0</code> identifies version 0 of the scrypt format, using a 128-bit salt and 256-bit derived key.
- * <p/>
- * 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<Integer> 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.
- * <p/>
- * Example:
- * <p/>
- * Hash: $s0$e0801$epIxT/h6HbbwHaehFnh/bw$7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0
- * Params: e0801
- * <p/>
- * 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<Integer> parseParameters(String encodedParams) {
- long params = Long.parseLong(encodedParams, 16);
-
- List<Integer> 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 <a href="http://www.tarsnap.com/scrypt/scrypt.pdf">scrypt KDF</a>.
- *
- * @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/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java
new file mode 100644
index 0000000..f28cde9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.security.util.crypto.bcrypt.BCrypt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BcryptCipherProvider extends RandomIVPBECipherProvider {
+ private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProvider.class);
+
+ private final int workFactor;
+ /**
+ * This can be calculated automatically using the code {@see BcryptCipherProviderGroovyTest#calculateMinimumWorkFactor} or manually updated by a maintainer
+ */
+ private static final int DEFAULT_WORK_FACTOR = 12;
+ private static final int DEFAULT_SALT_LENGTH = 16;
+
+ private static final Pattern BCRYPT_SALT_FORMAT = Pattern.compile("^\\$\\d\\w\\$\\d{2}\\$[\\w\\/\\.]{22}");
+
+ /**
+ * Instantiates a Bcrypt cipher provider with the default work factor 12 (2^12 key expansion rounds).
+ */
+ public BcryptCipherProvider() {
+ this(DEFAULT_WORK_FACTOR);
+ }
+
+ /**
+ * Instantiates a Bcrypt cipher provider with the specified work factor w (2^w key expansion rounds).
+ *
+ * @param workFactor the (log) number of key expansion rounds [4..30]
+ */
+ public BcryptCipherProvider(int workFactor) {
+ this.workFactor = workFactor;
+ if (workFactor < DEFAULT_WORK_FACTOR) {
+ logger.warn("The provided work factor {} is below the recommended minimum {}", workFactor, DEFAULT_WORK_FACTOR);
+ }
+ }
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. The IV is provided externally to allow for non-deterministic IVs, as IVs
+ * deterministically derived from the password are a potential vulnerability and compromise semantic security. See
+ * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a>
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param salt the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)})
+ * @param iv the IV
+ * @param keyLength the desired key length in bits
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ @Override
+ public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception {
+ try {
+ return getInitializedCipher(encryptionMethod, password, salt, iv, keyLength, encryptMode);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ProcessException("Error initializing the cipher", e);
+ }
+ }
+
+ @Override
+ Logger getLogger() {
+ return logger;
+ }
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation.
+ *
+ * The IV can be retrieved by the calling method using {@link Cipher#getIV()}.
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param salt the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)})
+ * @param keyLength the desired key length in bits
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ @Override
+ public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception {
+ return getCipher(encryptionMethod, password, salt, new byte[0], keyLength, encryptMode);
+ }
+
+ protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception {
+ if (encryptionMethod == null) {
+ throw new IllegalArgumentException("The encryption method must be specified");
+ }
+ if (!encryptionMethod.isCompatibleWithStrongKDFs()) {
+ throw new IllegalArgumentException(encryptionMethod.name() + " is not compatible with Bcrypt");
+ }
+
+ if (StringUtils.isEmpty(password)) {
+ throw new IllegalArgumentException("Encryption with an empty password is not supported");
+ }
+
+ String algorithm = encryptionMethod.getAlgorithm();
+ String provider = encryptionMethod.getProvider();
+
+ final String cipherName = CipherUtility.parseCipherFromAlgorithm(algorithm);
+ if (!CipherUtility.isValidKeyLength(keyLength, cipherName)) {
+ throw new IllegalArgumentException(String.valueOf(keyLength) + " is not a valid key length for " + cipherName);
+ }
+
+ String bcryptSalt = formatSaltForBcrypt(salt);
+
+ String hash = BCrypt.hashpw(password, bcryptSalt);
+
+ /* The SHA-512 hash is required in order to derive a key longer than 184 bits (the resulting size of the Bcrypt hash) and ensuring the avalanche effect causes higher key entropy (if all
+ derived keys follow a consistent pattern, it weakens the strength of the encryption) */
+ MessageDigest digest = MessageDigest.getInstance("SHA-512", provider);
+ byte[] dk = digest.digest(hash.getBytes(StandardCharsets.UTF_8));
+ dk = Arrays.copyOf(dk, keyLength / 8);
+ SecretKey tempKey = new SecretKeySpec(dk, algorithm);
+
+ KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider();
+ return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode);
+ }
+
+ private String formatSaltForBcrypt(byte[] salt) {
+ if (salt == null || salt.length == 0) {
+ throw new IllegalArgumentException("The salt cannot be empty. To generate a salt, use BcryptCipherProvider#generateSalt()");
+ }
+
+ String rawSalt = new String(salt, StandardCharsets.UTF_8);
+ Matcher matcher = BCRYPT_SALT_FORMAT.matcher(rawSalt);
+
+ if (matcher.find()) {
+ return rawSalt;
+ } else {
+ throw new IllegalArgumentException("The salt must be of the format $2a$10$gUVbkVzp79H8YaCOsCVZNu. To generate a salt, use BcryptCipherProvider#generateSalt()");
+ }
+ }
+
+ @Override
+ public byte[] generateSalt() {
+ return BCrypt.gensalt(workFactor).getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public int getDefaultSaltLength() {
+ return DEFAULT_SALT_LENGTH;
+ }
+
+ protected int getWorkFactor() {
+ return workFactor;
+ }
+}
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/CipherProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java
new file mode 100644
index 0000000..09004bf
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.KeyDerivationFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CipherProviderFactory {
+ private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactory.class);
+
+ private static Map<KeyDerivationFunction, Class<? extends CipherProvider>> registeredCipherProviders;
+
+ static {
+ registeredCipherProviders = new HashMap<>();
+ registeredCipherProviders.put(KeyDerivationFunction.NIFI_LEGACY, NiFiLegacyCipherProvider.class);
+ registeredCipherProviders.put(KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY, OpenSSLPKCS5CipherProvider.class);
+ registeredCipherProviders.put(KeyDerivationFunction.PBKDF2, PBKDF2CipherProvider.class);
+ registeredCipherProviders.put(KeyDerivationFunction.BCRYPT, BcryptCipherProvider.class);
+ registeredCipherProviders.put(KeyDerivationFunction.SCRYPT, ScryptCipherProvider.class);
+ registeredCipherProviders.put(KeyDerivationFunction.NONE, AESKeyedCipherProvider.class);
+ }
+
+ public static CipherProvider getCipherProvider(KeyDerivationFunction kdf) {
+ logger.debug("{} KDFs registered", registeredCipherProviders.size());
+
+ if (registeredCipherProviders.containsKey(kdf)) {
+ Class<? extends CipherProvider> clazz = registeredCipherProviders.get(kdf);
+ try {
+ return clazz.newInstance();
+ } catch (Exception e) {
+ logger.error("Error instantiating new {} with default parameters for {}", clazz.getName(), kdf.getName());
+ throw new ProcessException("Error instantiating cipher provider");
+ }
+ }
+
+ throw new IllegalArgumentException("No cipher provider registered for " + kdf.getName());
+ }
+}
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/KeyedEncryptor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/KeyedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/KeyedEncryptor.java
new file mode 100644
index 0000000..011eb1f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/KeyedEncryptor.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.security.util.KeyDerivationFunction;
+
+public class KeyedEncryptor implements Encryptor {
+
+ private EncryptionMethod encryptionMethod;
+ private SecretKey key;
+ private byte[] iv;
+
+ private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128;
+
+ private static boolean isUnlimitedStrengthCryptographyEnabled;
+
+ // Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system
+ static {
+ try {
+ isUnlimitedStrengthCryptographyEnabled = (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH);
+ } catch (NoSuchAlgorithmException e) {
+ // if there are issues with this, we default back to the value established
+ isUnlimitedStrengthCryptographyEnabled = false;
+ }
+ }
+
+ public KeyedEncryptor(final EncryptionMethod encryptionMethod, final SecretKey key) {
+ this(encryptionMethod, key == null ? new byte[0] : key.getEncoded(), new byte[0]);
+ }
+
+ public KeyedEncryptor(final EncryptionMethod encryptionMethod, final SecretKey key, final byte[] iv) {
+ this(encryptionMethod, key == null ? new byte[0] : key.getEncoded(), iv);
+ }
+
+ public KeyedEncryptor(final EncryptionMethod encryptionMethod, final byte[] keyBytes) {
+ this(encryptionMethod, keyBytes, new byte[0]);
+ }
+
+ public KeyedEncryptor(final EncryptionMethod encryptionMethod, final byte[] keyBytes, final byte[] iv) {
+ super();
+ try {
+ if (encryptionMethod == null) {
+ throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with null encryption method");
+ }
+ if (!encryptionMethod.isKeyedCipher()) {
+ throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with encryption method " + encryptionMethod.name());
+ }
+ this.encryptionMethod = encryptionMethod;
+ if (keyBytes == null || keyBytes.length == 0) {
+ throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with empty key");
+ }
+ if (!CipherUtility.isValidKeyLengthForAlgorithm(keyBytes.length * 8, encryptionMethod.getAlgorithm())) {
+ throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with key of length " + keyBytes.length);
+ }
+ String cipherName = CipherUtility.parseCipherFromAlgorithm(encryptionMethod.getAlgorithm());
+ this.key = new SecretKeySpec(keyBytes, cipherName);
+
+ this.iv = iv;
+ } catch (Exception e) {
+ throw new ProcessException(e);
+ }
+ }
+
+ public static int getMaxAllowedKeyLength(final String algorithm) {
+ if (StringUtils.isEmpty(algorithm)) {
+ return DEFAULT_MAX_ALLOWED_KEY_LENGTH;
+ }
+ String parsedCipher = CipherUtility.parseCipherFromAlgorithm(algorithm);
+ try {
+ return Cipher.getMaxAllowedKeyLength(parsedCipher);
+ } catch (NoSuchAlgorithmException e) {
+ // Default algorithm max key length on unmodified JRE
+ return DEFAULT_MAX_ALLOWED_KEY_LENGTH;
+ }
+ }
+
+ public static boolean supportsUnlimitedStrength() {
+ return isUnlimitedStrengthCryptographyEnabled;
+ }
+
+ @Override
+ public StreamCallback getEncryptionCallback() throws ProcessException {
+ return new EncryptCallback();
+ }
+
+ @Override
+ public StreamCallback getDecryptionCallback() throws ProcessException {
+ return new DecryptCallback();
+ }
+
+ private class DecryptCallback implements StreamCallback {
+
+ public DecryptCallback() {
+ }
+
+ @Override
+ public void process(final InputStream in, final OutputStream out) throws IOException {
+ // Initialize cipher provider
+ KeyedCipherProvider cipherProvider = (KeyedCipherProvider) CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
+
+ // Generate cipher
+ try {
+ Cipher cipher;
+ // The IV could have been set by the constructor, but if not, read from the cipher stream
+ if (iv.length == 0) {
+ iv = cipherProvider.readIV(in);
+ }
+ cipher = cipherProvider.getCipher(encryptionMethod, key, iv, false);
+ CipherUtility.processStreams(cipher, in, out);
+ } catch (Exception e) {
+ throw new ProcessException(e);
+ }
+ }
+ }
+
+ private class EncryptCallback implements StreamCallback {
+
+ public EncryptCallback() {
+ }
+
+ @Override
+ public void process(final InputStream in, final OutputStream out) throws IOException {
+ // Initialize cipher provider
+ KeyedCipherProvider cipherProvider = (KeyedCipherProvider) CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
+
+ // Generate cipher
+ try {
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, iv, true);
+ cipherProvider.writeIV(cipher.getIV(), out);
+ CipherUtility.processStreams(cipher, in, out);
+ } catch (Exception e) {
+ throw new ProcessException(e);
+ }
+ }
+ }
+}
\ 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/main/java/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProvider.java
new file mode 100644
index 0000000..df8a54d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProvider.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SecureRandom;
+import javax.crypto.Cipher;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.stream.io.StreamUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides a cipher initialized with the original NiFi key derivation process for password-based encryption (MD5 @ 1000 iterations). This is not a secure
+ * {@link org.apache.nifi.security.util.KeyDerivationFunction} (KDF) and should no longer be used.
+ * It is provided only for backward-compatibility with legacy data. A strong KDF should be selected for any future use.
+ *
+ * @see BcryptCipherProvider
+ * @see ScryptCipherProvider
+ * @see PBKDF2CipherProvider
+ */
+@Deprecated
+public class NiFiLegacyCipherProvider extends OpenSSLPKCS5CipherProvider implements PBECipherProvider {
+ private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProvider.class);
+
+ // Legacy magic number value
+ private static final int ITERATION_COUNT = 1000;
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived using the NiFi legacy code, based on @see org.apache.nifi.crypto
+ * .OpenSSLPKCS5CipherProvider#getCipher(java.lang.String, java.lang.String, java.lang.String, byte[], boolean) [essentially {@code MD5(password || salt) * 1000 }].
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param salt the salt
+ * @param keyLength the desired key length in bits (ignored because OpenSSL ciphers provide key length in algorithm name)
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ @Override
+ public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception {
+ try {
+ // This method is defined in the OpenSSL implementation and just uses a locally-overridden iteration count
+ return getInitializedCipher(encryptionMethod, password, salt, encryptMode);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ProcessException("Error initializing the cipher", e);
+ }
+ }
+
+ public byte[] generateSalt(EncryptionMethod encryptionMethod) {
+ byte[] salt = new byte[calculateSaltLength(encryptionMethod)];
+ new SecureRandom().nextBytes(salt);
+ return salt;
+ }
+
+ protected void validateSalt(EncryptionMethod encryptionMethod, byte[] salt) {
+ final int saltLength = calculateSaltLength(encryptionMethod);
+ if (salt.length != saltLength && salt.length != 0) {
+ throw new IllegalArgumentException("Salt must be " + saltLength + " bytes or empty");
+ }
+ }
+
+ private int calculateSaltLength(EncryptionMethod encryptionMethod) {
+ try {
+ Cipher cipher = Cipher.getInstance(encryptionMethod.getAlgorithm(), encryptionMethod.getProvider());
+ return cipher.getBlockSize() > 0 ? cipher.getBlockSize() : getDefaultSaltLength();
+ } catch (Exception e) {
+ logger.warn("Encountered exception determining salt length from encryption method {}", encryptionMethod.getAlgorithm(), e);
+ final int defaultSaltLength = getDefaultSaltLength();
+ logger.warn("Returning default length: {} bytes", defaultSaltLength);
+ return defaultSaltLength;
+ }
+ }
+
+ @Override
+ public byte[] readSalt(InputStream in) throws IOException, ProcessException {
+ return readSalt(EncryptionMethod.AES_CBC, in);
+ }
+
+ /**
+ * Returns the salt provided as part of the cipher stream, or throws an exception if one cannot be detected.
+ * This method is only implemented by {@link NiFiLegacyCipherProvider} because the legacy salt generation was dependent on the cipher block size.
+ *
+ * @param encryptionMethod the encryption method
+ * @param in the cipher InputStream
+ * @return the salt
+ */
+ public byte[] readSalt(EncryptionMethod encryptionMethod, InputStream in) throws IOException {
+ if (in == null) {
+ throw new IllegalArgumentException("Cannot read salt from null InputStream");
+ }
+
+ // The first 8-16 bytes (depending on the cipher blocksize) of the input stream are the salt
+ final int saltLength = calculateSaltLength(encryptionMethod);
+ if (in.available() < saltLength) {
+ throw new ProcessException("The cipher stream is too small to contain the salt");
+ }
+ byte[] salt = new byte[saltLength];
+ StreamUtils.fillBuffer(in, salt);
+ return salt;
+ }
+
+ @Override
+ public void writeSalt(byte[] salt, OutputStream out) throws IOException {
+ if (out == null) {
+ throw new IllegalArgumentException("Cannot write salt to null OutputStream");
+ }
+ out.write(salt);
+ }
+
+ @Override
+ protected int getIterationCount() {
+ return ITERATION_COUNT;
+ }
+}
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/OpenPGPKeyBasedEncryptor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptor.java
new file mode 100644
index 0000000..6b6c2fc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptor.java
@@ -0,0 +1,379 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import static org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE;
+import static org.apache.nifi.processors.standard.util.PGPUtil.BUFFER_SIZE;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.zip.Deflater;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processors.standard.EncryptContent;
+import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
+import org.bouncycastle.openpgp.PGPEncryptedData;
+import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
+import org.bouncycastle.openpgp.PGPEncryptedDataList;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPLiteralData;
+import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPOnePassSignatureList;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OpenPGPKeyBasedEncryptor implements Encryptor {
+ private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class);
+
+ private String algorithm;
+ private String provider;
+ // TODO: This can hold either the secret or public keyring path
+ private String keyring;
+ private String userId;
+ private char[] passphrase;
+ private String filename;
+
+ public OpenPGPKeyBasedEncryptor(final String algorithm, final String provider, final String keyring, final String userId, final char[] passphrase, final String filename) {
+ this.algorithm = algorithm;
+ this.provider = provider;
+ this.keyring = keyring;
+ this.userId = userId;
+ this.passphrase = passphrase;
+ this.filename = filename;
+ }
+
+ @Override
+ public StreamCallback getEncryptionCallback() throws Exception {
+ return new OpenPGPEncryptCallback(algorithm, provider, keyring, userId, filename);
+ }
+
+ @Override
+ public StreamCallback getDecryptionCallback() throws Exception {
+ return new OpenPGPDecryptCallback(provider, keyring, passphrase);
+ }
+
+ /**
+ * Returns true if the passphrase is valid.
+ * <p>
+ * This is used in the EncryptContent custom validation to check if the passphrase can extract a private key from the secret key ring. After BC was upgraded from 1.46 to 1.53, the API changed
+ * so this is performed differently but the functionality is equivalent.
+ *
+ * @param provider the provider name
+ * @param secretKeyringFile the file path to the keyring
+ * @param passphrase the passphrase
+ * @return true if the passphrase can successfully extract any private key
+ * @throws IOException if there is a problem reading the keyring file
+ * @throws PGPException if there is a problem parsing/extracting the private key
+ * @throws NoSuchProviderException if the provider is not available
+ */
+ public static boolean validateKeyring(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException, NoSuchProviderException {
+ try {
+ getDecryptedPrivateKey(provider, secretKeyringFile, passphrase);
+ return true;
+ } catch (Exception e) {
+ // If this point is reached, no private key could be extracted with the given passphrase
+ return false;
+ }
+ }
+
+ private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException {
+ // TODO: Verify that key IDs cannot be 0
+ return getDecryptedPrivateKey(provider, secretKeyringFile, 0L, passphrase);
+ }
+
+ private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, long keyId, char[] passphrase) throws IOException, PGPException {
+ // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated
+
+ // Read in from the secret keyring file
+ try (FileInputStream keyInputStream = new FileInputStream(secretKeyringFile)) {
+
+ // Form the SecretKeyRing collection (1.53 way with fingerprint calculator)
+ PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator());
+
+ // The decryptor is identical for all keys
+ final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(provider).build(passphrase);
+
+ // Iterate over all secret keyrings
+ Iterator<PGPSecretKeyRing> keyringIterator = pgpSecretKeyRingCollection.getKeyRings();
+ PGPSecretKeyRing keyRing;
+ PGPSecretKey secretKey;
+
+ while (keyringIterator.hasNext()) {
+ keyRing = keyringIterator.next();
+
+ // If keyId exists, get a specific secret key; else, iterate over all
+ if (keyId != 0) {
+ secretKey = keyRing.getSecretKey(keyId);
+ try {
+ return secretKey.extractPrivateKey(decryptor);
+ } catch (Exception e) {
+ throw new PGPException("No private key available using passphrase", e);
+ }
+ } else {
+ Iterator<PGPSecretKey> keyIterator = keyRing.getSecretKeys();
+
+ while (keyIterator.hasNext()) {
+ secretKey = keyIterator.next();
+ try {
+ return secretKey.extractPrivateKey(decryptor);
+ } catch (Exception e) {
+ // TODO: Log (expected) failures?
+ }
+ }
+ }
+ }
+ }
+
+ // If this point is reached, no private key could be extracted with the given passphrase
+ throw new PGPException("No private key available using passphrase");
+ }
+
+ /*
+ * Get the public key for a specific user id from a keyring.
+ */
+ @SuppressWarnings("rawtypes")
+ public static PGPPublicKey getPublicKey(String userId, String publicKeyringFile) throws IOException, PGPException {
+ // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated
+
+ // Read in from the public keyring file
+ try (FileInputStream keyInputStream = new FileInputStream(publicKeyringFile)) {
+
+ // Form the PublicKeyRing collection (1.53 way with fingerprint calculator)
+ PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator());
+
+ // Iterate over all public keyrings
+ Iterator<PGPPublicKeyRing> iter = pgpPublicKeyRingCollection.getKeyRings();
+ PGPPublicKeyRing keyRing;
+ while (iter.hasNext()) {
+ keyRing = iter.next();
+
+ // Iterate over each public key in this keyring
+ Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
+ while (keyIter.hasNext()) {
+ PGPPublicKey publicKey = keyIter.next();
+
+ // Iterate over each userId attached to the public key
+ Iterator userIdIterator = publicKey.getUserIDs();
+ while (userIdIterator.hasNext()) {
+ String id = (String) userIdIterator.next();
+ if (userId.equalsIgnoreCase(id)) {
+ return publicKey;
+ }
+ }
+ }
+ }
+ }
+
+ // If this point is reached, no public key could be extracted with the given userId
+ throw new PGPException("Could not find a public key with the given userId");
+ }
+
+ private static class OpenPGPDecryptCallback implements StreamCallback {
+
+ private String provider;
+ private String secretKeyringFile;
+ private char[] passphrase;
+
+ OpenPGPDecryptCallback(final String provider, final String secretKeyringFile, final char[] passphrase) {
+ this.provider = provider;
+ this.secretKeyringFile = secretKeyringFile;
+ this.passphrase = passphrase;
+ }
+
+ @Override
+ public void process(InputStream in, OutputStream out) throws IOException {
+ try (InputStream pgpin = PGPUtil.getDecoderStream(in)) {
+ PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin, new BcKeyFingerprintCalculator());
+
+ Object obj = pgpFactory.nextObject();
+ if (!(obj instanceof PGPEncryptedDataList)) {
+ obj = pgpFactory.nextObject();
+ if (!(obj instanceof PGPEncryptedDataList)) {
+ throw new ProcessException("Invalid OpenPGP data");
+ }
+ }
+ PGPEncryptedDataList encList = (PGPEncryptedDataList) obj;
+
+ try {
+ PGPPrivateKey privateKey = null;
+ PGPPublicKeyEncryptedData encData = null;
+
+ // Find the secret key in the encrypted data
+ Iterator it = encList.getEncryptedDataObjects();
+ while (privateKey == null && it.hasNext()) {
+ obj = it.next();
+ if (!(obj instanceof PGPPublicKeyEncryptedData)) {
+ throw new ProcessException("Invalid OpenPGP data");
+ }
+ encData = (PGPPublicKeyEncryptedData) obj;
+
+ // Check each encrypted data object to see if it contains the key ID for the secret key -> private key
+ try {
+ privateKey = getDecryptedPrivateKey(provider, secretKeyringFile, encData.getKeyID(), passphrase);
+ } catch (PGPException e) {
+ // TODO: Log (expected) exception?
+ }
+ }
+ if (privateKey == null) {
+ throw new ProcessException("Secret keyring does not contain the key required to decrypt");
+ }
+
+ // Read in the encrypted data stream and decrypt it
+ final PublicKeyDataDecryptorFactory dataDecryptor = new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(provider).build(privateKey);
+ try (InputStream clear = encData.getDataStream(dataDecryptor)) {
+ // Create a plain object factory
+ JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);
+
+ Object message = plainFact.nextObject();
+
+ // Check the message type and act accordingly
+
+ // If compressed, decompress
+ if (message instanceof PGPCompressedData) {
+ PGPCompressedData cData = (PGPCompressedData) message;
+ JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream());
+
+ message = pgpFact.nextObject();
+ }
+
+ // If the message is literal data, read it and process to the out stream
+ if (message instanceof PGPLiteralData) {
+ PGPLiteralData literalData = (PGPLiteralData) message;
+
+ try (InputStream lis = literalData.getInputStream()) {
+ final byte[] buffer = new byte[BLOCK_SIZE];
+ int len;
+ while ((len = lis.read(buffer)) >= 0) {
+ out.write(buffer, 0, len);
+ }
+ }
+ } else if (message instanceof PGPOnePassSignatureList) {
+ // TODO: This is legacy code but should verify signature list here
+ throw new PGPException("encrypted message contains a signed message - not literal data.");
+ } else {
+ throw new PGPException("message is not a simple encrypted file - type unknown.");
+ }
+
+ if (encData.isIntegrityProtected()) {
+ if (!encData.verify()) {
+ throw new PGPException("Failed message integrity check");
+ }
+ } else {
+ logger.warn("No message integrity check");
+ }
+ }
+ } catch (Exception e) {
+ throw new ProcessException(e.getMessage());
+ }
+ }
+ }
+
+ }
+
+ private static class OpenPGPEncryptCallback implements StreamCallback {
+
+ private String algorithm;
+ private String provider;
+ private String publicKeyring;
+ private String userId;
+ private String filename;
+
+ OpenPGPEncryptCallback(final String algorithm, final String provider, final String keyring, final String userId, final String filename) {
+ this.algorithm = algorithm;
+ this.provider = provider;
+ this.publicKeyring = keyring;
+ this.userId = userId;
+ this.filename = filename;
+ }
+
+ @Override
+ public void process(InputStream in, OutputStream out) throws IOException {
+ PGPPublicKey publicKey;
+ final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm);
+
+ try {
+ publicKey = getPublicKey(userId, publicKeyring);
+ } catch (Exception e) {
+ throw new ProcessException("Invalid public keyring - " + e.getMessage());
+ }
+
+ try {
+ OutputStream output = out;
+ if (isArmored) {
+ output = new ArmoredOutputStream(out);
+ }
+
+ try {
+ // TODO: Refactor internal symmetric encryption algorithm to be customizable
+ PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
+ new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider));
+
+ encryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider(provider));
+
+ // TODO: Refactor shared encryption code to utility
+ try (OutputStream encryptedOut = encryptedDataGenerator.open(output, new byte[BUFFER_SIZE])) {
+ PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED);
+ try (OutputStream compressedOut = compressedDataGenerator.open(encryptedOut, new byte[BUFFER_SIZE])) {
+ PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
+ try (OutputStream literalOut = literalDataGenerator.open(compressedOut, PGPLiteralData.BINARY, filename, new Date(), new byte[BUFFER_SIZE])) {
+
+ final byte[] buffer = new byte[BLOCK_SIZE];
+ int len;
+ while ((len = in.read(buffer)) >= 0) {
+ literalOut.write(buffer, 0, len);
+ }
+ }
+ }
+ }
+ } finally {
+ if (isArmored) {
+ output.close();
+ }
+ }
+ } catch (Exception e) {
+ throw new ProcessException(e.getMessage());
+ }
+ }
+ }
+}
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/OpenPGPPasswordBasedEncryptor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptor.java
new file mode 100644
index 0000000..6d5bb6d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptor.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import static org.bouncycastle.openpgp.PGPUtil.getDecoderStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPEncryptedData;
+import org.bouncycastle.openpgp.PGPEncryptedDataList;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPLiteralData;
+import org.bouncycastle.openpgp.PGPPBEEncryptedData;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OpenPGPPasswordBasedEncryptor implements Encryptor {
+ private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class);
+
+ private String algorithm;
+ private String provider;
+ private char[] password;
+ private String filename;
+
+ public OpenPGPPasswordBasedEncryptor(final String algorithm, final String provider, final char[] passphrase, final String filename) {
+ this.algorithm = algorithm;
+ this.provider = provider;
+ this.password = passphrase;
+ this.filename = filename;
+ }
+
+ @Override
+ public StreamCallback getEncryptionCallback() throws Exception {
+ return new OpenPGPEncryptCallback(algorithm, provider, password, filename);
+ }
+
+ @Override
+ public StreamCallback getDecryptionCallback() throws Exception {
+ return new OpenPGPDecryptCallback(provider, password);
+ }
+
+ private static class OpenPGPDecryptCallback implements StreamCallback {
+
+ private String provider;
+ private char[] password;
+
+ OpenPGPDecryptCallback(final String provider, final char[] password) {
+ this.provider = provider;
+ this.password = password;
+ }
+
+ @Override
+ public void process(InputStream in, OutputStream out) throws IOException {
+ InputStream pgpin = getDecoderStream(in);
+ JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(pgpin);
+
+ Object obj = pgpFactory.nextObject();
+ if (!(obj instanceof PGPEncryptedDataList)) {
+ obj = pgpFactory.nextObject();
+ if (!(obj instanceof PGPEncryptedDataList)) {
+ throw new ProcessException("Invalid OpenPGP data");
+ }
+ }
+ PGPEncryptedDataList encList = (PGPEncryptedDataList) obj;
+
+ obj = encList.get(0);
+ if (!(obj instanceof PGPPBEEncryptedData)) {
+ throw new ProcessException("Invalid OpenPGP data");
+ }
+ PGPPBEEncryptedData encryptedData = (PGPPBEEncryptedData) obj;
+
+ try {
+ final PGPDigestCalculatorProvider digestCalculatorProvider = new JcaPGPDigestCalculatorProviderBuilder().setProvider(provider).build();
+ final PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(digestCalculatorProvider).setProvider(provider).build(password);
+ InputStream clear = encryptedData.getDataStream(decryptorFactory);
+
+ JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(clear);
+
+ obj = pgpObjectFactory.nextObject();
+ if (obj instanceof PGPCompressedData) {
+ PGPCompressedData compressedData = (PGPCompressedData) obj;
+ pgpObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream());
+ obj = pgpObjectFactory.nextObject();
+ }
+
+ PGPLiteralData literalData = (PGPLiteralData) obj;
+ InputStream plainIn = literalData.getInputStream();
+ final byte[] buffer = new byte[org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE];
+ int len;
+ while ((len = plainIn.read(buffer)) >= 0) {
+ out.write(buffer, 0, len);
+ }
+
+ if (encryptedData.isIntegrityProtected()) {
+ if (!encryptedData.verify()) {
+ throw new PGPException("Integrity check failed");
+ }
+ } else {
+ logger.warn("No message integrity check");
+ }
+ } catch (Exception e) {
+ throw new ProcessException(e.getMessage());
+ }
+ }
+ }
+
+ private static class OpenPGPEncryptCallback implements StreamCallback {
+
+ private String algorithm;
+ private String provider;
+ private char[] password;
+ private String filename;
+
+ OpenPGPEncryptCallback(final String algorithm, final String provider, final char[] password, final String filename) {
+ this.algorithm = algorithm;
+ this.provider = provider;
+ this.password = password;
+ this.filename = filename;
+ }
+
+ @Override
+ public void process(InputStream in, OutputStream out) throws IOException {
+ try {
+ PGPKeyEncryptionMethodGenerator encryptionMethodGenerator = new JcePBEKeyEncryptionMethodGenerator(password).setProvider(provider);
+ org.apache.nifi.processors.standard.util.PGPUtil.encrypt(in, out, algorithm, provider, PGPEncryptedData.AES_128, filename, encryptionMethodGenerator);
+ } catch (Exception e) {
+ throw new ProcessException(e.getMessage());
+ }
+ }
+ }
+}
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/OpenSSLPKCS5CipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProvider.java
new file mode 100644
index 0000000..597e516
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProvider.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.stream.io.StreamUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OpenSSLPKCS5CipherProvider implements PBECipherProvider {
+ private static final Logger logger = LoggerFactory.getLogger(OpenSSLPKCS5CipherProvider.class);
+
+ // Legacy magic number value
+ private static final int ITERATION_COUNT = 0;
+ private static final int DEFAULT_SALT_LENGTH = 8;
+ private static final byte[] EMPTY_SALT = new byte[8];
+
+ private static final String OPENSSL_EVP_HEADER_MARKER = "Salted__";
+ private static final int OPENSSL_EVP_HEADER_SIZE = 8;
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived using the
+ * <a href="https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html">OpenSSL EVP_BytesToKey proprietary KDF</a> [essentially {@code MD5(password || salt) }].
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param salt the salt
+ * @param keyLength the desired key length in bits (ignored because OpenSSL ciphers provide key length in algorithm name)
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ @Override
+ public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception {
+ try {
+ return getInitializedCipher(encryptionMethod, password, salt, encryptMode);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ProcessException("Error initializing the cipher", e);
+ }
+ }
+
+ /**
+ * Convenience method without key length parameter. See {@link OpenSSLPKCS5CipherProvider#getCipher(EncryptionMethod, String, int, boolean)}
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ public Cipher getCipher(EncryptionMethod encryptionMethod, String password, boolean encryptMode) throws Exception {
+ return getCipher(encryptionMethod, password, new byte[0], -1, encryptMode);
+ }
+
+ /**
+ * Convenience method without key length parameter. See {@link OpenSSLPKCS5CipherProvider#getCipher(EncryptionMethod, String, byte[], int, boolean)}
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param salt the salt
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, boolean encryptMode) throws Exception {
+ return getCipher(encryptionMethod, password, salt, -1, encryptMode);
+ }
+
+ protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, boolean encryptMode)
+ throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException,
+ InvalidAlgorithmParameterException {
+ if (encryptionMethod == null) {
+ throw new IllegalArgumentException("The encryption method must be specified");
+ }
+
+ if (StringUtils.isEmpty(password)) {
+ throw new IllegalArgumentException("Encryption with an empty password is not supported");
+ }
+
+ validateSalt(encryptionMethod, salt);
+
+ String algorithm = encryptionMethod.getAlgorithm();
+ String provider = encryptionMethod.getProvider();
+
+ // Initialize secret key from password
+ final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
+ final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, provider);
+ SecretKey tempKey = factory.generateSecret(pbeKeySpec);
+
+ final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationCount());
+ Cipher cipher = Cipher.getInstance(algorithm, provider);
+ cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, tempKey, parameterSpec);
+ return cipher;
+ }
+
+ protected void validateSalt(EncryptionMethod encryptionMethod, byte[] salt) {
+ if (salt.length != DEFAULT_SALT_LENGTH && salt.length != 0) {
+ // This does not enforce ASCII encoding, just length
+ throw new IllegalArgumentException("Salt must be 8 bytes US-ASCII encoded or empty");
+ }
+ }
+
+ protected int getIterationCount() {
+ return ITERATION_COUNT;
+ }
+
+ @Override
+ public byte[] generateSalt() {
+ byte[] salt = new byte[getDefaultSaltLength()];
+ new SecureRandom().nextBytes(salt);
+ return salt;
+ }
+
+ @Override
+ public int getDefaultSaltLength() {
+ return DEFAULT_SALT_LENGTH;
+ }
+
+ /**
+ * Returns the salt provided as part of the cipher stream, or throws an exception if one cannot be detected.
+ *
+ * @param in the cipher InputStream
+ * @return the salt
+ */
+ @Override
+ public byte[] readSalt(InputStream in) throws IOException {
+ if (in == null) {
+ throw new IllegalArgumentException("Cannot read salt from null InputStream");
+ }
+
+ // The header and salt format is "Salted__salt x8b" in ASCII
+ byte[] salt = new byte[DEFAULT_SALT_LENGTH];
+
+ // Try to read the header and salt from the input
+ byte[] header = new byte[OPENSSL_EVP_HEADER_SIZE];
+
+ // Mark the stream in case there is no salt
+ in.mark(OPENSSL_EVP_HEADER_SIZE + 1);
+ StreamUtils.fillBuffer(in, header);
+
+ final byte[] headerMarkerBytes = OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII);
+
+ if (!Arrays.equals(headerMarkerBytes, header)) {
+ // No salt present
+ salt = new byte[0];
+ // Reset the stream because we skipped 8 bytes of cipher text
+ in.reset();
+ }
+
+ StreamUtils.fillBuffer(in, salt);
+ return salt;
+ }
+
+ @Override
+ public void writeSalt(byte[] salt, OutputStream out) throws IOException {
+ if (out == null) {
+ throw new IllegalArgumentException("Cannot write salt to null OutputStream");
+ }
+
+ out.write(OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII));
+ out.write(salt);
+ }
+}
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/PBECipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/PBECipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/PBECipherProvider.java
new file mode 100644
index 0000000..235af00
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/PBECipherProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import javax.crypto.Cipher;
+import org.apache.nifi.security.util.EncryptionMethod;
+
+public interface PBECipherProvider extends CipherProvider {
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation.
+ * <p/>
+ * The IV can be retrieved by the calling method using {@link Cipher#getIV()}.
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param password the secret input
+ * @param salt the salt
+ * @param keyLength the desired key length in bits
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception;
+
+ /**
+ * Returns a random salt suitable for this cipher provider.
+ *
+ * @return a random salt
+ * @see PBECipherProvider#getDefaultSaltLength()
+ */
+ byte[] generateSalt();
+
+ /**
+ * Returns the default salt length for this implementation.
+ *
+ * @return the default salt length in bytes
+ */
+ int getDefaultSaltLength();
+
+ /**
+ * Returns the salt provided as part of the cipher stream, or throws an exception if one cannot be detected.
+ *
+ * @param in the cipher InputStream
+ * @return the salt
+ */
+ byte[] readSalt(InputStream in) throws IOException;
+
+ /**
+ * Writes the salt provided as part of the cipher stream, or throws an exception if it cannot be written.
+ *
+ * @param salt the salt
+ * @param out the cipher OutputStream
+ */
+ void writeSalt(byte[] salt, OutputStream out) throws IOException;
+}