mirror of https://github.com/apache/nifi.git
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:
parent
2bcdcab5dd
commit
a849ca044c
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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]";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue