From a849ca044c7f41f1f3c445abfe9164e248f62fe5 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Thu, 12 Oct 2023 08:14:41 -0500 Subject: [PATCH] NIFI-12218 Removed SensitiveValueEncoder and SecureHasher - SensitiveValueEncoder and SecureHasher are no longer required following the removal of support for flow.xml.gz Signed-off-by: Pierre Villard This closes #7873. --- nifi-commons/nifi-security-utils/pom.xml | 4 - .../security/util/KeyDerivationFunction.java | 67 --- .../util/crypto/AbstractSecureHasher.java | 273 ------------ .../util/crypto/Argon2SecureHasher.java | 302 ------------- .../util/crypto/PBKDF2SecureHasher.java | 296 ------------- .../security/util/crypto/SecureHasher.java | 81 ---- .../util/crypto/SecureHasherFactory.java | 57 --- .../util/crypto/Argon2SecureHasherTest.java | 414 ------------------ .../util/crypto/PBKDF2SecureHasherTest.java | 322 -------------- .../util/crypto/SecureHasherFactoryTest.java | 68 --- .../nifi/encrypt/SensitiveValueEncoder.java | 29 -- .../StandardSensitiveValueEncoder.java | 81 ---- .../nifi/controller/FlowController.java | 13 - 13 files changed, 2007 deletions(-) delete mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java delete mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AbstractSecureHasher.java delete mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/Argon2SecureHasher.java delete mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasher.java delete mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasher.java delete mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasherFactory.java delete mode 100644 nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/Argon2SecureHasherTest.java delete mode 100644 nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasherTest.java delete mode 100644 nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/SecureHasherFactoryTest.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/SensitiveValueEncoder.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/StandardSensitiveValueEncoder.java diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml index a6bfc32c11..69f93ddcb3 100644 --- a/nifi-commons/nifi-security-utils/pom.xml +++ b/nifi-commons/nifi-security-utils/pom.xml @@ -35,10 +35,6 @@ org.apache.nifi nifi-utils - - org.apache.nifi - nifi-deprecation-log - org.apache.nifi nifi-security-utils-api diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java deleted file mode 100644 index 5c79df7efd..0000000000 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java +++ /dev/null @@ -1,67 +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.security.util; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - -/** - * Enumeration capturing essential information about the various key derivation functions that might be supported. - */ -public enum KeyDerivationFunction { - - NONE("None", "The cipher is given a raw key conforming to the algorithm specifications"), - NIFI_LEGACY("NiFi Legacy KDF", "MD5 @ 1000 iterations"), - OPENSSL_EVP_BYTES_TO_KEY("OpenSSL EVP_BytesToKey", "Single iteration MD5 compatible with PKCS#5 v1.5"), - BCRYPT("Bcrypt", "Bcrypt with configurable work factor. See Admin Guide"), - SCRYPT("Scrypt", "Scrypt with configurable cost parameters. See Admin Guide"), - PBKDF2("PBKDF2", "PBKDF2 with configurable hash function and iteration count. See Admin Guide"), - ARGON2("Argon2", "Argon2 with configurable cost parameters. See Admin Guide."); - - private final String kdfName; - private final String description; - - KeyDerivationFunction(String kdfName, String description) { - this.kdfName = kdfName; - this.description = description; - } - - public String getKdfName() { - return kdfName; - } - - public String getDescription() { - return description; - } - - public boolean isStrongKDF() { - return (kdfName.equals(BCRYPT.kdfName) || kdfName.equals(SCRYPT.kdfName) || kdfName.equals(PBKDF2.kdfName) || kdfName.equals(ARGON2.kdfName)); - } - - public boolean hasFormattedSalt() { - return kdfName.equals(BCRYPT.kdfName) || kdfName.equals(SCRYPT.kdfName) || kdfName.equals(ARGON2.kdfName); - } - - @Override - public String toString() { - final ToStringBuilder builder = new ToStringBuilder(this); - ToStringBuilder.setDefaultStyle(ToStringStyle.SHORT_PREFIX_STYLE); - builder.append("KDF Name", kdfName); - builder.append("Description", description); - return builder.toString(); - } -} \ No newline at end of file diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AbstractSecureHasher.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AbstractSecureHasher.java deleted file mode 100644 index ec968215f5..0000000000 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AbstractSecureHasher.java +++ /dev/null @@ -1,273 +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.security.util.crypto; - -import org.bouncycastle.util.encoders.Hex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.Base64; - -public abstract class AbstractSecureHasher implements SecureHasher { - private static final Logger logger = LoggerFactory.getLogger(AbstractSecureHasher.class); - - protected int saltLength; - - private boolean usingStaticSalt; - - // A 16 byte salt (nonce) is recommended for password hashing - private static final byte[] STATIC_SALT = "NiFi Static Salt".getBytes(StandardCharsets.UTF_8); - - // Upper boundary for several cost parameters - static final Integer UPPER_BOUNDARY = Double.valueOf(Math.pow(2, 32)).intValue() - 1; - - /** - * Verifies the salt length is valid for this algorithm and if a static salt should be used. - * - * @param saltLength the salt length in bytes - */ - protected void initializeSalt(Integer saltLength) { - if (saltLength > 0) { - if (!isSaltLengthValid(saltLength)) { - logger.error("The salt length {} is outside the boundary of {} to {}.", saltLength, getMinSaltLength(), getMaxSaltLength()); - throw new IllegalArgumentException("Invalid salt length exceeds the saltLength boundary."); - } - this.usingStaticSalt = false; - } else { - this.usingStaticSalt = true; - logger.debug("Configured to use static salt"); - } - } - - /** - * Returns whether the provided salt length (saltLength) is within boundaries. The lower bound >= (usually) 8 and the - * upper bound <= (usually) {@link Integer#MAX_VALUE}. This method is not {@code static} because it depends on the - * instantiation of the algorithm-specific concrete class. - * - * @param saltLength the salt length in bytes - * @return true if saltLength is within boundaries - */ - public boolean isSaltLengthValid(Integer saltLength) { - final int SALT_LENGTH = getDefaultSaltLength(); - if (saltLength == 0) { - logger.debug("The provided salt length 0 indicates a static salt of {} bytes", SALT_LENGTH); - return true; - } - if (saltLength < SALT_LENGTH) { - logger.warn("The provided dynamic salt length {} is below the recommended minimum {}", saltLength, SALT_LENGTH); - } - return saltLength >= getMinSaltLength() && saltLength <= getMaxSaltLength(); - } - - /** - * Returns the algorithm-specific default salt length in bytes. - * - * @return the default salt length - */ - abstract int getDefaultSaltLength(); - - /** - * Returns the algorithm-specific minimum salt length in bytes. - * - * @return the min salt length - */ - abstract int getMinSaltLength(); - - /** - * Returns the algorithm-specific maximum salt length in bytes. - * - * @return the max salt length - */ - abstract int getMaxSaltLength(); - - /** - * Returns {@code true} if this instance is configured to use a static salt. - * - * @return true if all hashes will be generated using a static salt - */ - public boolean isUsingStaticSalt() { - return usingStaticSalt; - } - - /** - * Returns a salt to use. If using a static salt (see {@link #isUsingStaticSalt()}), - * this return value will be identical across every invocation. If using a dynamic salt, - * it will be {@link #saltLength} bytes of a securely-generated random value. - * - * @return the salt value - */ - byte[] getSalt() { - if (isUsingStaticSalt()) { - return STATIC_SALT; - } else { - SecureRandom sr = new SecureRandom(); - byte[] salt = new byte[saltLength]; - sr.nextBytes(salt); - return salt; - } - } - - /** - * Returns the algorithm-specific name for logging and messages. - * - * @return the algorithm name - */ - abstract String getAlgorithmName(); - - /** - * Returns {@code true} if the algorithm can accept empty (non-{@code null}) inputs. - * - * @return the true if {@code ""} is allowable input - */ - abstract boolean acceptsEmptyInput(); - - /** - * Returns a String representation of the hash in hex-encoded format. - * - * @param input the non-empty input - * @return the hex-encoded hash - */ - @Override - public String hashHex(String input) { - try { - input = validateInput(input); - } catch (IllegalArgumentException e) { - return ""; - } - return Hex.toHexString(hash(input.getBytes(StandardCharsets.UTF_8))); - } - - /** - * Returns a String representation of the hash in hex-encoded format. - * - * @param input the non-empty input - * @param salt the provided salt - * - * @return the hex-encoded hash - */ - @Override - public String hashHex(String input, String salt) { - try { - input = validateInput(input); - } catch (IllegalArgumentException e) { - return ""; - } - - return Hex.toHexString(hash(input.getBytes(StandardCharsets.UTF_8), salt.getBytes(StandardCharsets.UTF_8))); - } - - /** - * Returns a String representation of the hash in Base 64-encoded format. - * - * @param input the non-empty input - * @return the Base 64-encoded hash - */ - @Override - public String hashBase64(String input) { - try { - input = validateInput(input); - } catch (IllegalArgumentException e) { - return ""; - } - - return Base64.getEncoder().withoutPadding().encodeToString(hash(input.getBytes(StandardCharsets.UTF_8))); - } - - /** - * Returns a String representation of the hash in Base 64-encoded format. - * - * @param input the non-empty input - * @param salt the provided salt - * - * @return the Base 64-encoded hash - */ - @Override - public String hashBase64(String input, String salt) { - try { - input = validateInput(input); - } catch (IllegalArgumentException e) { - return ""; - } - - return Base64.getEncoder().withoutPadding().encodeToString(hash(input.getBytes(StandardCharsets.UTF_8), salt.getBytes(StandardCharsets.UTF_8))); - } - - /** - * Returns a byte[] representation of {@code SecureHasher.hash(input)}. - * - * @param input the input - * @return the hash - */ - public byte[] hashRaw(byte[] input) { - return hash(input); - } - - /** - * Returns a byte[] representation of {@code SecureHasher.hash(input)}. - * - * @param input the input - * @param salt the provided salt - * - * @return the hash - */ - @Override - public byte[] hashRaw(byte[] input, byte[] salt) { - return hash(input, salt); - } - - /** - * Returns the valid {@code input} String (if the algorithm accepts empty input, changes {@code null} to {@code ""}; if not, throws {@link IllegalArgumentException}). - * - * @param input the input to validate - * @return a valid input string - */ - private String validateInput(String input) { - if (acceptsEmptyInput()) { - if (input == null) { - logger.warn("Attempting to generate a hash using {} of null input; using empty input", getAlgorithmName()); - return ""; - } - } else { - if (input == null || input.length() == 0) { - logger.warn("Attempting to generate a hash using {} of null or empty input; returning 0 length string", getAlgorithmName()); - throw new IllegalArgumentException(); - } - } - - return input; - } - - /** - * Returns the algorithm-specific calculated hash for the input and generates or retrieves the salt according to - * the configured salt length. - * - * @param input the input in raw bytes - * @return the hash in raw bytes - */ - abstract byte[] hash(byte[] input); - - /** - * Returns the algorithm-specific calculated hash for the input and salt. - * - * @param input the input in raw bytes - * @param salt the provided salt - * @return the hash in raw bytes - */ - abstract byte[] hash(byte[] input, byte[] salt); -} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/Argon2SecureHasher.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/Argon2SecureHasher.java deleted file mode 100644 index ca8a510191..0000000000 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/Argon2SecureHasher.java +++ /dev/null @@ -1,302 +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.security.util.crypto; - -import java.util.concurrent.TimeUnit; -import org.bouncycastle.crypto.generators.Argon2BytesGenerator; -import org.bouncycastle.crypto.params.Argon2Parameters; -import org.bouncycastle.util.encoders.Hex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provides an implementation of {@code Argon2} for secure password hashing. This class is - * roughly based on Spring Security's implementation but does not include the full module - * in this utility module. This implementation uses {@code Argon2id} which provides a - * balance of protection against side-channel and memory attacks. - *

