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;
+}