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 <pierre.villard.fr@gmail.com>

This closes #7873.
This commit is contained in:
exceptionfactory 2023-10-12 08:14:41 -05:00 committed by Pierre Villard
parent 2bcdcab5dd
commit a849ca044c
No known key found for this signature in database
GPG Key ID: F92A93B30C07C6D5
13 changed files with 0 additions and 2007 deletions

View File

@ -35,10 +35,6 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-deprecation-log</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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.
* <p>
* One <strong>critical</strong> difference is that this implementation uses a
* <strong>static universal</strong> 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;
}
}

View File

@ -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.
* <p>
* One <strong>critical</strong> difference is that this implementation uses a
* <strong>static universal</strong> 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}).
* <p>
* The resulting output is referred to as a <em>hash</em> 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();
}
}
}

View File

@ -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);
}

View File

@ -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;
/**
* <p> 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<KeyDerivationFunction, Class<? extends SecureHasher>> registeredSecureHashers;
private static final Class<? extends SecureHasher> 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<? extends SecureHasher> 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);
}
}
}

View File

@ -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<String> 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<String> 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<String> 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<String> hexResults = new ArrayList<>();
final List<String> 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<String> results = new ArrayList<>();
final List<Long> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer, byte[]> 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}
}
}

View File

@ -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<String> 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<String> 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<String> inputs = Arrays.asList(null, "");
final String EXPECTED_HASH_HEX = "7f2d8d8c7aaa45471f6c05a8edfe0a3f75fe01478cc965c5dce664e2ac6f5d0a";
final String EXPECTED_HASH_BASE64 = "fy2NjHqqRUcfbAWo7f4KP3X+AUeMyWXF3OZk4qxvXQo";
// Act
PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher();
List<String> hexResults = inputs.stream()
.map(input -> pbkdf2SecureHasher.hashHex(input))
.collect(Collectors.toList());
List<String> 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<String> results = new ArrayList<>();
final List<Long> 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<Integer> 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<Integer> 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<Integer> validHLengths = Arrays.asList(32, 64);
// 1 and MAX_VALUE are the length boundaries, inclusive
List<Integer> 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<Integer> validHLengths = Arrays.asList(32, 64);
// MAX_VALUE + 1 will become MIN_VALUE because of signed integer math
List<Integer> 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<Integer> 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<Integer> saltLengths = Arrays.asList(-8, 1, Integer.MAX_VALUE + 1);
// Act and Assert
PBKDF2SecureHasher pbkdf2SecureHasher = new PBKDF2SecureHasher();
saltLengths.forEach(saltLength -> assertFalse(pbkdf2SecureHasher.isSaltLengthValid(saltLength)));
}
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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]";
}
}
}

View File

@ -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.