NIFI-7119 Implement boundary checking for Argon2 cost parameters (#4111)

* NIFI-7119 Implemented parameter boundary enforcement for Argon2SecureHasher constructor.
Added unit tests for validating each parameter check.

* NIFI-7119 Refactored parameter validations. Added more test sizes to boundary checkers. Changed logger severity to error and added bounds to messages.

* NIFI-7119 Refactored Argon2 parameter data types to handle unsigned integer boundary values.
Updated unit tests.

Co-authored-by: Andy LoPresto <alopresto@apache.org>

Signed-off-by: Andy LoPresto <alopresto@apache.org>
This commit is contained in:
M Tien 2020-03-11 15:51:15 -07:00 committed by GitHub
parent d68720920f
commit 290bd378d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 284 additions and 10 deletions

View File

@ -46,17 +46,28 @@ public class Argon2SecureHasher implements SecureHasher {
private static final int DEFAULT_PARALLELISM = 1;
private static final int DEFAULT_MEMORY = 1 << 12;
private static final int DEFAULT_ITERATIONS = 3;
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 long MAX_PARALLELISM = Math.round(Math.pow(2, 24)) - 1;
private static final int MIN_HASH_LENGTH = 4;
private static final int MIN_ITERATIONS = 1;
private static final int MIN_SALT_LENGTH = 8;
private final int hashLength;
private final int memory;
// 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 int iterations;
private final int saltLength;
private final Integer iterations;
private final Integer saltLength;
private final boolean usingStaticSalt;
private boolean usingStaticSalt;
// A 16 byte salt (nonce) is recommended for password hashing
private static final byte[] staticSalt = "NiFi Static Salt".getBytes(StandardCharsets.UTF_8);
private static final byte[] STATIC_SALT = "NiFi Static Salt".getBytes(StandardCharsets.UTF_8);
// Upper boundary for several cost parameters
private static final long UPPER_BOUNDARY = Math.round(Math.pow(2, 32)) - 1;
/**
* Instantiates an Argon2 secure hasher using the default cost parameters
@ -72,19 +83,21 @@ public class Argon2SecureHasher implements SecureHasher {
/**
* 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 KB 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(int hashLength, int memory, int parallelism, int iterations) {
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 KB used ({@code 8p to 2^32 - 1})
@ -92,15 +105,51 @@ public class Argon2SecureHasher implements SecureHasher {
* @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(int hashLength, int memory, int parallelism, int iterations, int saltLength) {
// TODO: Implement boundary checking
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 KB 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.");
}
if (saltLength > 0) {
if (!isSaltLengthValid(saltLength)) {
logger.error("The salt length {} is outside the boundary of 8 to 2^32 - 1.", saltLength);
throw new IllegalArgumentException("Invalid salt length exceeds the saltLength boundary.");
}
this.usingStaticSalt = false;
} else {
this.usingStaticSalt = true;
@ -126,7 +175,7 @@ public class Argon2SecureHasher implements SecureHasher {
*/
byte[] getSalt() {
if (isUsingStaticSalt()) {
return staticSalt;
return STATIC_SALT;
} else {
SecureRandom sr = new SecureRandom();
byte[] salt = new byte[saltLength];
@ -135,6 +184,75 @@ public class Argon2SecureHasher implements SecureHasher {
}
}
/**
* 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) {
if (hashLength < DEFAULT_HASH_LENGTH) {
logger.warn("The provided hash length {} is below the recommended minimum {}.", hashLength, DEFAULT_HASH_LENGTH);
}
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 whether the provided salt length (saltLength) is within boundaries. The lower bound >= 8 and the
* upper bound <= 2^32 - 1.
* @param saltLength the salt length in bytes
* @return true if saltLength is within boundaries
*/
public static boolean isSaltLengthValid(Integer saltLength) {
if (saltLength == 0) {
logger.debug("The provided salt length 0 indicates a static salt of {} bytes", DEFAULT_SALT_LENGTH);
return true;
}
if (saltLength < DEFAULT_SALT_LENGTH) {
logger.warn("The provided dynamic salt length {} is below the recommended minimum {}", saltLength, DEFAULT_SALT_LENGTH);
}
return saltLength >= MIN_SALT_LENGTH && saltLength <= UPPER_BOUNDARY;
}
/**
* Returns a String representation of {@code Argon2(input)} in hex-encoded format.
*

View File

@ -239,4 +239,160 @@ class Argon2SecureHasherTest extends GroovyTestCase {
assert resultDurations.min() > MIN_DURATION_NANOS
assert resultDurations.sum() / testIterations > MIN_DURATION_NANOS
}
@Test
void testShouldVerifyHashLengthBoundary() throws Exception {
// Arrange
final int hashLength = 128
// Act
boolean valid = Argon2SecureHasher.isHashLengthValid(hashLength)
// Assert
assert valid
}
@Test
void testShouldFailHashLengthBoundary() throws Exception {
// Arrange
def hashLengths = [-8, 0, 1, 2]
// Act
def results = hashLengths.collect { hashLength ->
def isValid = Argon2SecureHasher.isHashLengthValid(hashLength)
[hashLength, isValid]
}
// Assert
results.each { hashLength, isHashLengthValid ->
logger.info("For hashLength value ${hashLength}, hashLength is ${isHashLengthValid ? "valid" : "invalid"}")
assert !isHashLengthValid
}
}
@Test
void testShouldVerifyMemorySizeBoundary() throws Exception {
// Arrange
final int memory = 2048
// Act
boolean valid = Argon2SecureHasher.isMemorySizeValid(memory)
// Assert
assert valid
}
@Test
void testShouldFailMemorySizeBoundary() throws Exception {
// Arrange
def memorySizes = [-12, 0, 1, 6]
// Act
def results = memorySizes.collect { memory ->
def isValid = Argon2SecureHasher.isMemorySizeValid(memory)
[memory, isValid]
}
// Assert
results.each { memory, isMemorySizeValid ->
logger.info("For memory size ${memory}, memory is ${isMemorySizeValid ? "valid" : "invalid"}")
assert !isMemorySizeValid
}
}
@Test
void testShouldVerifyParallelismBoundary() throws Exception {
// Arrange
final int parallelism = 4
// Act
boolean valid = Argon2SecureHasher.isParallelismValid(parallelism)
// Assert
assert valid
}
@Test
void testShouldFailParallelismBoundary() throws Exception {
// Arrange
def parallelisms = [-8, 0, 16777220, 16778000]
// Act
def results = parallelisms.collect { parallelism ->
def isValid = Argon2SecureHasher.isParallelismValid(parallelism)
[parallelism, isValid]
}
// Assert
results.each { parallelism, isParallelismValid ->
logger.info("For parallelization factor ${parallelism}, parallelism is ${isParallelismValid ? "valid" : "invalid"}")
assert !isParallelismValid
}
}
@Test
void testShouldVerifyIterationsBoundary() throws Exception {
// Arrange
final int iterations = 4
// Act
boolean valid = Argon2SecureHasher.isIterationsValid(iterations)
// Assert
assert valid
}
@Test
void testShouldFailIterationsBoundary() throws Exception {
// Arrange
def iterationCounts = [-50, -1, 0]
// Act
def results = iterationCounts.collect { iterations ->
def isValid = Argon2SecureHasher.isIterationsValid(iterations)
[iterations, isValid]
}
// Assert
results.each { iterations, isIterationsValid ->
logger.info("For iteration counts ${iterations}, iteration is ${isIterationsValid ? "valid" : "invalid"}")
assert !isIterationsValid
}
}
@Test
void testShouldVerifySaltLengthBoundary() throws Exception {
// Arrange
def saltLengths = [0, 64]
// Act
def results = saltLengths.collect { saltLength ->
def isValid = Argon2SecureHasher.isSaltLengthValid(saltLength)
[saltLength, isValid]
}
// Assert
results.each { saltLength, isSaltLengthValid ->
logger.info("For salt length ${saltLength}, saltLength is ${isSaltLengthValid ? "valid" : "invalid"}")
assert isSaltLengthValid
}
}
@Test
void testShouldFailSaltLengthBoundary() throws Exception {
// Arrange
def saltLengths = [-16, 4]
// Act
def results = saltLengths.collect { saltLength ->
def isValid = Argon2SecureHasher.isSaltLengthValid(saltLength)
[saltLength, isValid]
}
// Assert
results.each { saltLength, isSaltLengthValid ->
logger.info("For salt length ${saltLength}, saltLength is ${isSaltLengthValid ? "valid" : "invalid"}")
assert !isSaltLengthValid
}
}
}