NIFI-5209 Removed secure hash functionality from ConfigEncryptionTool.

Removed relevant unit tests.

This closes #2761.

Signed-off-by: Kevin Doran <kdoran@apache.org>
This commit is contained in:
Andy LoPresto 2018-06-04 12:39:34 -07:00 committed by Kevin Doran
parent ead3969ab7
commit d02cd4f909
No known key found for this signature in database
GPG Key ID: 5621A6244B77AC02
2 changed files with 5 additions and 1133 deletions

View File

@ -17,7 +17,6 @@
package org.apache.nifi.properties
import groovy.io.GroovyPrintWriter
import groovy.json.JsonBuilder
import groovy.xml.XmlUtil
import org.apache.commons.cli.CommandLine
import org.apache.commons.cli.CommandLineParser
@ -28,8 +27,6 @@ import org.apache.commons.cli.Options
import org.apache.commons.cli.ParseException
import org.apache.commons.codec.binary.Hex
import org.apache.commons.io.IOUtils
import org.apache.nifi.security.util.crypto.CipherUtility
import org.apache.nifi.security.util.crypto.scrypt.Scrypt
import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
import org.apache.nifi.toolkit.tls.commandLine.ExitCode
import org.apache.nifi.util.NiFiProperties
@ -47,9 +44,7 @@ import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import java.nio.charset.StandardCharsets
import java.security.InvalidKeyException
import java.security.KeyException
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Security
import java.util.zip.GZIPInputStream
@ -67,15 +62,11 @@ class ConfigEncryptionTool {
public String outputAuthorizersPath
public String flowXmlPath
public String outputFlowXmlPath
// If this value can be set by the running user, it can point to a manipulated file anywhere
private static String secureHashPath = "./secure_hash.key"
private String keyHex
private String migrationKeyHex
private String password
private String migrationPassword
private String secureHashKey
private String secureHashPassword
// This is the raw value used in nifi.sensitive.props.key
private String flowPropertiesPassword
@ -90,7 +81,6 @@ class ConfigEncryptionTool {
private boolean usingPassword = true
private boolean usingPasswordMigration = true
private boolean usingSecureHash = false
private boolean migration = false
private boolean isVerbose = false
private boolean handlingNiFiProperties = false
@ -98,7 +88,6 @@ class ConfigEncryptionTool {
private boolean handlingAuthorizers = false
private boolean handlingFlowXml = false
private boolean ignorePropertiesFiles = false
private boolean queryingCurrentHashParams = false
private boolean translatingCli = false
private static final String HELP_ARG = "help"
@ -116,15 +105,12 @@ class ConfigEncryptionTool {
private static final String PASSWORD_ARG = "password"
private static final String KEY_MIGRATION_ARG = "oldKey"
private static final String PASSWORD_MIGRATION_ARG = "oldPassword"
private static final String HASHED_KEY_MIGRATION_ARG = "secureHashKey"
private static final String HASHED_PASSWORD_MIGRATION_ARG = "secureHashPassword"
private static final String USE_KEY_ARG = "useRawKey"
private static final String MIGRATION_ARG = "migrate"
private static final String PROPS_KEY_ARG = "propsKey"
private static final String DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG = "encryptFlowXmlOnly"
private static final String NEW_FLOW_ALGORITHM_ARG = "newFlowAlgorithm"
private static final String NEW_FLOW_PROVIDER_ARG = "newFlowProvider"
private static final String CURRENT_HASH_PARAMS_ARG = "currentHashParams"
private static final String TRANSLATE_CLI_ARG = "translateCli"
// Static holder to avoid re-generating the options object multiple times in an invocation
@ -140,7 +126,6 @@ class ConfigEncryptionTool {
private static final int SCRYPT_R = 8
private static final int SCRYPT_P = 1
static final String CURRENT_SCRYPT_VERSION = "s0"
private static final String NIFI_SCRYPT_PATTERN = /^\$\w{2}\$\w{5,}\$[\w\/\=\+]{22,}\$[\w\/\=\+]{43}$/
// Hard-coded values from StandardPBEByteEncryptor which will be removed during refactor of all flow encryption code in NIFI-1465
private static final int DEFAULT_KDF_ITERATIONS = 1000
@ -200,7 +185,6 @@ class ConfigEncryptionTool {
private static final String DEFAULT_PROVIDER = BouncyCastleProvider.PROVIDER_NAME
private static final String DEFAULT_FLOW_ALGORITHM = "PBEWITHMD5AND256BITAES-CBC-OPENSSL"
private static final int AMBARI_COMPATIBLE_SCRYPT_HASH_LENGTH = 256
private static final Map<String, String> PROPERTY_KEY_MAP = [
"nifi.security.keystore": "keystore",
@ -250,15 +234,12 @@ class ConfigEncryptionTool {
options.addOption(Option.builder("e").longOpt(KEY_MIGRATION_ARG).hasArg(true).argName("keyhex").desc("The old raw hexadecimal key to use during key migration").build())
options.addOption(Option.builder("p").longOpt(PASSWORD_ARG).hasArg(true).argName("password").desc("The password from which to derive the key to use to encrypt the sensitive properties").build())
options.addOption(Option.builder("w").longOpt(PASSWORD_MIGRATION_ARG).hasArg(true).argName("password").desc("The old password from which to derive the key during migration").build())
options.addOption(Option.builder("y").longOpt(HASHED_KEY_MIGRATION_ARG).hasArg(true).argName("hashed_keyhex").desc("The old securely-hashed hexadecimal key to authenticate during key migration (see NiFi Admin Guide)").build())
options.addOption(Option.builder("z").longOpt(HASHED_PASSWORD_MIGRATION_ARG).hasArg(true).argName("hashed_password").desc("The old securely-hashed password to authenticate during key migration (see NiFi Admin Guide)").build())
options.addOption(Option.builder("r").longOpt(USE_KEY_ARG).hasArg(false).desc("If provided, the secure console will prompt for the raw key value in hexadecimal form").build())
options.addOption(Option.builder("m").longOpt(MIGRATION_ARG).hasArg(false).desc("If provided, the nifi.properties and/or login-identity-providers.xml sensitive properties will be re-encrypted with a new key").build())
options.addOption(Option.builder("x").longOpt(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG).hasArg(false).desc("If provided, the properties in flow.xml.gz will be re-encrypted with a new key but the nifi.properties and/or login-identity-providers.xml files will not be modified").build())
options.addOption(Option.builder("s").longOpt(PROPS_KEY_ARG).hasArg(true).argName("password|keyhex").desc("The password or key to use to encrypt the sensitive processor properties in flow.xml.gz").build())
options.addOption(Option.builder("A").longOpt(NEW_FLOW_ALGORITHM_ARG).hasArg(true).argName("algorithm").desc("The algorithm to use to encrypt the sensitive processor properties in flow.xml.gz").build())
options.addOption(Option.builder("P").longOpt(NEW_FLOW_PROVIDER_ARG).hasArg(true).argName("algorithm").desc("The security provider to use to encrypt the sensitive processor properties in flow.xml.gz").build())
options.addOption(Option.builder().longOpt(CURRENT_HASH_PARAMS_ARG).hasArg(false).desc("Returns the current salt and cost params used to store the hashed key/password").build())
options.addOption(Option.builder("c").longOpt(TRANSLATE_CLI_ARG).hasArg(false).desc("Translates the nifi.properties file to a format suitable for the NiFi CLI tool").build())
options
}
@ -304,17 +285,6 @@ class ConfigEncryptionTool {
isVerbose = commandLine.hasOption(VERBOSE_ARG)
// If this flag is present, ensure no other options are present and then fail/return
if (commandLine.hasOption(CURRENT_HASH_PARAMS_ARG)) {
queryingCurrentHashParams = true
if (commandLineHasActionFlags(commandLine, [CURRENT_HASH_PARAMS_ARG])) {
printUsageAndThrow("When '--${CURRENT_HASH_PARAMS_ARG}' is specified, only '-h'/'--${HELP_ARG}' and '-v'/'--${VERBOSE_ARG}' are allowed", ExitCode.INVALID_ARGS)
} else {
// Otherwise return (avoid unnecessary parsing)
return commandLine
}
}
// If this flag is present, ensure no other options are present and then fail/return
if (commandLine.hasOption(TRANSLATE_CLI_ARG)) {
translatingCli = true
@ -439,29 +409,6 @@ class ConfigEncryptionTool {
if (isVerbose) {
logger.info("Key migration mode activated")
}
if (isSecureHashArgumentPresent(commandLine)) {
logger.info("Secure hash argument present")
// Check for old key/password and throw error
if (commandLine.hasOption(KEY_MIGRATION_ARG) || commandLine.hasOption(PASSWORD_MIGRATION_ARG)) {
printUsageAndThrow("If the '-w'/'--${PASSWORD_MIGRATION_ARG}' or '-e'/'--${KEY_MIGRATION_ARG}' arguments are present, '-z'/'--${HASHED_PASSWORD_MIGRATION_ARG}' and '-y'/'--${HASHED_KEY_MIGRATION_ARG}' cannot be used", ExitCode.INVALID_ARGS)
}
// Check for both key and password and throw error
if (commandLine.hasOption(HASHED_KEY_MIGRATION_ARG) && commandLine.hasOption(HASHED_PASSWORD_MIGRATION_ARG)) {
printUsageAndThrow("Only one of '-z'/'--${HASHED_PASSWORD_MIGRATION_ARG}' and '-y'/'--${HASHED_KEY_MIGRATION_ARG}' can be used together", ExitCode.INVALID_ARGS)
}
// Extract flags to field
if (commandLine.hasOption(HASHED_KEY_MIGRATION_ARG)) {
secureHashKey = commandLine.getOptionValue(HASHED_KEY_MIGRATION_ARG)
} else {
secureHashPassword = commandLine.getOptionValue(HASHED_PASSWORD_MIGRATION_ARG)
}
// Set boolean flag to true
usingSecureHash = true
}
if (commandLine.hasOption(PASSWORD_MIGRATION_ARG)) {
usingPasswordMigration = true
if (commandLine.hasOption(KEY_MIGRATION_ARG)) {
@ -471,8 +418,8 @@ class ConfigEncryptionTool {
}
} else {
migrationKeyHex = commandLine.getOptionValue(KEY_MIGRATION_ARG)
// Use the "migration password" value if the migration key hex is absent and the secure hash password/key hex is absent (if either are present, the migration password is not)
usingPasswordMigration = !migrationKeyHex && !usingSecureHash
// Use the "migration password" value if the migration key hex is absent
usingPasswordMigration = !migrationKeyHex
}
} else {
if (commandLine.hasOption(PASSWORD_MIGRATION_ARG) || commandLine.hasOption(KEY_MIGRATION_ARG)) {
@ -544,9 +491,6 @@ class ConfigEncryptionTool {
}
}
static boolean isSecureHashArgumentPresent(CommandLine commandLine) {
commandLine.hasOption(HASHED_PASSWORD_MIGRATION_ARG) || commandLine.hasOption(HASHED_KEY_MIGRATION_ARG)
}
/**
* The method returns the provided, derived, or securely-entered key in hex format. The reason the parameters must be provided instead of read from the fields is because this is used for the regular key/password and the migration key/password.
*
@ -583,86 +527,7 @@ class ConfigEncryptionTool {
}
private String getMigrationKey() {
if (usingSecureHash) {
// The boolean flag for "key" means the expression should evaluate to true when key is present and password is not
String knownHashValue = readSecureHashValueFromFile(secureHashKey && !secureHashPassword)
if (checkHashedValue(knownHashValue, getProvidedSecureHashValue())) {
// Retrieve the key from bootstrap.conf because the caller only has the hashed version available
return readMasterKeyFromBootstrap()
} else {
throw new InvalidKeyException("The provided hashed key/password is not correct")
}
} else {
return getKeyInternal(TextDevices.defaultTextDevice(), migrationKeyHex, migrationPassword, usingPasswordMigration)
}
}
private String getProvidedSecureHashValue() {
if (usingSecureHash) {
return secureHashPassword ?: secureHashKey
} else {
return ""
}
}
private static String readSecureHashValueFromFile(boolean readKey = true) {
File secureHashFile = new File(secureHashPath)
if (!secureHashFile.canRead()) {
throw new IOException("Cannot read from secure hash file")
}
List<String> lines = secureHashFile.readLines()
String linePrefix = readKey ? "secureHashKey" : "secureHashPassword"
String relevantLine = lines.find { it.startsWith(linePrefix) }
String hashValue = relevantLine?.split("=")?.last()
if (!hashValue) {
throw new InvalidKeyException("The secure hash of the ${readKey ? "key" : "password"} could not be read from the stored file")
} else {
return hashValue
}
}
/**
* Returns true if the hash values are equivalent. Currently performs a *constant-time equality* check on the two values. As the scrypt format does not allow for reversing, two "equivalent" but non-identical hash values cannot be compared for equality.
*
* Example (byte equivalent):
*
* KHV: {@code $s0$40801$AAAAAAAAAAAAAAAAAAAAAA$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM}
* UHV: {@code $s0$40801$AAAAAAAAAAAAAAAAAAAAAA$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM}
* Result: EQUAL
*
* Example (semantically equivalent):
*
* KHV: {@code $s0$40801$AAAAAAAAAAAAAAAAAAAAAA$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM}
* UHV: {@code $s0$40801$ABCDEFGHIJKLMNOPQRSTUQ$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8}
* Result: NOT EQUAL
*
* Even though both hash values are the result of the input "password", by design the hash values cannot be reversed to a common origin to determine their equality. If the raw input was known, both hashes could be determined to be valid, thus asserting the correctness of the raw input and the functional equivalence of the hash values.
*
* @param knownHashValue
* @param unknownHashValue
* @return true if the hash values are present and equal
*/
static boolean checkHashedValue(String knownHashValue, String unknownHashValue) {
if (!knownHashValue || !unknownHashValue) {
return false
}
// The values should be in scrypt format
if (!verifyHashFormat(knownHashValue) || !verifyHashFormat(unknownHashValue)) {
return false
}
return MessageDigest.isEqual(knownHashValue.getBytes(StandardCharsets.UTF_8), unknownHashValue.getBytes(StandardCharsets.UTF_8))
}
/**
* Returns true if the provided hash is in the valid format for NiFi secured hash storage. NiFi enforces additional constraints over the minimum Scrypt requirements (16+ byte [22+ B64] salt, 32 byte [43 B64] hash).
*
* @param hash the hash to verify
* @return true if the format is acceptable
*/
static boolean verifyHashFormat(String hash) {
hash =~ NIFI_SCRYPT_PATTERN
}
private static String getFlowPassword(TextDevice textDevice = TextDevices.defaultTextDevice()) {
@ -679,10 +544,6 @@ class ConfigEncryptionTool {
new String(textDevice.readPassword())
}
private String readMasterKeyFromBootstrap() {
NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfPath)
}
/**
* Returns the key in uppercase hexadecimal format with delimiters (spaces, '-', etc.) removed. All non-hex chars are removed. If the result is not a valid length (32, 48, 64 chars depending on the JCE), an exception is thrown.
*
@ -1389,41 +1250,6 @@ class ConfigEncryptionTool {
}
}
/**
* Writes the contents of the secure hash configuration file (hashed value of current migration password/key hex) to the output {@code secure_hash.key} file.
*
* @throw IOException if there is a problem reading or writing the secure_hash.key file
*/
private void writeSecureHash() throws IOException {
if (!secureHashPath) {
throw new IllegalArgumentException("Cannot write hashed password/key to empty secure_hash.key path")
}
File secureHashFile = new File(secureHashPath)
if (isSafeToWrite(secureHashFile)) {
try {
List<String> secureHashFileLines = []
// Calculate the secure hash of the current key (and password if provided) using current default values for cost params and random salt
String secureHashKey = secureHashKey(keyHex)
secureHashFileLines << "secureHashKey=${secureHashKey}"
if (password) {
String secureHashPassword = secureHashPassword(password)
secureHashFileLines << "secureHashPassword=${secureHashPassword}"
}
// Write the updated values back to the file
secureHashFile.text = secureHashFileLines.join("\n")
} catch (IOException e) {
def msg = "Encountered an exception updating the secure_hash.key file with the hashed value(s)"
logger.error(msg, e)
throw e
}
} else {
throw new IOException("The secure_hash.key file at ${secureHashPath} must be writable by the user running this tool")
}
}
private
static List<String> serializeNiFiPropertiesAndPreserveFormat(NiFiProperties niFiProperties, File originalPropertiesFile) {
List<String> lines = originalPropertiesFile.readLines()
@ -1555,32 +1381,6 @@ class ConfigEncryptionTool {
"NIFI_SCRYPT_SALT".getBytes(StandardCharsets.UTF_8)
}
private static byte[] generateRandomSalt(int bits = 128) {
byte[] salt = new byte[bits / 8]
new SecureRandom().nextBytes(salt)
salt
}
/**
* Generates an scrypt salt using the provided parameters and encodes it in the proper format. If a value is not provided, the listed default will be used.
*
* @param rawSalt the raw 128 bit salt to use (default is to generate a random value)
* @param n the CPU/memory cost factor (default is {@value ConfigEncryptionTool#SCRYPT_N})
* @param r the blocksize factor (default is {@value ConfigEncryptionTool#SCRYPT_R})
* @param p the parallelization factor (default is {@value ConfigEncryptionTool#SCRYPT_P})
* @param version the salt version identifier (default is {@value ConfigEncryptionTool#CURRENT_SCRYPT_VERSION})
* @return the scrypt-formatted salt (e.g. {@code $s0$e0101$ABCDEFGHIJKLMNOPQRSTUV})
*/
private
static String generateScryptSalt(byte[] rawSalt = generateRandomSalt(128), int n = SCRYPT_N, int r = SCRYPT_R, int p = SCRYPT_P, String version = CURRENT_SCRYPT_VERSION) {
String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
def versionString = "\$${version}\$"
if (!formattedSalt.startsWith(versionString)) {
formattedSalt = formattedSalt.replaceFirst(/$\w{2}$/, versionString)
}
return formattedSalt
}
private String getExistingFlowPassword() {
return niFiProperties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY) as String ?: DEFAULT_NIFI_SENSITIVE_PROPS_KEY
}
@ -1603,73 +1403,6 @@ class ConfigEncryptionTool {
}
}
static String secureHashPassword(String password, String salt = generateScryptSalt()) {
// If empty, generate complete salt
if (!salt) {
salt = generateScryptSalt()
}
// The findAll() drops any empty elements
def saltComponents = salt.split('\\$').findAll()
if (saltComponents.size() < 3) {
throw new IllegalArgumentException("The provided salt was not in valid scrypt format")
}
if (saltComponents.first() != CURRENT_SCRYPT_VERSION) {
throw new IllegalArgumentException("Currently, only scrypt hashes with version ${CURRENT_SCRYPT_VERSION} are supported")
}
// Split the encoded params into N, R, P
List<Integer> params = Scrypt.parseParameters(saltComponents[1])
// Generate the hashed format
Scrypt.scrypt(password, Base64.decoder.decode(saltComponents[2]), params[0], params[1], params[2], AMBARI_COMPATIBLE_SCRYPT_HASH_LENGTH)
}
static String secureHashKey(String keyHex, String salt = generateScryptSalt()) {
// Lowercase the key hex, then call secureHashPassword() as the algorithm is the same
secureHashPassword(keyHex?.toLowerCase(), salt)
}
private String getCurrentHashParams() {
try {
int N
int r
int p
String base64Salt
try {
String secureHash = readSecureHashValueFromFile()
// The findAll() drops any empty elements
def hashComponents = secureHash.split('\\$').findAll()
if (hashComponents.size() < 3) {
throw new IllegalArgumentException("The provided secure hash was not in valid scrypt format")
}
if (hashComponents.first() != CURRENT_SCRYPT_VERSION) {
throw new IllegalArgumentException("Currently, only scrypt hashes with version ${CURRENT_SCRYPT_VERSION} are supported")
}
// Split the encoded params into N, R, P
List<Integer> params = Scrypt.parseParameters(hashComponents[1])
N = params[0]
r = params[1]
p = params[2]
base64Salt = hashComponents[2]
} catch (IOException | InvalidKeyException e) {
// These exceptions occur if the file doesn't exist, can't be read, or doesn't have secure hashes populated
N = SCRYPT_N
r = SCRYPT_R
p = SCRYPT_P
base64Salt = CipherUtility.encodeBase64NoPadding(generateRandomSalt())
}
return new JsonBuilder([N: N, r: r, p: p, salt: base64Salt]).toString()
} catch (Exception e) {
logger.error("Encountered an exception getting current hash parameters: ${e.getLocalizedMessage()}")
printUsageAndThrow(e.getLocalizedMessage(), ExitCode.ERROR_READING_CONFIG)
}
}
/**
* Runs main tool logic (parsing arguments, reading files, protecting properties, and writing key and properties out to destination files).
*
@ -1684,12 +1417,6 @@ class ConfigEncryptionTool {
try {
tool.parse(args)
// Ensure the only content written to STDOUT is the JSON (consumable by other processes)
if (tool.queryingCurrentHashParams) {
System.out.println(tool.getCurrentHashParams())
System.exit(ExitCode.SUCCESS.ordinal())
}
// Handle the translate CLI case
if (tool.translatingCli) {
if (tool.bootstrapConfPath) {
@ -1851,9 +1578,6 @@ class ConfigEncryptionTool {
synchronized (this) {
if (!tool.ignorePropertiesFiles) {
tool.writeKeyToBootstrapConf()
// Always write the secure hash in case the next invocation needs it
tool.writeSecureHash()
}
if (tool.handlingFlowXml) {
tool.writeFlowXmlToFile(tool.flowXml)

View File

@ -16,8 +16,6 @@
*/
package org.apache.nifi.properties
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import org.apache.commons.cli.CommandLine
import org.apache.commons.cli.CommandLineParser
import org.apache.commons.cli.DefaultParser
@ -25,13 +23,11 @@ import org.apache.commons.lang3.SystemUtils
import org.apache.log4j.AppenderSkeleton
import org.apache.log4j.spi.LoggingEvent
import org.apache.nifi.encrypt.StringEncryptor
import org.apache.nifi.security.util.crypto.scrypt.Scrypt
import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
import org.apache.nifi.util.NiFiProperties
import org.apache.nifi.util.console.TextDevice
import org.apache.nifi.util.console.TextDevices
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.util.encoders.Hex
import org.junit.After
import org.junit.AfterClass
import org.junit.Assume
@ -124,10 +120,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
}
@Before
void setUp() throws Exception {
// Manually override the constant path to allow for easy cleanup
ConfigEncryptionTool.secureHashPath = "target/tmp/secure_hash.key"
}
void setUp() throws Exception {}
@After
void tearDown() throws Exception {
@ -425,81 +418,6 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
assert msg =~ "Only one of '-w'/'--oldPassword' and '-e'/'--oldKey' can be used"
}
@Test
void testParseShouldFailIfMigrationPasswordAndHashedPasswordBothProvided() {
// Arrange
ConfigEncryptionTool tool = new ConfigEncryptionTool()
// Act
def msg = shouldFail {
tool.parse("-m -n nifi.properties -w oldPassword -z oldPasswordHashed".split(" ") as String[])
}
logger.expected(msg)
// Assert
assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' cannot be used"
}
@Test
void testParseShouldFailIfMigrationPasswordAndHashedKeyBothProvided() {
// Arrange
ConfigEncryptionTool tool = new ConfigEncryptionTool()
// Act
def msg = shouldFail {
tool.parse("-m -n nifi.properties -w oldPassword -y oldKeyHashed".split(" ") as String[])
}
logger.expected(msg)
// Assert
assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' cannot be used"
}
@Test
void testParseShouldFailIfMigrationKeyAndHashedPasswordBothProvided() {
// Arrange
ConfigEncryptionTool tool = new ConfigEncryptionTool()
// Act
def msg = shouldFail {
tool.parse("-m -n nifi.properties -e oldKey -z oldPasswordHashed".split(" ") as String[])
}
logger.expected(msg)
// Assert
assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' cannot be used"
}
@Test
void testParseShouldFailIfMigrationKeyAndHashedKeyBothProvided() {
// Arrange
ConfigEncryptionTool tool = new ConfigEncryptionTool()
// Act
def msg = shouldFail {
tool.parse("-m -n nifi.properties -e oldKey -y oldKeyHashed".split(" ") as String[])
}
logger.expected(msg)
// Assert
assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' cannot be used"
}
@Test
void testParseShouldFailIfHashedPasswordAndHashedKeyBothProvided() {
// Arrange
ConfigEncryptionTool tool = new ConfigEncryptionTool()
// Act
def msg = shouldFail {
tool.parse("-m -n nifi.properties -z oldPasswordHashed -y oldKeyHashed".split(" ") as String[])
}
logger.expected(msg)
// Assert
assert msg =~ "Only one of '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' can be used together"
}
@Test
void testParseShouldFailIfPropertiesAndProvidersMissing() {
// Arrange
@ -2009,631 +1927,6 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Assertions in common method above
}
/**
* Helper method to execute key migration test for varying combinations of new key/password with securely hashed key/password.
*
* @param scenario a human-readable description of the test scenario
* @param scenarioArgs a list of the arguments specific to this scenario to be passed to the tool
* @param oldHashedPassword the secure hash of the original password
* @param newPassword the new password
* @param oldHashedKeyHex the original key hex (if present, original hashed password is ignored)
* @param newKeyHex the new key hex (if present, new password is ignored; if not, this is derived)
*/
private void performSecureHashKeyMigration(String scenario, List scenarioArgs, String oldHashedPassword = HASHED_PASSWORD, String newPassword = PASSWORD.reverse(), String oldHashedKeyHex = "", String newKeyHex = "", int desiredExitCode = 0) {
// Arrange
exit.expectSystemExitWithStatus(desiredExitCode)
// Initial set up
File tmpDir = new File("target/tmp/")
tmpDir.mkdirs()
setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
String bootstrapPath = isUnlimitedStrengthCryptoAvailable() ? "src/test/resources/bootstrap_with_master_key_password.conf" :
"src/test/resources/bootstrap_with_master_key_password_128.conf"
File originalKeyFile = new File(bootstrapPath)
File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
bootstrapFile.delete()
Files.copy(originalKeyFile.toPath(), bootstrapFile.toPath())
final List<String> originalBootstrapLines = bootstrapFile.readLines()
String originalKeyLine = originalBootstrapLines.find {
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
}
// Copy the hashed credentials file
String secureHashedPasswordPath = isUnlimitedStrengthCryptoAvailable() ? "src/test/resources/secure_hash.key" :
"src/test/resources/secure_hash_128.key"
File originalSecureHashedPasswordFile = new File(secureHashedPasswordPath)
File secureHashedFile = new File("target/tmp/tmp_secure_hash.key")
secureHashedFile.delete()
Files.copy(originalSecureHashedPasswordFile.toPath(), secureHashedFile.toPath())
// Perform necessary key derivations
if (!newKeyHex) {
newKeyHex = ConfigEncryptionTool.deriveKeyFromPassword(newPassword)
logger.info("Migration key derived from password [${newPassword}]: \t${newKeyHex}")
} else {
logger.info("Migration key provided directly: \t${newKeyHex}")
}
// Extract old key hex from bootstrap.conf
String oldKeyHex = originalKeyLine.split("=", 2).last()
logger.info("Extracted old key hex from bootstrap.conf: ${oldKeyHex}")
String inputPropertiesPath = isUnlimitedStrengthCryptoAvailable() ?
"src/test/resources/nifi_with_sensitive_properties_protected_aes_password.properties" :
"src/test/resources/nifi_with_sensitive_properties_protected_aes_password_128.properties"
File inputPropertiesFile = new File(inputPropertiesPath)
File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties")
outputPropertiesFile.delete()
// Log original sensitive properties (encrypted with first key)
NiFiProperties inputProperties = NiFiPropertiesLoader.withKey(oldKeyHex).load(inputPropertiesFile)
logger.info("Loaded ${inputProperties.size()} properties from input file")
ProtectedNiFiProperties protectedInputProperties = new ProtectedNiFiProperties(inputProperties)
def originalSensitiveValues = protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key -> [(key): protectedInputProperties.getProperty(key)] }
logger.info("Original sensitive values: ${originalSensitiveValues}")
final String EXPECTED_NEW_KEY_LINE = ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + newKeyHex
// Act
String[] args = ["-n", inputPropertiesFile.path,
"-b", bootstrapFile.path,
"-o", outputPropertiesFile.path,
"-m",
"-v"]
List<String> localArgs = args + scenarioArgs
logger.info("Running [${scenario}] with args: ${localArgs}")
// If an error is expected, check that the nifi.properties file doesn't exist (i.e. nothing happened)
Assertion assertion
if (desiredExitCode != 0) {
assertion = new Assertion() {
void checkAssertion() {
assert !outputPropertiesFile.exists()
logger.expected("No output nifi.properties found")
// Check that the key was NOT persisted to the bootstrap.conf
final List<String> updatedBootstrapLines = bootstrapFile.readLines()
String updatedKeyLine = updatedBootstrapLines.find {
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
}
logger.info("'Updated' key line: ${updatedKeyLine}")
assert updatedKeyLine == originalKeyLine
assert originalBootstrapLines.size() == updatedBootstrapLines.size()
// Check that the secure hash was NOT persisted to the secure_hash.key
final List<String> updatedSecureHashLines = secureHashedFile.readLines()
String updatedSecureHashKeyLine = updatedSecureHashLines.find { it.startsWith("secureHashKey=") }
logger.info("'Updated' secure hash lines: \n${updatedSecureHashLines.join("\n")}")
int expectedSecureHashLineCount = 1
List<String> originalSecureHashLines = originalSecureHashedPasswordFile.readLines()
// Only evaluate the secure hash password line if the raw password was provided (otherwise, can't store hash)
if (newPassword) {
String updatedSecureHashPasswordLine = updatedSecureHashLines.find {
it.startsWith("secureHashPassword=")
}
expectedSecureHashLineCount = 2
logger.info("Asserting \n${updatedSecureHashPasswordLine} == \n${originalSecureHashLines.last()}")
assert updatedSecureHashPasswordLine == originalSecureHashLines.last()
}
logger.info("Asserting \n${updatedSecureHashKeyLine} == \n${originalSecureHashLines.first()}")
assert updatedSecureHashKeyLine == originalSecureHashLines.first()
assert updatedSecureHashLines.size() == expectedSecureHashLineCount
// Clean up
outputPropertiesFile.deleteOnExit()
bootstrapFile.deleteOnExit()
tmpDir.deleteOnExit()
secureHashedFile.deleteOnExit()
}
}
} else {
assertion = new Assertion() {
void checkAssertion() {
assert outputPropertiesFile.exists()
final List<String> updatedPropertiesLines = outputPropertiesFile.readLines()
logger.info("Updated nifi.properties:")
logger.info("\n" * 2 + updatedPropertiesLines.join("\n"))
// Check that the output values for sensitive properties are not the same as the original (i.e. it was re-encrypted)
NiFiProperties updatedProperties = new NiFiPropertiesLoader().readProtectedPropertiesFromDisk(outputPropertiesFile)
assert updatedProperties.size() >= inputProperties.size()
originalSensitiveValues.every { String key, String originalValue ->
assert updatedProperties.getProperty(key) != originalValue
}
// Check that the new NiFiProperties instance matches the output file (values still encrypted)
updatedProperties.getPropertyKeys().every { String key ->
assert updatedPropertiesLines.contains("${key}=${updatedProperties.getProperty(key)}".toString())
}
// Check that the key was persisted to the bootstrap.conf
final List<String> updatedBootstrapLines = bootstrapFile.readLines()
String updatedKeyLine = updatedBootstrapLines.find {
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
}
logger.info("Updated key line: ${updatedKeyLine}")
assert updatedKeyLine == EXPECTED_NEW_KEY_LINE
assert originalBootstrapLines.size() == updatedBootstrapLines.size()
// Check that the secure hash was persisted to the secure_hash.key
final List<String> updatedSecureHashLines = secureHashedFile.readLines()
String updatedSecureHashKeyLine = updatedSecureHashLines.find { it.startsWith("secureHashKey=") }
logger.info("Updated secure hash lines: \n${updatedSecureHashLines.join("\n")}")
int expectedSecureHashLineCount = 1
// Extract the salt(s) so the credentials can be hashed with the same salt
final String keySalt = updatedSecureHashKeyLine.find(SCRYPT_SALT_PATTERN)
logger.info("Extracted key salt: ${keySalt}")
final String EXPECTED_NEW_SECURE_HASH_KEY_LINE = "secureHashKey=${ConfigEncryptionTool.secureHashKey(newKeyHex, keySalt)}"
// Only evaluate the secure hash password line if the raw password was provided (otherwise, can't store hash)
if (newPassword) {
String updatedSecureHashPasswordLine = updatedSecureHashLines.find {
it.startsWith("secureHashPassword=")
}
final String passwordSalt = updatedSecureHashPasswordLine.find(SCRYPT_SALT_PATTERN)
logger.info("Extracted password salt: ${passwordSalt}")
final String EXPECTED_NEW_SECURE_HASH_PASSWORD_LINE = "secureHashPassword=${ConfigEncryptionTool.secureHashPassword(newPassword, passwordSalt)}"
expectedSecureHashLineCount = 2
logger.info("Asserting \n${updatedSecureHashPasswordLine} == \n${EXPECTED_NEW_SECURE_HASH_PASSWORD_LINE}")
assert updatedSecureHashPasswordLine == EXPECTED_NEW_SECURE_HASH_PASSWORD_LINE
}
logger.info("Asserting \n${updatedSecureHashKeyLine} == \n${EXPECTED_NEW_SECURE_HASH_KEY_LINE}")
assert updatedSecureHashKeyLine == EXPECTED_NEW_SECURE_HASH_KEY_LINE
assert updatedSecureHashLines.size() == expectedSecureHashLineCount
// Clean up
outputPropertiesFile.deleteOnExit()
bootstrapFile.deleteOnExit()
tmpDir.deleteOnExit()
secureHashedFile.deleteOnExit()
}
}
}
exit.checkAssertionAfterwards(assertion)
logger.info("Migrating key (${scenario}) with ${localArgs.join(" ")}")
// Override the "final" secure hash file path
ConfigEncryptionTool.secureHashPath = secureHashedFile.path
ConfigEncryptionTool.main(localArgs as String[])
// Assert
// Assertions defined above
}
/**
* Ideally all of the combination tests would be a single test with iterative argument lists, but due to the System.exit(), it can only be captured once per test.
*/
// @Ignore
// TODO re-enable once this is passing on all platforms
@Test
void testShouldMigrateFromHashedPasswordToPassword() {
// Arrange
String scenario = "hashed password to password"
def args = ["-z", HASHED_PASSWORD, "-p", PASSWORD.reverse()]
// Act
performSecureHashKeyMigration(scenario, args, HASHED_PASSWORD, PASSWORD.reverse())
// Assert
// Assertions in common method above
}
// @Ignore
// TODO re-enable once this is passing on all platforms
@Test
void testShouldMigrateFromHashedPasswordToKey() {
// Arrange
String scenario = "hashed password to key"
def args = ["-z", HASHED_PASSWORD, "-k", KEY_HEX]
// Act
performSecureHashKeyMigration(scenario, args, HASHED_PASSWORD, "", "", KEY_HEX)
// Assert
// Assertions in common method above
}
// @Ignore
// TODO re-enable once this is passing on all platforms
@Test
void testShouldMigrateFromHashedKeyToPassword() {
// Arrange
String scenario = "hashed key to password"
def args = ["-y", HASHED_KEY_HEX, "-p", PASSWORD.reverse()]
// Act
performSecureHashKeyMigration(scenario, args, "", PASSWORD.reverse(), HASHED_KEY_HEX, "")
// Assert
// Assertions in common method above
}
// @Ignore
// TODO re-enable once this is passing on all platforms
@Test
void testShouldMigrateFromHashedKeyToKey() {
// Arrange
String scenario = "hashed key to key"
def args = ["-y", HASHED_KEY_HEX, "-k", KEY_HEX]
// Act
performSecureHashKeyMigration(scenario, args, "", "", HASHED_KEY_HEX, KEY_HEX)
// Assert
// Assertions in common method above
}
// @Ignore
// TODO re-enable once this is passing on all platforms
@Test
void testShouldFailToMigrateFromIncorrectHashedPasswordToPassword() {
// Arrange
String scenario = "(incorrect) hashed password to password"
final String INCORRECT_HASHED_PASSWORD = "\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$thisIsDefinitelyNotTheCorrectPasswordHashxx"
def args = ["-z", INCORRECT_HASHED_PASSWORD, "-p", PASSWORD.reverse()]
// Act
performSecureHashKeyMigration(scenario, args, HASHED_PASSWORD, PASSWORD.reverse(), "", "", 4)
// Assert
// Assertions in common method above
}
@Test
void testShouldDeriveSecureHashOfPassword() {
// Arrange
def testPasswords = ["password", "thisIsABadPassword", "bWZerzZo6fw9ZrDz*YfM6CVj2Ktx(YJd"]
// All zero, 22 (16B) Base64 static, 40 (32B) Base64 randomly-generated
def salts = [
Hex.decode("00" * 16),
Base64.decoder.decode("ABCDEFGHIJKLMNOPQRSTUV"),
Base64.decoder.decode("eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc=")
]
// These values were generated using CET#secureHashPassword() and verified using src/test/resources/scrypt.py
def passwordHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$99aTTB39TJo69aZCONQmRdyWOgYsDi+1MI+8D0EgMNM"
]
def badPasswordHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$Gk7K9YmlsWbd8FS7e4RKVWnkg9vlsqYnlD593pJ71gg",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$Ri78VZbrp2cCVmGh2a9Nbfdov8LPnFb49MYyzPCaXmE",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$rZIrP2qdIY7LN4CZAMgbCzl3YhXz6WhaNyXJXqFIjaI"
]
def randomPasswordHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$GxH68bGykmPDZ6gaPIGOONOT2omlZ7cd0xlcZ9UsY/0",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$KLGZjWlo59sbCbtmTg5b4k0Nu+biWZRRzhPhN7K5kkI",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$6Ql6Efd2ac44ERoV31CL3Q0J3LffNZKN4elyMHux99Y"
]
def expectedHashes = [
(testPasswords[0]): passwordHashes,
(testPasswords[1]): badPasswordHashes,
(testPasswords[2]): randomPasswordHashes
]
// Low cost factors for performance
int n = 2**4
int r = 8
int p = 1
logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
// Act
testPasswords.each { String password ->
salts.eachWithIndex { byte[] rawSalt, int i ->
logger.info("Hashing '${password}' with salt ${Base64.encoder.encodeToString(rawSalt)}")
String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
logger.info("Formatted salt: ${formattedSalt}")
String generatedHash = ConfigEncryptionTool.secureHashPassword(password, formattedSalt)
logger.info("Generated hash: ${generatedHash}")
// Assert
String expectedHash = expectedHashes[(password)][i]
logger.info("Comparing to expectedHashes['${password}'][${i}]: ${expectedHash}")
// Remember to perform constant-time equality check in production code
assert generatedHash == expectedHash
}
}
}
@Test
void testShouldDeriveSecureHashOfKey() {
// Arrange
def testKeys = [
"00" * 32,
"0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210",
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"
]
// All zero, 22 (16B) Base64 static, 40 (32B) Base64 randomly-generated
def salts = [
Hex.decode("00" * 16),
Base64.decoder.decode("ABCDEFGHIJKLMNOPQRSTUV"),
Base64.decoder.decode("eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc=")
]
// These values were generated using CET#secureHashKey() and verified using src/test/resources/scrypt.py
def zeroHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$pOoIk4K9OPYxusXBFNGtEaoHzIIxlgDOTiVO9OiLJrE",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$kQJ7CeAt5qHK4/r2lMnuBzNyBt1h1WDDkmgXH7N0hRc",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$diExTMVvETmC6gjKx+9ITn1L/0FOYNHeQq2oPLMsFvY"
]
def uppercaseHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$K5uQBtbkmq2b2M1H6kX/U7g5QiPgmoLCuJYfpOar8w4",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$TbPrKP7+/xPlc74L15QFG+iDqIysPW/dOFVRaj4Rk/k",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$yGpGz7FyBE3nf8Ed/o84o8Glyd4m091HxdVQEhN55zI"
]
// Should be identical to uppercase hashes due to case-normalization in method
def lowercaseHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$K5uQBtbkmq2b2M1H6kX/U7g5QiPgmoLCuJYfpOar8w4",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$TbPrKP7+/xPlc74L15QFG+iDqIysPW/dOFVRaj4Rk/k",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$yGpGz7FyBE3nf8Ed/o84o8Glyd4m091HxdVQEhN55zI"
]
def expectedHashes = [
(testKeys[0]): zeroHashes,
(testKeys[1]): uppercaseHashes,
(testKeys[2]): lowercaseHashes
]
// Low cost factors for performance
int n = 2**4
int r = 8
int p = 1
logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
// Act
testKeys.each { String key ->
salts.eachWithIndex { byte[] rawSalt, int i ->
logger.info("Hashing '${key}' with salt ${Base64.encoder.encodeToString(rawSalt)}")
String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
logger.info("Formatted salt: ${formattedSalt}")
String generatedHash = ConfigEncryptionTool.secureHashKey(key, formattedSalt)
logger.info("Generated hash: ${generatedHash}")
// Assert
String expectedHash = expectedHashes[(key)][i]
logger.info("Comparing to expectedHashes['${key}'][${i}]: ${expectedHash}")
// Remember to perform constant-time equality check in production code
assert generatedHash == expectedHash
}
}
}
@Test
void testShouldVerifySecureHashOfPassword() {
// Arrange
String password = "password"
// This is a known existing hash of "password"
String existingHashedPassword = "\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM"
logger.info("Known existing hash: ${existingHashedPassword}")
// Low cost factors for performance
int n = 2**4
int r = 8
int p = 1
logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
byte[] rawSalt = Hex.decode("00" * 16)
// This is a generated hash of "password"
String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
logger.info("Formatted salt: ${formattedSalt}")
String hashedPassword = ConfigEncryptionTool.secureHashPassword(password, formattedSalt)
logger.info("Generated hash: ${hashedPassword}")
// This is a generated hash of "password" using a different salt
byte[] otherRawSalt = Hex.decode("01" * 16)
String otherFormattedSalt = Scrypt.formatSalt(otherRawSalt, n, r, p)
logger.info("Formatted salt: ${otherFormattedSalt}")
String otherHashedPassword = ConfigEncryptionTool.secureHashPassword(password, otherFormattedSalt)
logger.info("Generated hash: ${otherHashedPassword}")
// Act
logger.info("Checking \n${existingHashedPassword} against hash(${password}, ${formattedSalt}) -> \n${hashedPassword}")
boolean hashIsIdentical = ConfigEncryptionTool.checkHashedValue(existingHashedPassword, hashedPassword)
logger.info("Hash values equal: ${hashIsIdentical}")
logger.info("Checking \n${existingHashedPassword} against hash(${password}, ${otherFormattedSalt}) -> \n${otherHashedPassword}")
boolean otherHashIsIdentical = ConfigEncryptionTool.checkHashedValue(existingHashedPassword, otherHashedPassword)
logger.info("Hash values equal: ${otherHashIsIdentical}")
// Assert
assert hashIsIdentical
assert !otherHashIsIdentical
}
@Test
void testCheckHashedValueShouldVerifyScryptFormat() {
// Arrange
def validHashes = [
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$99aTTB39TJo69aZCONQmRdyWOgYsDi+1MI+8D0EgMNM",
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$Gk7K9YmlsWbd8FS7e4RKVWnkg9vlsqYnlD593pJ71gg",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$Ri78VZbrp2cCVmGh2a9Nbfdov8LPnFb49MYyzPCaXmE",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$rZIrP2qdIY7LN4CZAMgbCzl3YhXz6WhaNyXJXqFIjaI",
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$GxH68bGykmPDZ6gaPIGOONOT2omlZ7cd0xlcZ9UsY/0",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$KLGZjWlo59sbCbtmTg5b4k0Nu+biWZRRzhPhN7K5kkI",
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$6Ql6Efd2ac44ERoV31CL3Q0J3LffNZKN4elyMHux99Y"
]
// Some of these are valid "scrypt" hashes but do not conform to the additional requirements NiFi imposes
def invalidHashes = [
"\$s1\$40801\$AAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM",
"\$s0\$\$ABCDEFGHIJKLMNOPQRSTUQ\$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8",
"\$s0\$40801\$\$99aTTB39TJo69aZCONQmRdyWOgYsDi+1MI+8D0EgMNM",
"\$s0\$40801\$!!!!\$Gk7K9YmlsWbd8FS7e4RKVWnkg9vlsqYnlD593pJ71gg",
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$xxxx",
]
// Low cost factors for performance
int n = 2**4
int r = 8
int p = 1
logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
// Act
logger.info("Checking ${validHashes.size()} valid hashes")
validHashes.each { String hash ->
logger.info("Verifying hash format: ${hash}")
boolean validFormat = ConfigEncryptionTool.verifyHashFormat(hash)
logger.info("Valid format: ${validFormat}")
// Assert
assert validFormat
}
logger.info("Checking ${invalidHashes.size()} invalid hashes")
invalidHashes.each { String invalidHash ->
logger.info("Verifying hash format: ${invalidHash}")
boolean validFormat = ConfigEncryptionTool.verifyHashFormat(invalidHash)
logger.info("Valid format: ${validFormat}")
// Assert
assert !validFormat
}
}
@Test
void testGetMigrationKeyShouldVerifySecureHashOfPassword() {
// Arrange
File bootstrapWithKeyFile = new File("src/test/resources/bootstrap_with_master_key_password.conf")
File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
bootstrapFile.delete()
Files.copy(bootstrapWithKeyFile.toPath(), bootstrapFile.toPath())
String expectedMigrationKey = bootstrapFile.readLines().find {
it.startsWith("nifi.bootstrap.sensitive.key=")
}.split("=").last()
logger.info("Retrieved expected migration key ${expectedMigrationKey} from bootstrap.conf")
File secureHashSourceFile = new File("src/test/resources/secure_hash.key")
File secureHashFile = new File("target/tmp/secure_hash.key")
secureHashFile.delete()
Files.copy(secureHashSourceFile.toPath(), secureHashFile.toPath())
// The second line in the file is for the password
String expectedHash = secureHashFile.readLines().last().split("=").last()
logger.info("Retrieved expected hash ${expectedHash} from secure_hash.key")
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.usingSecureHash = true
tool.secureHashPath = secureHashFile.path
tool.bootstrapConfPath = bootstrapFile.path
String correctHash = expectedHash
String incorrectHash = correctHash[0..-10] + ("x" * 9)
// Act
tool.secureHashPassword = correctHash
logger.info("Trying to retrieve migration key comparing: \n" +
"Command-line provided hash: ${correctHash}\n" +
" Hash from secure_hash.key: ${expectedHash}")
String correctRetrievedMigrationKey = tool.getMigrationKey()
logger.info(" [Correct] Retrieved migration key: ${correctRetrievedMigrationKey}")
tool.secureHashPassword = incorrectHash
logger.info("Trying to retrieve migration key comparing: \n" +
"Command-line provided hash: ${incorrectHash}\n" +
" Hash from secure_hash.key: ${expectedHash}")
def msg = shouldFail() {
String incorrectRetrievedMigrationKey = tool.getMigrationKey()
logger.info("[Incorrect] Retrieved migration key: ${incorrectRetrievedMigrationKey}")
}
logger.expected(msg)
// Assert
assert correctRetrievedMigrationKey == expectedMigrationKey
assert msg =~ "The provided hashed key/password is not correct"
}
@Test
void testGetMigrationKeyShouldVerifySecureHashOfKey() {
// Arrange
File bootstrapWithKeyFile = new File("src/test/resources/bootstrap_with_master_key_password.conf")
File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
bootstrapFile.delete()
Files.copy(bootstrapWithKeyFile.toPath(), bootstrapFile.toPath())
String expectedMigrationKey = bootstrapFile.readLines().find {
it.startsWith("nifi.bootstrap.sensitive.key=")
}.split("=").last()
logger.info("Retrieved expected migration key ${expectedMigrationKey} from bootstrap.conf")
File secureHashSourceFile = new File("src/test/resources/secure_hash.key")
File secureHashFile = new File("target/tmp/secure_hash.key")
secureHashFile.delete()
Files.copy(secureHashSourceFile.toPath(), secureHashFile.toPath())
// The first line in the file is for the key
String expectedHash = secureHashFile.readLines().first().split("=").last()
logger.info("Retrieved expected hash ${expectedHash} from secure_hash.key")
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.usingSecureHash = true
tool.secureHashPath = secureHashFile.path
tool.bootstrapConfPath = bootstrapFile.path
String correctHash = expectedHash
String incorrectHash = correctHash[0..-10] + ("x" * 9)
// Act
tool.secureHashKey = correctHash
logger.info("Trying to retrieve migration key comparing: \n" +
"Command-line provided hash: ${correctHash}\n" +
" Hash from secure_hash.key: ${expectedHash}")
String correctRetrievedMigrationKey = tool.getMigrationKey()
logger.info(" [Correct] Retrieved migration key: ${correctRetrievedMigrationKey}")
tool.secureHashKey = incorrectHash
logger.info("Trying to retrieve migration key comparing: \n" +
"Command-line provided hash: ${incorrectHash}\n" +
" Hash from secure_hash.key: ${expectedHash}")
def msg = shouldFail() {
String incorrectRetrievedMigrationKey = tool.getMigrationKey()
logger.info("[Incorrect] Retrieved migration key: ${incorrectRetrievedMigrationKey}")
}
logger.expected(msg)
// Assert
assert correctRetrievedMigrationKey == expectedMigrationKey
assert msg =~ "The provided hashed key/password is not correct"
}
@Test
void testShouldDecryptLoginIdentityProviders() {
// Arrange
@ -5165,13 +4458,13 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
void testShouldDetectActionFlags() {
// Arrange
final def HELP_AND_VERBOSE_ARGS = [["-h", "--help"], ["-v", "--verbose"]]
final List<String> IGNORED_ARGS = ["currentHashParams"]
final List<String> IGNORED_ARGS = ["translateCli"]
// Create a list with combinations of h[elp] and v[erbose], individual flags, and empty flag
def args = GroovyCollections.combinations(HELP_AND_VERBOSE_ARGS as Iterable) + HELP_AND_VERBOSE_ARGS.flatten().collect {
[it]
} + [[""]]
String acceptableArg = "--currentHashParams"
String acceptableArg = "--translateCli"
String unacceptableArg = "--migrate"
ConfigEncryptionTool tool = new ConfigEncryptionTool()
@ -5210,151 +4503,6 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
}
}
@Ignore
// TODO re-enable once this is passing on all platforms
@Test
void testShouldReturnCurrentHashParams() {
// Arrange
// Params from secure_hash.key
int N = 2**4
int r = 8
int p = 1
String base64Salt = "A" * 22
String expectedJsonParams = new JsonBuilder([N: N, r: r, p: p, salt: base64Salt]).toString()
logger.info("Expected JSON params: ${expectedJsonParams}")
// Set up assertions for after System.exit()
exit.expectSystemExitWithStatus(0)
// Initial set up
File tmpDir = new File("target/tmp/")
tmpDir.mkdirs()
setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
// Copy the hashed credentials file
String secureHashedPasswordPath = isUnlimitedStrengthCryptoAvailable() ? "src/test/resources/secure_hash.key" :
"src/test/resources/secure_hash_128.key"
File originalSecureHashedPasswordFile = new File(secureHashedPasswordPath)
File secureHashedFile = new File("target/tmp/tmp_secure_hash.key")
secureHashedFile.delete()
Files.copy(originalSecureHashedPasswordFile.toPath(), secureHashedFile.toPath())
exit.checkAssertionAfterwards(new Assertion() {
void checkAssertion() {
// If JSON ordering changes, may need to capture and build JSON object from this text
assert systemOutRule.getLog().contains(expectedJsonParams)
// Clean up
tmpDir.deleteOnExit()
secureHashedFile.deleteOnExit()
}
})
// Override the "final" secure hash file path
ConfigEncryptionTool.secureHashPath = secureHashedFile.path
// Act
ConfigEncryptionTool.main(["--currentHashParams"] as String[])
// Assert
// Assertions defined above
}
@Test
void testShouldReturnDefaultHashParamsIfNonePresent() {
// Arrange
// Default params
int N = ConfigEncryptionTool.SCRYPT_N
int r = ConfigEncryptionTool.SCRYPT_R
int p = ConfigEncryptionTool.SCRYPT_P
String expectedJsonParams = new JsonBuilder([N: N, r: r, p: p, salt: "<some 22 char B64 str>"]).toString()
logger.info("Expected JSON params: ${expectedJsonParams}")
// Set up assertions for after System.exit()
exit.expectSystemExitWithStatus(0)
// Initial set up
File tmpDir = new File("target/tmp/")
tmpDir.mkdirs()
setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
// Ensure the file is not present
File secureHashedFile = new File("target/tmp/tmp_secure_hash.key")
secureHashedFile.delete()
exit.checkAssertionAfterwards(new Assertion() {
void checkAssertion() {
// If JSON ordering changes, may need to capture and build JSON object from this text
List<String> returnedJSONParams = systemOutRule.getLog().readLines()
logger.returned("Returned JSON params: ${returnedJSONParams.join("\n")}")
JsonSlurper slurper = new JsonSlurper()
def expectedJson = slurper.parseText(expectedJsonParams)
def returnedJson = slurper.parseText(returnedJSONParams.first())
assert returnedJson.N == expectedJson.N
assert returnedJson.r == expectedJson.r
assert returnedJson.p == expectedJson.p
assert returnedJson.salt =~ /[\w\/+=]{22}/
// Clean up
tmpDir.deleteOnExit()
}
})
// Override the "final" secure hash file path
ConfigEncryptionTool.secureHashPath = secureHashedFile.path
// Act
ConfigEncryptionTool.main(["--currentHashParams"] as String[])
// Assert
// Assertions defined above
}
@Test
void testShouldFailOnCurrentHashParamsIfOtherFlagsPresent() {
// Arrange
ConfigEncryptionTool tool = new ConfigEncryptionTool()
def validOpts = [
"",
"-v",
"--verbose"
]
def invalidOpts = [
"--migrate",
"-f flow.xml.gz",
"-n nifi.properties",
"-o output"
]
// Act
validOpts.each { String valid ->
def args = (valid + " --currentHashParams").split(" ")
logger.info("Testing with ${args}")
tool.parse(args as String[])
}
invalidOpts.each { String invalid ->
def args = (invalid + " --currentHashParams").split(" ")
logger.info("Testing with ${args}")
def msg = shouldFail(CommandLineParseException) {
tool.parse(args as String[])
}
// Assert
assert msg == "When '--currentHashParams' is specified, only '-h'/'--help' and '-v'/'--verbose' are allowed"
assert systemOutRule.getLog().contains("usage: org.apache.nifi.properties.ConfigEncryptionTool [")
}
}
@Test
void testShouldTranslateCliWithPlaintextInput() {
// Arrange