- * One critical difference is that this implementation uses a - * static universal salt unless instructed otherwise, which provides - * strict determinism across nodes in a cluster. The purpose for this is to allow for - * blind equality comparison of sensitive values hashed on different nodes (with - * potentially different {@code nifi.sensitive.props.key} values) during flow inheritance - * (see {@code FingerprintFactory}). - */ -public class Argon2SecureHasher extends AbstractSecureHasher { - private static final Logger logger = LoggerFactory.getLogger(Argon2SecureHasher.class); - - private static final int DEFAULT_HASH_LENGTH = 32; - public static final int DEFAULT_PARALLELISM = 8; - public static final int DEFAULT_MEMORY = 1 << 16; - public static final int DEFAULT_ITERATIONS = 5; - private static final int DEFAULT_SALT_LENGTH = 16; - private static final int MIN_MEMORY_SIZE_KB = 8; - private static final int MIN_PARALLELISM = 1; - private static final int MAX_PARALLELISM = Double.valueOf(Math.pow(2, 24)).intValue() - 1; - private static final int MIN_HASH_LENGTH = 4; - private static final int MIN_ITERATIONS = 1; - private static final int MIN_SALT_LENGTH = 8; - - // Using Integer vs. int to allow for unsigned 32b (values can exceed Integer.MAX_VALUE) - private final Integer hashLength; - private final Integer memory; - private final int parallelism; - private final Integer iterations; - - /** - * Instantiates an Argon2 secure hasher using the default cost parameters - * ({@code hashLength = }{@link #DEFAULT_HASH_LENGTH}, - * {@code memory = }{@link #DEFAULT_MEMORY}, - * {@code parallelism = }{@link #DEFAULT_PARALLELISM}, - * {@code iterations = }{@link #DEFAULT_ITERATIONS}). A static salt is also used. - */ - public Argon2SecureHasher() { - this(DEFAULT_HASH_LENGTH, DEFAULT_MEMORY, DEFAULT_PARALLELISM, DEFAULT_ITERATIONS, 0); - } - - /** - * Instantiates an Argon2 secure hasher using the provided hash length and default cost parameters - * ({@code memory = }{@link #DEFAULT_MEMORY}, - * {@code parallelism = }{@link #DEFAULT_PARALLELISM}, - * {@code iterations = }{@link #DEFAULT_ITERATIONS}). A static salt is also used. - * - * @param hashLength the desired hash output length in bytes - */ - public Argon2SecureHasher(Integer hashLength) { - this(hashLength, DEFAULT_MEMORY, DEFAULT_PARALLELISM, DEFAULT_ITERATIONS, 0); - } - - /** - * Instantiates an Argon2 secure hasher using the provided cost parameters. A static - * {@link #DEFAULT_SALT_LENGTH} byte salt will be generated on every hash request. - * {@link Integer} is used instead of {@code int} for parameters which have a max value of {@code 2^32 - 1} to allow for unsigned integers exceeding {@link Integer#MAX_VALUE}. - * - * @param hashLength the output length in bytes ({@code 4 to 2^32 - 1}) - * @param memory the integer number of KiB used ({@code 8p to 2^32 - 1}) - * @param parallelism degree of parallelism ({@code 1 to 2^24 - 1}) - * @param iterations number of iterations ({@code 1 to 2^32 - 1}) - */ - public Argon2SecureHasher(Integer hashLength, Integer memory, int parallelism, Integer iterations) { - this(hashLength, memory, parallelism, iterations, 0); - } - - /** - * Instantiates an Argon2 secure hasher using the provided cost parameters. A unique - * salt of the specified length will be generated on every hash request. - * {@link Integer} is used instead of {@code int} for parameters which have a max value of {@code 2^32 - 1} to allow for unsigned integers exceeding {@link Integer#MAX_VALUE}. - * - * @param hashLength the output length in bytes ({@code 4 to 2^32 - 1}) - * @param memory the integer number of KiB used ({@code 8p to 2^32 - 1}) - * @param parallelism degree of parallelism ({@code 1 to 2^24 - 1}) - * @param iterations number of iterations ({@code 1 to 2^32 - 1}) - * @param saltLength the salt length in bytes {@code 8 to 2^32 - 1}) - */ - public Argon2SecureHasher(Integer hashLength, Integer memory, int parallelism, Integer iterations, Integer saltLength) { - validateParameters(hashLength, memory, parallelism, iterations, saltLength); - - this.hashLength = hashLength; - this.memory = memory; - this.parallelism = parallelism; - this.iterations = iterations; - - this.saltLength = saltLength; - } - - /** - * Enforces valid Argon2 secure hasher cost parameters are provided. - * - * @param hashLength the output length in bytes ({@code 4 to 2^32 - 1}) - * @param memory the integer number of KiB used ({@code 8p to 2^32 - 1}) - * @param parallelism degree of parallelism ({@code 1 to 2^24 - 1}) - * @param iterations number of iterations ({@code 1 to 2^32 - 1}) - * @param saltLength the salt length in bytes {@code 8 to 2^32 - 1}) - */ - private void validateParameters(Integer hashLength, Integer memory, int parallelism, Integer iterations, Integer saltLength) { - if (!isHashLengthValid(hashLength)) { - logger.error("The provided hash length {} is outside the boundary of 4 to 2^32 - 1.", hashLength); - throw new IllegalArgumentException("Invalid hash length is not within the hashLength boundary."); - } - if (!isMemorySizeValid(memory)) { - logger.error("The provided memory size {} KiB is outside the boundary of 8p to 2^32 - 1.", memory); - throw new IllegalArgumentException("Invalid memory size is not within the memory boundary."); - } - if (!isParallelismValid(parallelism)) { - logger.error("The provided parallelization factor {} is outside the boundary of 1 to 2^24 - 1.", parallelism); - throw new IllegalArgumentException("Invalid parallelization factor exceeds the parallelism boundary."); - } - if (!isIterationsValid(iterations)) { - logger.error("The iteration count {} is outside the boundary of 1 to 2^32 - 1.", iterations); - throw new IllegalArgumentException("Invalid iteration count exceeds the iterations boundary."); - } - - initializeSalt(saltLength); - } - - /** - * Returns the algorithm-specific name for logging and messages. - * - * @return the algorithm name - */ - @Override - String getAlgorithmName() { - return "Argon2"; - } - - /** - * Returns {@code true} if the algorithm can accept empty (non-{@code null}) inputs. - * - * @return the true if {@code ""} is allowable input - */ - @Override - boolean acceptsEmptyInput() { - return true; - } - - /** - * Returns whether the provided hash length is within boundaries. The lower bound >= 4 and the - * upper bound <= 2^32 - 1. - * @param hashLength the output length in bytes - * @return true if hashLength is within boundaries - */ - public static boolean isHashLengthValid(Integer hashLength) { - return hashLength >= MIN_HASH_LENGTH && hashLength <= UPPER_BOUNDARY; - } - - /** - * Returns whether the provided memory size is within boundaries. The lower bound >= 8 and the - * upper bound <= 2^32 - 1. - * @param memory the integer number of KiB used - * @return true if memory is within boundaries - */ - public static boolean isMemorySizeValid(Integer memory) { - if (memory < DEFAULT_MEMORY) { - logger.warn("The provided memory size {} KiB is below the recommended minimum {} KiB.", memory, DEFAULT_MEMORY); - } - return memory >= MIN_MEMORY_SIZE_KB && memory <= UPPER_BOUNDARY; - } - - /** - * Returns whether the provided parallelization factor is within boundaries. The lower bound >= 1 and the - * upper bound <= 2^24 - 1. - * @param parallelism degree of parallelism - * @return true if parallelism is within boundaries - */ - public static boolean isParallelismValid(int parallelism) { - if (parallelism < DEFAULT_PARALLELISM) { - logger.warn("The provided parallelization factor {} is below the recommended minimum {}.", parallelism, DEFAULT_PARALLELISM); - } - return parallelism >= MIN_PARALLELISM && parallelism <= MAX_PARALLELISM; - } - - /** - * Returns whether the provided iteration count is within boundaries. The lower bound >= 1 and the - * upper bound <= 2^32 - 1. - * @param iterations number of iterations - * @return true if iterations is within boundaries - */ - public static boolean isIterationsValid(Integer iterations) { - if (iterations < DEFAULT_ITERATIONS) { - logger.warn("The provided iteration count {} is below the recommended minimum {}.", iterations, DEFAULT_ITERATIONS); - } - return iterations >= MIN_ITERATIONS && iterations <= UPPER_BOUNDARY; - } - - /** - * Returns the algorithm-specific default salt length in bytes. - * - * @return the default salt length - */ - @Override - int getDefaultSaltLength() { - return DEFAULT_SALT_LENGTH; - } - - /** - * Returns the algorithm-specific minimum salt length in bytes. - * - * @return the min salt length - */ - @Override - int getMinSaltLength() { - return MIN_SALT_LENGTH; - } - - /** - * Returns the algorithm-specific maximum salt length in bytes. - * - * @return the max salt length - */ - @Override - int getMaxSaltLength() { - return Integer.MAX_VALUE; - } - - /** - * Internal method to hash the raw bytes. - * - * @param input the raw bytes to hash (can be length 0) - * @return the generated hash - */ - byte[] hash(byte[] input) { - // Contains only the raw salt - byte[] rawSalt = getSalt(); - - return hash(input, rawSalt); - } - - /** - * Internal method to hash the raw bytes. - * - * @param input the raw bytes to hash (can be length 0) - * @param rawSalt the raw bytes to salt - * @return the generated hash - */ - byte[] hash(byte[] input, byte[] rawSalt) { - logger.debug("Creating {} byte Argon2 hash with salt [{}]", hashLength, Hex.toHexString(rawSalt)); - - if (!isSaltLengthValid(rawSalt.length)) { - throw new IllegalArgumentException("The salt length (" + rawSalt.length + " bytes) is invalid"); - } - - byte[] hash = new byte[hashLength]; - - final long startNanos = System.nanoTime(); - - Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) - .withSalt(rawSalt) - .withParallelism(parallelism) - .withMemoryAsKB(memory) - .withIterations(iterations) - .build(); - Argon2BytesGenerator generator = new Argon2BytesGenerator(); - generator.init(params); - - final long initNanos = System.nanoTime(); - - generator.generateBytes(input, hash); - - final long generateNanos = System.nanoTime(); - - final long initDurationMicros = TimeUnit.NANOSECONDS.toMicros(initNanos - startNanos); - final long generateDurationMicros = TimeUnit.NANOSECONDS.toMicros(generateNanos - initNanos); - final long totalDurationMillis = TimeUnit.MICROSECONDS.toMillis(initDurationMicros + generateDurationMicros); - - logger.debug("Generated Argon2 hash in {} ms (init: {} µs, generate: {} µs)", totalDurationMillis, initDurationMicros, generateDurationMicros); - - return hash; - } -} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasher.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasher.java deleted file mode 100644 index 3dca2e81a4..0000000000 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasher.java +++ /dev/null @@ -1,296 +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.security.util.crypto; - -import org.apache.commons.lang3.StringUtils; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.MD5Digest; -import org.bouncycastle.crypto.digests.SHA1Digest; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.digests.SHA384Digest; -import org.bouncycastle.crypto.digests.SHA512Digest; -import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; -import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.util.encoders.Hex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -/** - * Provides an implementation of {@code PBKDF2} for secure password hashing. - *

- * One critical difference is that this implementation uses a - * static universal salt unless instructed otherwise, which provides - * strict determinism across nodes in a cluster. The purpose for this is to allow for - * blind equality comparison of sensitive values hashed on different nodes (with - * potentially different {@code nifi.sensitive.props.key} values) during flow inheritance - * (see {@code FingerprintFactory}). - *

- * The resulting output is referred to as a hash to be consistent with {@link SecureHasher} terminology. - */ -public class PBKDF2SecureHasher extends AbstractSecureHasher { - private static final Logger logger = LoggerFactory.getLogger(PBKDF2SecureHasher.class); - - private static final String DEFAULT_PRF = "SHA-512"; - private static final int DEFAULT_SALT_LENGTH = 16; - /** - * This can be calculated automatically using the code {@see PBKDF2CipherProviderGroovyTest#calculateMinimumIterationCount} or manually updated by a maintainer - */ - private static final int DEFAULT_ITERATION_COUNT = 160_000; - - // Different sources list this in bits and bytes, but RFC 8018 uses bytes (octets [8-bit sequences] to be precise) - private static final int DEFAULT_DK_LENGTH = 32; - - private static final int MIN_ITERATION_COUNT = 1; - private static final int MIN_DK_LENGTH = 1; - private static final int MIN_SALT_LENGTH = 8; - - private final Digest prf; - private final Integer iterationCount; - private final int dkLength; - - /** - * Instantiates a PBKDF2 secure hasher with the default number of iterations and the default PRF. Currently 160,000 iterations and SHA-512. - */ - public PBKDF2SecureHasher() { - this(DEFAULT_PRF, DEFAULT_ITERATION_COUNT, 0, DEFAULT_DK_LENGTH); - } - - /** - * Instantiates a PBKDF2 secure hasher with the default number of iterations and the default PRF using the specified derived key length. - * - * @param dkLength Derived Key Length in bytes - */ - public PBKDF2SecureHasher(final int dkLength) { - this(DEFAULT_PRF, DEFAULT_ITERATION_COUNT, 0, dkLength); - } - - /** - * Instantiates a PBKDF2 secure hasher with the provided number of iterations and derived key (output) length in bytes, using the default PRF ({@code SHA512}). - * - * @param iterationCount the number of iterations - * @param dkLength the desired output length in bytes - */ - public PBKDF2SecureHasher(int iterationCount, int dkLength) { - this(DEFAULT_PRF, iterationCount, 0, dkLength); - } - - /** - * Instantiates a PBKDF2 secure hasher using the provided cost parameters. A unique - * salt of the specified length will be generated on every hash request. - * Currently supported PRFs are {@code MD5} (deprecated), {@code SHA1} (deprecated), {@code SHA256}, - * {@code SHA384}, and {@code SHA512}. Unknown PRFs will default to {@code SHA512}. - * - * @param prf a String representation of the PRF name, e.g. "SHA256", "SHA-384" "sha_512" - * @param iterationCount the number of iterations - * @param saltLength the salt length in bytes ({@code >= 8}, {@code 0} indicates a static salt) - * @param dkLength the output length in bytes ({@code 1 to (2^32 - 1) * hLen}) - */ - public PBKDF2SecureHasher(String prf, Integer iterationCount, int saltLength, int dkLength) { - validateParameters(prf, iterationCount, saltLength, dkLength); - this.prf = resolvePRF(prf); - this.iterationCount = iterationCount; - this.saltLength = saltLength; - this.dkLength = dkLength; - } - - /** - * Enforces valid PBKDF2 secure hasher cost parameters are provided. - * - * @param iterationCount the (log) number of key expansion rounds - * @param saltLength the salt length in bytes {@code >= 8}) - * @param dkLength the output length in bytes ({@code 1 to (2^32 - 1) * hLen}) - */ - private void validateParameters(String prf, Integer iterationCount, int saltLength, int dkLength) { - logger.debug("Validating PBKDF2 secure hasher with prf {}, iteration count {}, salt length {} bytes, output length {} bytes", prf, iterationCount, saltLength, dkLength); - - if (!isIterationCountValid(iterationCount)) { - logger.error("The provided iteration count {} is below the minimum {}.", iterationCount, MIN_ITERATION_COUNT); - throw new IllegalArgumentException("Invalid iterationCount is not within iteration count boundary."); - } - initializeSalt(saltLength); - - // Calculate hLen based on PRF - Digest prfType = resolvePRF(prf); - int hLen = prfType.getDigestSize(); - logger.debug("The PRF is {}, with a digest size (hLen) of {} bytes", prfType.getAlgorithmName(), hLen); - - if (!isDKLengthValid(hLen, dkLength)) { - logger.error("The provided dkLength {} bytes is outside the output boundary {} to {}.", dkLength, MIN_DK_LENGTH, getMaxDKLength(hLen)); - throw new IllegalArgumentException("Invalid dkLength is not within derived key length boundary."); - } - } - - /** - * Returns the algorithm-specific name for logging and messages. - * - * @return the algorithm name - */ - @Override - String getAlgorithmName() { - return "PBKDF2"; - } - - /** - * Returns {@code true} if the algorithm can accept empty (non-{@code null}) inputs. - * - * @return the true if {@code ""} is allowable input - */ - @Override - boolean acceptsEmptyInput() { - return true; - } - - /** - * Returns true if the provided cost factor is within boundaries. The lower bound >= 1. - * - * @param iterationCount the (log) number of key expansion rounds - * @return true if cost factor is within boundaries - */ - public static boolean isIterationCountValid(Integer iterationCount) { - if (iterationCount < DEFAULT_ITERATION_COUNT) { - logger.warn("The provided iteration count {} is below the recommended minimum {}.", iterationCount, DEFAULT_ITERATION_COUNT); - } - // By definition, all ints are <= Integer.MAX_VALUE - return iterationCount >= MIN_ITERATION_COUNT; - } - - /** - * Returns the algorithm-specific default salt length in bytes. - * - * @return the default salt length - */ - @Override - int getDefaultSaltLength() { - return DEFAULT_SALT_LENGTH; - } - - /** - * Returns the algorithm-specific minimum salt length in bytes. - * - * @return the min salt length - */ - @Override - int getMinSaltLength() { - return MIN_SALT_LENGTH; - } - - /** - * Returns the algorithm-specific maximum salt length in bytes. - * - * @return the max salt length - */ - @Override - int getMaxSaltLength() { - return Integer.MAX_VALUE; - } - - /** - * Returns whether the provided hash (derived key) length is within boundaries given the configured PRF. The lower bound >= 1 and the - * upper bound <= ((2^32 - 1) * 32) * hLen. - * - * @param hLen the PRF digest size in bytes - * @param dkLength the output length in bytes - * @return true if dkLength is within boundaries - */ - public static boolean isDKLengthValid(int hLen, Integer dkLength) { - final int MAX_DK_LENGTH = getMaxDKLength(hLen); - logger.debug("The max dkLength is {} bytes for hLen {} bytes.", MAX_DK_LENGTH, hLen); - - return dkLength >= MIN_DK_LENGTH && dkLength <= MAX_DK_LENGTH; - } - - /** - * Returns the maximum length of the derived key in bytes given the digest length in bytes of the underlying PRF. - * If the calculated maximum exceeds {@link Integer#MAX_VALUE}, that is returned instead, as RFC 8018 specifies - * {@code keyLength INTEGER (1..MAX) OPTIONAL}. - * - * @param hLen the length of the PRF digest output in bytes - * @return the maximum possible length of the derived key in bytes - */ - private static int getMaxDKLength(int hLen) { - final long MAX_LENGTH = ((Double.valueOf((Math.pow(2, 32)))).longValue() - 1) * hLen; - return Long.valueOf(Math.min(MAX_LENGTH, Integer.MAX_VALUE)).intValue(); - } - - /** - * Internal method to hash the raw bytes. - * - * @param input the raw bytes to hash (can be length 0) - * @return the generated hash - */ - byte[] hash(byte[] input) { - // Contains only the raw salt - byte[] rawSalt = getSalt(); - - return hash(input, rawSalt); - } - - /** - * Internal method to hash the raw bytes. - * - * @param input the raw bytes to hash (can be length 0) - * @param rawSalt the raw bytes to salt - * @return the generated hash - */ - byte[] hash(byte[] input, byte[] rawSalt) { - logger.debug("Creating PBKDF2 hash with salt [{}] ({} bytes)", Hex.toHexString(rawSalt), rawSalt.length); - - if (!isSaltLengthValid(rawSalt.length)) { - throw new IllegalArgumentException("The salt length (" + rawSalt.length + " bytes) is invalid"); - } - - final long startNanos = System.nanoTime(); - PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(this.prf); - gen.init(input, rawSalt, iterationCount); - // The generateDerivedParameters method expects the dkLength in bits - byte[] hash = ((KeyParameter) gen.generateDerivedParameters(dkLength * 8)).getKey(); - final long generateNanos = System.nanoTime(); - - final long totalDurationMillis = TimeUnit.NANOSECONDS.toMillis(generateNanos - startNanos); - - logger.debug("Generated PBKDF2 hash in {} ms", totalDurationMillis); - - return hash; - } - - private Digest resolvePRF(final String prf) { - if (StringUtils.isEmpty(prf)) { - throw new IllegalArgumentException("Cannot resolve empty PRF"); - } - String formattedPRF = prf.toLowerCase().replaceAll("[\\W]+", ""); - logger.debug("Resolved PRF {} to {}", prf, formattedPRF); - switch (formattedPRF) { - case "md5": - logger.warn("MD5 is a deprecated cryptographic hash function and should not be used"); - return new MD5Digest(); - case "sha1": - logger.warn("SHA-1 is a deprecated cryptographic hash function and should not be used"); - return new SHA1Digest(); - case "sha256": - return new SHA256Digest(); - case "sha384": - return new SHA384Digest(); - case "sha512": - return new SHA512Digest(); - default: - logger.warn("Could not resolve PRF {}. Using default PRF {} instead", prf, DEFAULT_PRF); - return new SHA512Digest(); - } - } -} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasher.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasher.java deleted file mode 100644 index 6cef8dbd3d..0000000000 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasher.java +++ /dev/null @@ -1,81 +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.security.util.crypto; - -/** - * An interface which specifies that implementations should provide a - * cryptographic hash function (CHF) which accepts input and returns a - * deterministic, (mathematically-difficult) irreversible value. - * While SHA-256, SHA-512, and Blake2 are CHF implementations, this interface is intended to - * be used by password protection or key derivation functions (KDF). - */ -public interface SecureHasher { - - /** - * Returns a String representation of {@code CHF(input)} in hex-encoded format. - * - * @param input the input - * @return the hex-encoded hash - */ - String hashHex(String input); - - /** - * Returns a String representation of {@code CHF(input)} in hex-encoded format. - * - * @param input the input - * @param salt the provided salt - * - * @return the hex-encoded hash - */ - String hashHex(String input, String salt); - - /** - * Returns a String representation of {@code CHF(input)} in Base 64-encoded format. - * - * @param input the input - * @return the Base 64-encoded hash - */ - String hashBase64(String input); - - /** - * Returns a String representation of {@code CHF(input)} in Base 64-encoded format. - * - * @param input the input - * @param salt the provided salt - * - * @return the Base 64-encoded hash - */ - String hashBase64(String input, String salt); - - /** - * Returns a byte[] representation of {@code CHF(input)}. - * - * @param input the input - * @return the hash - */ - byte[] hashRaw(byte[] input); - - /** - * Returns a byte[] representation of {@code CHF(input)}. - * - * @param input the input - * @param salt the provided salt - * - * @return the hash - */ - byte[] hashRaw(byte[] input, byte[] salt); -} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasherFactory.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasherFactory.java deleted file mode 100644 index 2f291b22ae..0000000000 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/SecureHasherFactory.java +++ /dev/null @@ -1,57 +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.security.util.crypto; - -import org.apache.nifi.security.util.KeyDerivationFunction; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.Map; - -/** - *

Provides a factory for SecureHasher implementations. Will return Argon2 by default if no algorithm parameter is given. - * Algorithm parameter should align with the below registered secure hasher names (PBKDF2, BCRYPT, SCRYPT, ARGON2). - */ -public class SecureHasherFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(SecureHasherFactory.class); - - private static Map> registeredSecureHashers; - private static final Class DEFAULT_SECURE_HASHER_CLASS = Argon2SecureHasher.class; - - static { - registeredSecureHashers = new HashMap<>(); - registeredSecureHashers.put(KeyDerivationFunction.ARGON2, Argon2SecureHasher.class); - registeredSecureHashers.put(KeyDerivationFunction.PBKDF2, PBKDF2SecureHasher.class); - } - - public static SecureHasher getSecureHasher(final String algorithm) { - Class secureHasherClass = DEFAULT_SECURE_HASHER_CLASS; - final String algorithmPattern = algorithm.toUpperCase(); - try { - for (KeyDerivationFunction keyDerivationFunction : registeredSecureHashers.keySet()) { - final String functionName = keyDerivationFunction.getKdfName().toUpperCase(); - if (algorithmPattern.contains(functionName)) { - secureHasherClass = registeredSecureHashers.get(keyDerivationFunction); - } - } - LOGGER.debug("Creating SecureHasher [{}] for algorithm [{}]", secureHasherClass.getName(), algorithm); - return secureHasherClass.getDeclaredConstructor().newInstance(); - } catch (Exception e) { - throw new IllegalStateException(String.format("Failed to create SecureHasher for algorithm [%s]", algorithm), e); - } - } -} \ No newline at end of file diff --git a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/Argon2SecureHasherTest.java b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/Argon2SecureHasherTest.java deleted file mode 100644 index 91c0938816..0000000000 --- a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/Argon2SecureHasherTest.java +++ /dev/null @@ -1,414 +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.security.util.crypto; - -import org.bouncycastle.util.encoders.Hex; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class Argon2SecureHasherTest { - @Test - void testShouldBeDeterministicWithStaticSalt() { - // Arrange - int hashLength = 32; - int memory = 8; - int parallelism = 4; - int iterations = 4; - - int testIterations = 10; - byte[] inputBytes = "This is a sensitive value".getBytes(); - - final String EXPECTED_HASH_HEX = "a73a471f51b2900901a00b81e770b9c1dfc595602bb7aec64cd27754a4174919"; - - Argon2SecureHasher a2sh = new Argon2SecureHasher(hashLength, memory, parallelism, iterations); - - final List results = new ArrayList<>(); - - // Act - for (int i = 0; i < testIterations; i++) { - byte[] hash = a2sh.hashRaw(inputBytes); - String hashHex = new String(Hex.encode(hash)); - results.add(hashHex); - } - - // Assert - results.forEach(result -> assertEquals(EXPECTED_HASH_HEX, result)); - } - - @Test - void testShouldBeDifferentWithRandomSalt() { - // Arrange - int hashLength = 32; - int memory = 8; - int parallelism = 4; - int iterations = 4; - - int testIterations = 10; - byte[] inputBytes = "This is a sensitive value".getBytes(); - - final String EXPECTED_HASH_HEX = "a73a471f51b2900901a00b81e770b9c1dfc595602bb7aec64cd27754a4174919"; - - Argon2SecureHasher a2sh = new Argon2SecureHasher(hashLength, memory, parallelism, iterations, 16); - - final List results = new ArrayList<>(); - - // Act - for (int i = 0; i < testIterations; i++) { - byte[] hash = a2sh.hashRaw(inputBytes); - String hashHex = new String(Hex.encode(hash)); - results.add(hashHex); - } - - // Assert - assertTrue(results.stream().distinct().collect(Collectors.toList()).size() == results.size()); - results.forEach(result -> assertNotEquals(EXPECTED_HASH_HEX, result)); - } - - @Test - void testShouldHandleArbitrarySalt() { - // Arrange - int hashLength = 32; - int memory = 8; - int parallelism = 4; - int iterations = 4; - - final String input = "This is a sensitive value"; - byte[] inputBytes = input.getBytes(); - - final String EXPECTED_HASH_HEX = "a73a471f51b2900901a00b81e770b9c1dfc595602bb7aec64cd27754a4174919"; - final String EXPECTED_HASH_BASE64 = "pzpHH1GykAkBoAuB53C5wd/FlWArt67GTNJ3VKQXSRk"; - final byte[] EXPECTED_HASH_BYTES = Hex.decode(EXPECTED_HASH_HEX); - - // Static salt instance - Argon2SecureHasher staticSaltHasher = new Argon2SecureHasher(hashLength, memory, parallelism, iterations); - Argon2SecureHasher arbitrarySaltHasher = new Argon2SecureHasher(hashLength, memory, parallelism, iterations, 16); - - final byte[] STATIC_SALT = "NiFi Static Salt".getBytes(StandardCharsets.UTF_8); - final String DIFFERENT_STATIC_SALT = "Diff Static Salt"; - - // Act - byte[] staticSaltHash = staticSaltHasher.hashRaw(inputBytes); - byte[] arbitrarySaltHash = arbitrarySaltHasher.hashRaw(inputBytes, STATIC_SALT); - byte[] differentArbitrarySaltHash = arbitrarySaltHasher.hashRaw(inputBytes, DIFFERENT_STATIC_SALT.getBytes(StandardCharsets.UTF_8)); - byte[] differentSaltHash = arbitrarySaltHasher.hashRaw(inputBytes); - - String staticSaltHashHex = staticSaltHasher.hashHex(input); - String arbitrarySaltHashHex = arbitrarySaltHasher.hashHex(input, new String(STATIC_SALT, StandardCharsets.UTF_8)); - String differentArbitrarySaltHashHex = arbitrarySaltHasher.hashHex(input, DIFFERENT_STATIC_SALT); - String differentSaltHashHex = arbitrarySaltHasher.hashHex(input); - - String staticSaltHashBase64 = staticSaltHasher.hashBase64(input); - String arbitrarySaltHashBase64 = arbitrarySaltHasher.hashBase64(input, new String(STATIC_SALT, StandardCharsets.UTF_8)); - String differentArbitrarySaltHashBase64 = arbitrarySaltHasher.hashBase64(input, DIFFERENT_STATIC_SALT); - String differentSaltHashBase64 = arbitrarySaltHasher.hashBase64(input); - - // Assert - assertArrayEquals(EXPECTED_HASH_BYTES, staticSaltHash); - assertArrayEquals(EXPECTED_HASH_BYTES, arbitrarySaltHash); - assertFalse(Arrays.equals(EXPECTED_HASH_BYTES, differentArbitrarySaltHash)); - assertFalse(Arrays.equals(EXPECTED_HASH_BYTES, differentSaltHash)); - - assertEquals(EXPECTED_HASH_HEX, staticSaltHashHex); - assertEquals(EXPECTED_HASH_HEX, arbitrarySaltHashHex); - assertNotEquals(EXPECTED_HASH_HEX, differentArbitrarySaltHashHex); - assertNotEquals(EXPECTED_HASH_HEX, differentSaltHashHex); - - assertEquals(EXPECTED_HASH_BASE64, staticSaltHashBase64); - assertEquals(EXPECTED_HASH_BASE64, arbitrarySaltHashBase64); - assertNotEquals(EXPECTED_HASH_BASE64, differentArbitrarySaltHashBase64); - assertNotEquals(EXPECTED_HASH_BASE64, differentSaltHashBase64); - } - - @Test - void testShouldValidateArbitrarySalt() { - // Arrange - int hashLength = 32; - int memory = 8; - int parallelism = 4; - int iterations = 4; - - final String input = "This is a sensitive value"; - byte[] inputBytes = input.getBytes(); - - // Static salt instance - Argon2SecureHasher secureHasher = new Argon2SecureHasher(hashLength, memory, parallelism, iterations, 16); - final byte[] STATIC_SALT = "bad_sal".getBytes(); - - // Act - assertThrows(IllegalArgumentException.class, () -> - new Argon2SecureHasher(hashLength, memory, parallelism, iterations, 7) - ); - - assertThrows(RuntimeException.class, () -> secureHasher.hashRaw(inputBytes, STATIC_SALT)); - assertThrows(RuntimeException.class, () -> secureHasher.hashHex(input, new String(STATIC_SALT, StandardCharsets.UTF_8))); - assertThrows(RuntimeException.class, () -> secureHasher.hashBase64(input, new String(STATIC_SALT, StandardCharsets.UTF_8))); - } - - @Test - void testShouldFormatHex() { - // Arrange - String input = "This is a sensitive value"; - - final String EXPECTED_HASH_HEX = "0c2920c52f28e0a2c77d006ec6138c8dc59580881468b85541cf886abdebcf18"; - - Argon2SecureHasher a2sh = new Argon2SecureHasher(32, 4096, 1, 3); - - // Act - String hashHex = a2sh.hashHex(input); - - // Assert - assertEquals(EXPECTED_HASH_HEX, hashHex); - } - - @Test - void testShouldFormatBase64() { - // Arrange - String input = "This is a sensitive value"; - - final String EXPECTED_HASH_B64 = "DCkgxS8o4KLHfQBuxhOMjcWVgIgUaLhVQc+Iar3rzxg"; - - Argon2SecureHasher a2sh = new Argon2SecureHasher(32, 4096, 1, 3); - - // Act - String hashB64 = a2sh.hashBase64(input); - - // Assert - assertEquals(EXPECTED_HASH_B64, hashB64); - } - - @Test - void testShouldHandleNullInput() { - // Arrange - List inputs = Arrays.asList(null, ""); - - final String EXPECTED_HASH_HEX = "8e5625a66b94ed9d31c1496d7f9ff49249cf05d6753b50ba0e2bf2a1108973dd"; - final String EXPECTED_HASH_B64 = "jlYlpmuU7Z0xwUltf5/0kknPBdZ1O1C6DivyoRCJc90"; - - Argon2SecureHasher a2sh = new Argon2SecureHasher(32, 4096, 1, 3); - - final List hexResults = new ArrayList<>(); - final List b64Results = new ArrayList<>(); - - // Act - for (final String input : inputs) { - String hashHex = a2sh.hashHex(input); - hexResults.add(hashHex); - - String hashB64 = a2sh.hashBase64(input); - b64Results.add(hashB64); - } - - // Assert - hexResults.forEach(hexResult -> assertEquals(EXPECTED_HASH_HEX, hexResult)); - b64Results.forEach(b64Result -> assertEquals(EXPECTED_HASH_B64, b64Result)); - } - - /** - * This test can have the minimum time threshold updated to determine if the performance - * is still sufficient compared to the existing threat model. - */ - @EnabledIfSystemProperty(named = "nifi.test.performance", matches = "true") - @Test - void testDefaultCostParamsShouldBeSufficient() { - // Arrange - int testIterations = 100; //_000 - byte[] inputBytes = "This is a sensitive value".getBytes(); - - Argon2SecureHasher a2sh = new Argon2SecureHasher(16, (int) Math.pow(2, 16), 8, 5); - - final List results = new ArrayList<>(); - final List resultDurations = new ArrayList<>(); - - // Act - for (int i = 0; i < testIterations; i++) { - long startNanos = System.nanoTime(); - byte[] hash = a2sh.hashRaw(inputBytes); - long endNanos = System.nanoTime(); - long durationNanos = endNanos - startNanos; - - String hashHex = new String(Hex.encode(hash)); - - results.add(hashHex); - resultDurations.add(durationNanos); - } - - // Assert - final long MIN_DURATION_NANOS = 500_000_000; // 500 ms - assertTrue(Collections.min(resultDurations) > MIN_DURATION_NANOS); - assertTrue(resultDurations.stream().mapToLong(Long::longValue).sum() / testIterations > MIN_DURATION_NANOS); - } - - @Test - void testShouldVerifyHashLengthBoundary() throws Exception { - // Arrange - final int hashLength = 128; - - // Act - boolean valid = Argon2SecureHasher.isHashLengthValid(hashLength); - - // Assert - assertTrue(valid); - } - - @Test - void testShouldFailHashLengthBoundary() throws Exception { - // Arrange - final List hashLengths = Arrays.asList(-8, 0, 1, 2); - - // Act & Assert - for (final int hashLength: hashLengths) { - assertFalse(Argon2SecureHasher.isHashLengthValid(hashLength)); - } - } - - @Test - void testShouldVerifyMemorySizeBoundary() throws Exception { - // Arrange - final int memory = 2048; - - // Act - boolean valid = Argon2SecureHasher.isMemorySizeValid(memory); - - // Assert - assertTrue(valid); - } - - @Test - void testShouldFailMemorySizeBoundary() throws Exception { - // Arrange - final List memorySizes = Arrays.asList(-12, 0, 1, 6); - - // Act & Assert - for (final int memory : memorySizes) { - assertFalse(Argon2SecureHasher.isMemorySizeValid(memory)); - } - } - - @Test - void testShouldVerifyParallelismBoundary() throws Exception { - // Arrange - final int parallelism = 4; - - // Act - boolean valid = Argon2SecureHasher.isParallelismValid(parallelism); - - // Assert - assertTrue(valid); - } - - @Test - void testShouldFailParallelismBoundary() throws Exception { - // Arrange - final List parallelisms = Arrays.asList(-8, 0, 16777220, 16778000); - - // Act & Assert - for (final int parallelism : parallelisms) { - assertFalse(Argon2SecureHasher.isParallelismValid(parallelism)); - } - } - - @Test - void testShouldVerifyIterationsBoundary() throws Exception { - // Arrange - final int iterations = 4; - - // Act - boolean valid = Argon2SecureHasher.isIterationsValid(iterations); - - // Assert - assertTrue(valid); - } - - @Test - void testShouldFailIterationsBoundary() throws Exception { - // Arrange - final List iterationCounts = Arrays.asList(-50, -1, 0); - - // Act & Assert - for (final int iterations: iterationCounts) { - assertFalse(Argon2SecureHasher.isIterationsValid(iterations)); - } - } - - @Test - void testShouldVerifySaltLengthBoundary() throws Exception { - // Arrange - final List saltLengths = Arrays.asList(0, 64); - - // Act and Assert - Argon2SecureHasher argon2SecureHasher = new Argon2SecureHasher(); - saltLengths.forEach(saltLength -> - assertTrue(argon2SecureHasher.isSaltLengthValid(saltLength)) - ); - } - - @Test - void testShouldFailSaltLengthBoundary() throws Exception { - // Arrange - final List saltLengths = Arrays.asList(-16, 4); - - // Act and Assert - Argon2SecureHasher argon2SecureHasher = new Argon2SecureHasher(); - saltLengths.forEach(saltLength -> assertFalse(argon2SecureHasher.isSaltLengthValid(saltLength))); - } - - @Test - void testShouldCreateHashOfDesiredLength() throws Exception { - // Arrange - final List hashLengths = Arrays.asList(16, 32); - - final String PASSWORD = "password"; - final byte[] SALT = new byte[16]; - Arrays.fill(SALT, (byte) '\0'); - final byte[] EXPECTED_HASH = Hex.decode("411c9c87e7c91d8c8eacc418665bd2e1"); - - // Act - Map results = hashLengths - .stream() - .collect( - Collectors.toMap( - Function.identity(), - hashLength -> { - Argon2SecureHasher ash = new Argon2SecureHasher(hashLength, 8, 1, 3); - final byte[] hash = ash.hashRaw(PASSWORD.getBytes(), SALT); - return hash; - } - ) - ); - - // Assert - assertFalse(Arrays.equals(Arrays.copyOf(results.get(16), 16), Arrays.copyOf(results.get(32), 16))); - // Demonstrates that internal hash truncation is not supported -// assert results.every { int k, byte[] v -> v[0..15] as byte[] == EXPECTED_HASH} - } -} diff --git a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasherTest.java b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasherTest.java deleted file mode 100644 index 76b7ed75b5..0000000000 --- a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/PBKDF2SecureHasherTest.java +++ /dev/null @@ -1,322 +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.security.util.crypto; - -import org.bouncycastle.util.encoders.Hex; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class PBKDF2SecureHasherTest { - private static final byte[] STATIC_SALT = "NiFi Static Salt".getBytes(StandardCharsets.UTF_8); - - @Test - void testShouldBeDeterministicWithStaticSalt() { - // Arrange - int cost = 10_000; - int dkLength = 32; - byte[] inputBytes = "This is a sensitive value".getBytes(); - final String EXPECTED_HASH_HEX = "2c47a6d801b71e087f94792079c40880aea29013bfffd0ab94b1bc112ea52511"; - - // Act - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(cost, dkLength); - List results = Stream.iterate(0, n -> n + 1) - .limit(10) - .map(iteration -> { - byte[] hash = pbkdf2SecureHasher.hashRaw(inputBytes); - return new String(Hex.encode(hash)); - }) - .collect(Collectors.toList()); - - // Assert - results.forEach(result -> assertEquals(EXPECTED_HASH_HEX, result)); - } - - @Test - void testShouldBeDifferentWithRandomSalt() { - // Arrange - String prf = "SHA512"; - int cost = 10_000; - int saltLength = 16; - int dkLength = 32; - byte[] inputBytes = "This is a sensitive value".getBytes(); - final String EXPECTED_HASH_HEX = "2c47a6d801b71e087f94792079c40880aea29013bfffd0ab94b1bc112ea52511"; - - //Act - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(prf, cost, saltLength, dkLength); - List results = Stream.iterate(0, n -> n + 1) - .limit(10) - .map(iteration -> { - byte[] hash = pbkdf2SecureHasher.hashRaw(inputBytes); - return new String(Hex.encode(hash)); - }) - .collect(Collectors.toList()); - - // Assert - assertEquals(results.stream().distinct().collect(Collectors.toList()).size(), results.size()); - results.forEach(result -> assertNotEquals(EXPECTED_HASH_HEX, result)); - } - - @Test - void testShouldHandleArbitrarySalt() { - // Arrange - String prf = "SHA512"; - int cost = 10_000; - int saltLength = 16; - int dkLength = 32; - - final String input = "This is a sensitive value"; - byte[] inputBytes = input.getBytes(); - - final String EXPECTED_HASH_HEX = "2c47a6d801b71e087f94792079c40880aea29013bfffd0ab94b1bc112ea52511"; - final String EXPECTED_HASH_BASE64 = "LEem2AG3Hgh/lHkgecQIgK6ikBO//9CrlLG8ES6lJRE"; - final byte[] EXPECTED_HASH_BYTES = Hex.decode(EXPECTED_HASH_HEX); - - PBKDF2SecureHasher staticSaltHasher = new PBKDF2SecureHasher(cost, dkLength); - PBKDF2SecureHasher arbitrarySaltHasher = new PBKDF2SecureHasher(prf, cost, saltLength, dkLength); - - final String DIFFERENT_STATIC_SALT = "Diff Static Salt"; - - // Act - byte[] staticSaltHash = staticSaltHasher.hashRaw(inputBytes); - byte[] arbitrarySaltHash = arbitrarySaltHasher.hashRaw(inputBytes, STATIC_SALT); - byte[] differentArbitrarySaltHash = arbitrarySaltHasher.hashRaw(inputBytes, DIFFERENT_STATIC_SALT.getBytes(StandardCharsets.UTF_8)); - byte[] differentSaltHash = arbitrarySaltHasher.hashRaw(inputBytes); - - String staticSaltHashHex = staticSaltHasher.hashHex(input); - String arbitrarySaltHashHex = arbitrarySaltHasher.hashHex(input, new String(STATIC_SALT, StandardCharsets.UTF_8)); - String differentArbitrarySaltHashHex = arbitrarySaltHasher.hashHex(input, DIFFERENT_STATIC_SALT); - String differentSaltHashHex = arbitrarySaltHasher.hashHex(input); - - String staticSaltHashBase64 = staticSaltHasher.hashBase64(input); - String arbitrarySaltHashBase64 = arbitrarySaltHasher.hashBase64(input, new String(STATIC_SALT, StandardCharsets.UTF_8)); - String differentArbitrarySaltHashBase64 = arbitrarySaltHasher.hashBase64(input, DIFFERENT_STATIC_SALT); - String differentSaltHashBase64 = arbitrarySaltHasher.hashBase64(input); - - // Assert - assertArrayEquals(EXPECTED_HASH_BYTES, staticSaltHash); - assertArrayEquals(EXPECTED_HASH_BYTES, arbitrarySaltHash); - assertFalse(Arrays.equals(EXPECTED_HASH_BYTES, differentArbitrarySaltHash)); - assertFalse(Arrays.equals(EXPECTED_HASH_BYTES, differentSaltHash)); - - assertEquals(EXPECTED_HASH_HEX, staticSaltHashHex); - assertEquals(EXPECTED_HASH_HEX, arbitrarySaltHashHex); - assertNotEquals(EXPECTED_HASH_HEX, differentArbitrarySaltHashHex); - assertNotEquals(EXPECTED_HASH_HEX, differentSaltHashHex); - - assertEquals(EXPECTED_HASH_BASE64, staticSaltHashBase64); - assertEquals(EXPECTED_HASH_BASE64, arbitrarySaltHashBase64); - assertNotEquals(EXPECTED_HASH_BASE64, differentArbitrarySaltHashBase64); - assertNotEquals(EXPECTED_HASH_BASE64, differentSaltHashBase64); - } - - @Test - void testShouldValidateArbitrarySalt() { - // Assert - String prf = "SHA512"; - int cost = 10_000; - int saltLength = 16; - int dkLength = 32; - - final String input = "This is a sensitive value"; - byte[] inputBytes = input.getBytes(); - - // Static salt instance - PBKDF2SecureHasher secureHasher = new PBKDF2SecureHasher(prf, cost, saltLength, dkLength); - byte[] STATIC_SALT = "bad_sal".getBytes(); - - assertThrows(IllegalArgumentException.class, () -> new PBKDF2SecureHasher(prf, cost, 7, dkLength)); - assertThrows(RuntimeException.class, () -> secureHasher.hashRaw(inputBytes, STATIC_SALT)); - assertThrows(RuntimeException.class, () -> secureHasher.hashHex(input, new String(STATIC_SALT, StandardCharsets.UTF_8))); - assertThrows(RuntimeException.class, () -> secureHasher.hashBase64(input, new String(STATIC_SALT, StandardCharsets.UTF_8))); - } - - @Test - void testShouldFormatHex() { - // Arrange - String input = "This is a sensitive value"; - - final String EXPECTED_HASH_HEX = "8f67110e87d225366e2d79ad251d2cf48f8cb15845800452e0e2cff09f95ef1c"; - - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(); - - // Act - String hashHex = pbkdf2SecureHasher.hashHex(input); - - // Assert - assertEquals(EXPECTED_HASH_HEX, hashHex); - } - - @Test - void testShouldFormatBase64() { - // Arrange - String input = "This is a sensitive value"; - - final String EXPECTED_HASH_BASE64 = "j2cRDofSJTZuLXmtJR0s9I+MsVhFgARS4OLP8J+V7xw"; - - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(); - - // Act - String hashB64 = pbkdf2SecureHasher.hashBase64(input); - - // Assert - assertEquals(EXPECTED_HASH_BASE64, hashB64); - } - - @Test - void testShouldHandleNullInput() { - // Arrange - List inputs = Arrays.asList(null, ""); - - final String EXPECTED_HASH_HEX = "7f2d8d8c7aaa45471f6c05a8edfe0a3f75fe01478cc965c5dce664e2ac6f5d0a"; - final String EXPECTED_HASH_BASE64 = "fy2NjHqqRUcfbAWo7f4KP3X+AUeMyWXF3OZk4qxvXQo"; - - // Act - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(); - List hexResults = inputs.stream() - .map(input -> pbkdf2SecureHasher.hashHex(input)) - .collect(Collectors.toList()); - List B64Results = inputs.stream() - .map(input -> pbkdf2SecureHasher.hashBase64(input)) - .collect(Collectors.toList()); - - // Assert - hexResults.forEach(result -> assertEquals(EXPECTED_HASH_HEX, result)); - B64Results.forEach(result -> assertEquals(EXPECTED_HASH_BASE64, result)); - } - - /** - * This test can have the minimum time threshold updated to determine if the performance - * is still sufficient compared to the existing threat model. - */ - @EnabledIfSystemProperty(named = "nifi.test.performance", matches = "true") - @Test - void testDefaultCostParamsShouldBeSufficient() { - // Arrange - int testIterations = 100; - byte[] inputBytes = "This is a sensitive value".getBytes(); - - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(); - - final List results = new ArrayList<>(); - final List resultDurations = new ArrayList<>(); - - // Act - for (int i = 0; i < testIterations; i++) { - long startNanos = System.nanoTime(); - byte[] hash = pbkdf2SecureHasher.hashRaw(inputBytes); - long endNanos = System.nanoTime(); - long durationNanos = endNanos - startNanos; - - String hashHex = Arrays.toString(Hex.encode(hash)); - - results.add(hashHex); - resultDurations.add(durationNanos); - } - - // Assert - final long MIN_DURATION_NANOS = 75_000_000; // 75 ms - assertTrue(Collections.min(resultDurations) > MIN_DURATION_NANOS); - assertTrue(resultDurations.stream().mapToLong(Long::longValue).sum() / testIterations > MIN_DURATION_NANOS); - } - - @Test - void testShouldVerifyIterationCountBoundary() throws Exception { - // Arrange - final List validIterationCounts = Arrays.asList(1, 1000, 1_000_000); - - // Act & Assert - for (final int iterationCount : validIterationCounts) { - assertTrue(PBKDF2SecureHasher.isIterationCountValid(iterationCount)); - } - } - - @Test - void testShouldFailIterationCountBoundary() throws Exception { - // Arrange - List invalidIterationCounts = Arrays.asList(-1, 0, Integer.MAX_VALUE + 1); - - // Act and Assert - invalidIterationCounts.forEach(i -> assertFalse(PBKDF2SecureHasher.isIterationCountValid(i))); - } - - @Test - void testShouldVerifyDKLengthBoundary() throws Exception { - // Arrange - List validHLengths = Arrays.asList(32, 64); - - // 1 and MAX_VALUE are the length boundaries, inclusive - List validDKLengths = Arrays.asList(1, 1000, 1_000_000, Integer.MAX_VALUE); - - // Act and Assert - validHLengths.forEach(hLen -> { - validDKLengths.forEach(dkLength -> { - assertTrue(PBKDF2SecureHasher.isDKLengthValid(hLen, dkLength)); - }); - }); - } - - @Test - void testShouldFailDKLengthBoundary() throws Exception { - // Arrange - List validHLengths = Arrays.asList(32, 64); - - // MAX_VALUE + 1 will become MIN_VALUE because of signed integer math - List invalidDKLengths = Arrays.asList(-1, 0, Integer.MAX_VALUE + 1, Integer.valueOf(Integer.MAX_VALUE * 2 - 1)); - - // Act and Assert - validHLengths.forEach(hLen -> { - invalidDKLengths.forEach(dkLength -> { - assertFalse(PBKDF2SecureHasher.isDKLengthValid(hLen, dkLength)); - }); - }); - } - - @Test - void testShouldVerifySaltLengthBoundary() throws Exception { - // Arrange - List saltLengths = Arrays.asList(0, 16, 64); - - // Act and Assert - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(); - saltLengths.forEach(saltLength -> assertTrue(pbkdf2SecureHasher.isSaltLengthValid(saltLength))); - } - - @Test - void testShouldFailSaltLengthBoundary() throws Exception { - // Arrange - List saltLengths = Arrays.asList(-8, 1, Integer.MAX_VALUE + 1); - - // Act and Assert - PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher(); - saltLengths.forEach(saltLength -> assertFalse(pbkdf2SecureHasher.isSaltLengthValid(saltLength))); - } -} diff --git a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/SecureHasherFactoryTest.java b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/SecureHasherFactoryTest.java deleted file mode 100644 index ddec893c02..0000000000 --- a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/SecureHasherFactoryTest.java +++ /dev/null @@ -1,68 +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.security.util.crypto; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class SecureHasherFactoryTest { - - private static final Argon2SecureHasher DEFAULT_HASHER = new Argon2SecureHasher(); - - @Test - public void testSecureHasherFactoryArgon2() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher("NIFI_ARGON2_AES_GCM_256"); - assertEquals(Argon2SecureHasher.class, hasher.getClass()); - } - - @Test - public void testSecureHasherFactoryPBKDF2() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher("NIFI_PBKDF2_AES_GCM_256"); - assertEquals(PBKDF2SecureHasher.class, hasher.getClass()); - } - - @Test - public void testSecureHasherFactoryArgon2ShortName() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher("ARGON2"); - assertEquals(Argon2SecureHasher.class, hasher.getClass()); - } - - @Test - public void testSecureHasherFactoryArgon2SimilarName() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher("ARGON_2"); - assertEquals(Argon2SecureHasher.class, hasher.getClass()); - } - - @Test - public void testSecureHasherFactoryFailsUnknownAlgorithmName() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher("wrongString"); - assertEquals(Argon2SecureHasher.class, hasher.getClass()); - } - - @Test - public void testSecureHasherFactoryDefaultsToArgon2IfLongUnknownAlgorithmName() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher("NIFI_UNKNONWN_AES_GCM_256"); - assertEquals(Argon2SecureHasher.class, hasher.getClass()); - } - - @Test - public void testSecureHasherFactoryEmptyString() { - SecureHasher hasher = SecureHasherFactory.getSecureHasher(""); - assertEquals(DEFAULT_HASHER.getClass(), hasher.getClass()); - } -} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/SensitiveValueEncoder.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/SensitiveValueEncoder.java deleted file mode 100644 index 8dc3186aae..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/SensitiveValueEncoder.java +++ /dev/null @@ -1,29 +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.encrypt; - -/** SensitiveValueEncoder implementations should securely encode a sensitive value into a loggable, secure representation - * of that value. - */ -public interface SensitiveValueEncoder { - - /** - * Returns a securely-derived, deterministic representation of a decrypted NiFi sensitive property value - * for logging/comparison purposes. - */ - String getEncoded(String plaintextSensitiveValue); -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/StandardSensitiveValueEncoder.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/StandardSensitiveValueEncoder.java deleted file mode 100644 index 3bdf1d3960..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/encrypt/StandardSensitiveValueEncoder.java +++ /dev/null @@ -1,81 +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.encrypt; - -import org.apache.nifi.security.util.crypto.SecureHasher; -import org.apache.nifi.security.util.crypto.SecureHasherFactory; -import org.apache.nifi.util.NiFiProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.Objects; - -/** - * Encode a sensitive value using the NiFi sensitive properties key to derive the secret key used in the MAC operation. - */ -public class StandardSensitiveValueEncoder implements SensitiveValueEncoder { - - private static final Logger logger = LoggerFactory.getLogger(StandardSensitiveValueEncoder.class); - - private SecretKeySpec secretKeySpec; - private static Base64.Encoder base64Encoder; - private static final String HMAC_SHA256 = "HmacSHA256"; - private static final Charset PROPERTY_CHARSET = StandardCharsets.UTF_8; - - public StandardSensitiveValueEncoder(final NiFiProperties properties) { - this(properties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY), - SecureHasherFactory.getSecureHasher(properties.getProperty(NiFiProperties.SENSITIVE_PROPS_ALGORITHM))); - } - - // We use the sensitive properties key and a SecureHasher impl to derive a secret key for the getEncoded() method - private StandardSensitiveValueEncoder(final String sensitivePropertiesKey, final SecureHasher hasher) { - Objects.requireNonNull(sensitivePropertiesKey, "Sensitive Properties Key is required"); - Objects.requireNonNull(hasher, "SecureHasher is required"); - byte[] hashedSensitivePropertyKey = hasher.hashRaw(sensitivePropertiesKey.getBytes(PROPERTY_CHARSET)); - secretKeySpec = new SecretKeySpec(hashedSensitivePropertyKey, HMAC_SHA256); - base64Encoder = Base64.getEncoder(); - } - - /** - * Creates a securely-derived, deterministic representation of the provided decrypted NiFi property value - * for logging/comparison purposes. A SecureHasher implementation is used to derive a secret key from the sensitive which is - * then used to generate an HMAC using HMAC+SHA256. - * - * @param plaintextSensitiveValue A decrypted, sensitive property value - * - * @return a deterministic, securely hashed representation of the value which will be consistent across nodes. Safe to print in a log. - */ - @Override - public String getEncoded(final String plaintextSensitiveValue) { - try { - Mac mac = Mac.getInstance(HMAC_SHA256); - mac.init(secretKeySpec); - byte[] hashedBytes = mac.doFinal(plaintextSensitiveValue.getBytes(PROPERTY_CHARSET)); - return "[MASKED] (" + base64Encoder.encodeToString(hashedBytes) + ")"; - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - logger.error("Encountered an error making the sensitive value loggable: {}", e.getLocalizedMessage()); - return "[Unable to mask value]"; - } - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java index 5fee9763a5..7bb4eab8d5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java @@ -141,8 +141,6 @@ import org.apache.nifi.diagnostics.StorageUsage; import org.apache.nifi.diagnostics.SystemDiagnostics; import org.apache.nifi.diagnostics.SystemDiagnosticsFactory; import org.apache.nifi.encrypt.PropertyEncryptor; -import org.apache.nifi.encrypt.SensitiveValueEncoder; -import org.apache.nifi.encrypt.StandardSensitiveValueEncoder; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.events.BulletinFactory; import org.apache.nifi.events.EventReporter; @@ -356,11 +354,6 @@ public class FlowController implements ReportingTaskProvider, FlowAnalysisRulePr */ private final PropertyEncryptor encryptor; - /** - * The sensitive value string encoder (hasher) - */ - private final SensitiveValueEncoder sensitiveValueEncoder; - private final ScheduledExecutorService clusterTaskExecutor = new FlowEngine(3, "Clustering Tasks", true); private final ResourceClaimManager resourceClaimManager = new StandardResourceClaimManager(); @@ -514,8 +507,6 @@ public class FlowController implements ReportingTaskProvider, FlowAnalysisRulePr throw new IllegalStateException("Flow controller TLS configuration is invalid", e); } - this.sensitiveValueEncoder = new StandardSensitiveValueEncoder(nifiProperties); - timerDrivenEngineRef = new AtomicReference<>(new FlowEngine(maxTimerDrivenThreads.get(), "Timer-Driven Process")); final FlowFileRepository flowFileRepo = createFlowFileRepository(nifiProperties, extensionManager, resourceClaimManager); @@ -1409,10 +1400,6 @@ public class FlowController implements ReportingTaskProvider, FlowAnalysisRulePr return encryptor; } - public SensitiveValueEncoder getSensitiveValueEncoder() { - return sensitiveValueEncoder; - } - /** * @return the ExtensionManager used for instantiating Processors, * Prioritizers, etc.