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