NIFI-7638 Implemented custom nifi.sensitive.props.algorithm for AES-G/CM with Argon2 KDF.

Added documentation for encryption of flow sensitive values.
Added unit tests.

This closes #4427.
This commit is contained in:
Andy LoPresto 2020-07-17 15:33:47 -07:00
parent 5cb8d24689
commit 7d20c03f89
No known key found for this signature in database
GPG Key ID: 6EC293152D90B61D
4 changed files with 239 additions and 73 deletions

View File

@ -46,7 +46,7 @@ public abstract class RandomIVPBECipherProvider implements PBECipherProvider {
* @return the initialized cipher
* @throws Exception if there is a problem initializing the cipher
*/
abstract Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception;
public abstract Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception;
abstract Logger getLogger();

View File

@ -1580,7 +1580,19 @@ If on a system where the unlimited strength policies cannot be installed, it is
If it is not possible to install the unlimited strength jurisdiction policies, the `Allow Weak Crypto` setting can be changed to `allowed`, but *this is _not_ recommended*. Changing this setting explicitly acknowledges the inherent risk in using weak cryptographic configurations.
=====================
It is preferable to request upstream/downstream systems to switch to link:https://cwiki.apache.org/confluence/display/NIFI/Encryption+Information[keyed encryption^] or use a "strong" link:https://cwiki.apache.org/confluence/display/NIFI/Key+Derivation+Function+Explanations[Key Derivation Function (KDF) supported by NiFi^].
It is preferable to request upstream/downstream systems to switch to link:https://cwiki.apache.org/confluence/display/NIFI/Encryption+Information[keyed encryption^] or use a "strong" <<key-derivation-functions, Key Derivation Function (KDF) supported by NiFi>>.
[[nifi_sensitive_props_key]]
== Encrypted Passwords in Flow Definitions
NiFi always stores all sensitive values (passwords, tokens, and other credentials) populated into a flow in an encrypted format on disk. The encryption algorithm used is specified by `nifi.sensitive.props.algorithm` and the password from which the encryption key is derived is specified by `nifi.sensitive.props.key` in _nifi.properties_ (see <<security_configuration,Security Configuration>> for additional information). Prior to version 1.12.0, the list of available algorithms was all password-based encryption (PBE) algorithms supported by the `EncryptionMethod` enum in that version. Unfortunately many of these algorithms are provided for legacy compatibility, and use weak key derivation functions and block cipher algorithms & modes of operation. In 1.12.0, a pair of custom algorithms was introduced for security-conscious users looking for more robust protection of the flow sensitive values. These options combine the <<argon2-kdf, Argon2id>> KDF with reasonable cost parameters (2^16^ or `65,536 KB` of memory, `5` iterations, and parallelism `8`) with an authenticated encryption with associated data (AEAD) mode of operation, `AES-G/CM` (Galois Counter Mode). The algorithms are specified as:
* `NIFI_ARGON2_AES_GCM_256` -- 256-bit key length
* `NIFI_ARGON2_AES_GCM_128` -- 128-bit key length
Both options require a password (`nifi.sensitive.props.key` value) of *at least 12 characters*. This means the "default" value (if left empty, a hard-coded default is used) will not be sufficient.
These options provide a bridge solution to higher security without requiring a change to the structure of _nifi.properties_. Due to the implementation of flow synchronization, on every change to the flow definition, all sensitive properties are re-encrypted during flow serialization, and each encryption operation requires the derivation of the key. _As Argon2 is intentionally time-hard, this will introduce an approximately 1 second cost per sensitive value per flow modification._ This is determined to be an acceptable tradeoff for security at this time but will be remedied with an internal key caching mechanism in a future release. In addition, a more full-featured configuration process, allowing for arbitrary combinations of KDFs and encryption algorithms, will be added in a future release. See link:https://issues.apache.org/jira/browse/NIFI-7638[NIFI-7638^], link:https://issues.apache.org/jira/browse/NIFI-7668[NIFI-7668^], link:https://issues.apache.org/jira/browse/NIFI-7669[NIFI-7669^], and link:https://issues.apache.org/jira/browse/NIFI-7670[NIFI-7670^] for more details.
[[encrypt-config_tool]]
== Encrypted Passwords in Configuration Files

View File

@ -38,6 +38,7 @@ import org.apache.nifi.security.util.crypto.CipherProviderFactory;
import org.apache.nifi.security.util.crypto.CipherUtility;
import org.apache.nifi.security.util.crypto.KeyedCipherProvider;
import org.apache.nifi.security.util.crypto.PBECipherProvider;
import org.apache.nifi.security.util.crypto.RandomIVPBECipherProvider;
import org.apache.nifi.util.NiFiProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
@ -76,18 +77,30 @@ public class StringEncryptor {
private static final List<String> SUPPORTED_ALGORITHMS = new ArrayList<>();
private static final List<String> SUPPORTED_PROVIDERS = new ArrayList<>();
private static final String ARGON2_AES_GCM_256_ALGORITHM = "NIFI_ARGON2_AES_GCM_256";
private static final String ARGON2_AES_GCM_128_ALGORITHM = "NIFI_ARGON2_AES_GCM_128";
private static final List<String> CUSTOM_ALGORITHMS = Arrays.asList(ARGON2_AES_GCM_128_ALGORITHM, ARGON2_AES_GCM_256_ALGORITHM);
// Length of Argon2 encoded cost parameters + 22 B64 raw salt
public static final int CUSTOM_ALGORITHM_SALT_LENGTH = 53;
private static final int IV_LENGTH = 16;
private final String algorithm;
private final String provider;
private final PBEKeySpec password;
private final SecretKeySpec key;
private String encoding = "HEX";
private static final String HEX_ENCODING = "HEX";
private static final String B64_ENCODING = "BASE64";
private String encoding = HEX_ENCODING;
private CipherProvider cipherProvider;
static {
Security.addProvider(new BouncyCastleProvider());
SUPPORTED_ALGORITHMS.addAll(CUSTOM_ALGORITHMS);
for (EncryptionMethod em : EncryptionMethod.values()) {
SUPPORTED_ALGORITHMS.add(em.getAlgorithm());
}
@ -110,8 +123,8 @@ public class StringEncryptor {
* <p>
* For actual raw key provision, see {@link #StringEncryptor(String, String, byte[])}.
*
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#getAlgorithm()})
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
* @param key the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
*/
public StringEncryptor(final String algorithm, final String provider, final String key) {
@ -128,8 +141,8 @@ public class StringEncryptor {
* This constructor creates an encryptor using <em>Keyed Encryption</em>. The <em>key</em> value is the raw byte value of a symmetric encryption key
* (usually expressed for human-readability/transmission in hexadecimal or Base64 encoded format).
*
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#getAlgorithm()})
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
* @param key a raw encryption key in bytes
*/
public StringEncryptor(final String algorithm, final String provider, final byte[] key) {
@ -153,7 +166,7 @@ public class StringEncryptor {
/**
* Extracts the cipher "family" (i.e. "AES", "DES", "RC4") from the full algorithm name.
*
* @param algorithm the algorithm ({@link EncryptionMethod#algorithm})
* @param algorithm the algorithm ({@link EncryptionMethod#getAlgorithm()})
* @return the cipher family
* @throws EncryptionException if the algorithm is null/empty or not supported
*/
@ -199,8 +212,8 @@ public class StringEncryptor {
/**
* Creates an instance of the NiFi sensitive property encryptor. If the password is blank, the default will be used and an error will be printed to the log.
*
* @param algorithm the encryption (and key derivation) algorithm ({@link EncryptionMethod#algorithm})
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
* @param algorithm the encryption (and key derivation) algorithm ({@link EncryptionMethod#getAlgorithm()})
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
* @param password the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
* @return the initialized encryptor
*/
@ -245,7 +258,10 @@ public class StringEncryptor {
}
if (paramsAreValid()) {
if (CipherUtility.isPBECipher(algorithm)) {
if (isCustomAlgorithm(algorithm)) {
// Handle the initialization for Argon2 + AES
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.ARGON2);
} else if (CipherUtility.isPBECipher(algorithm)) {
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NIFI_LEGACY);
} else {
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
@ -255,10 +271,27 @@ public class StringEncryptor {
}
}
/**
* Returns {@code true} if the provided algorithm is considered a "custom" algorithm (a combination of KDF
* and cipher not present in {@link EncryptionMethod} and implemented specially for string encryption). Case-insensitive.
*
* @param algorithm the algorithm to evaluate
* @return true if present in {@link #CUSTOM_ALGORITHMS}
*/
public static boolean isCustomAlgorithm(String algorithm) {
return CUSTOM_ALGORITHMS.contains(algorithm.toUpperCase());
}
private boolean paramsAreValid() {
boolean algorithmAndProviderValid = algorithmIsValid(algorithm) && providerIsValid(provider);
boolean secretIsValid = false;
if (CipherUtility.isPBECipher(algorithm)) {
if (isCustomAlgorithm(algorithm)) {
// If this isn't valid, throw an exception directly to indicate the problem (minimum password length)
secretIsValid = customSecretIsValid(password, key, algorithm);
if (!secretIsValid) {
throw new EncryptionException("The nifi.sensitive.props.key password provided is invalid for algorithm " + algorithm + "; must be >= 12 characters");
}
} else if (CipherUtility.isPBECipher(algorithm)) {
secretIsValid = passwordIsValid(password);
} else if (CipherUtility.isKeyedCipher(algorithm)) {
secretIsValid = keyIsValid(key, algorithm);
@ -267,6 +300,13 @@ public class StringEncryptor {
return algorithmAndProviderValid && secretIsValid;
}
private boolean customSecretIsValid(PBEKeySpec password, SecretKeySpec key, String algorithm) {
// Currently, the only custom algorithms use AES-G/CM with a password via Argon2
String rawPassword = new String(password.getPassword());
final boolean secretIsValid = StringUtils.isNotBlank(rawPassword) && rawPassword.trim().length() >= 12;
return secretIsValid;
}
private boolean keyIsValid(SecretKeySpec key, String algorithm) {
return key != null && CipherUtility.getValidKeyLengthsForAlgorithm(algorithm).contains(key.getEncoded().length * 8);
}
@ -280,10 +320,10 @@ public class StringEncryptor {
}
public void setEncoding(String base) {
if ("HEX".equalsIgnoreCase(base)) {
this.encoding = "HEX";
} else if ("BASE64".equalsIgnoreCase(base)) {
this.encoding = "BASE64";
if (HEX_ENCODING.equalsIgnoreCase(base)) {
this.encoding = HEX_ENCODING;
} else if (B64_ENCODING.equalsIgnoreCase(base)) {
this.encoding = B64_ENCODING;
} else {
throw new IllegalArgumentException("The encoding base must be 'HEX' or 'BASE64'");
}
@ -300,7 +340,8 @@ public class StringEncryptor {
try {
if (isInitialized()) {
byte[] rawBytes;
if (CipherUtility.isPBECipher(algorithm)) {
// Currently all custom algorithms are PBE (Argon2)
if (CipherUtility.isPBECipher(algorithm) || isCustomAlgorithm(algorithm)) {
rawBytes = encryptPBE(clearText);
} else {
rawBytes = encryptKeyed(clearText);
@ -316,7 +357,7 @@ public class StringEncryptor {
private byte[] encryptPBE(String plaintext) {
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
final EncryptionMethod encryptionMethod = getEncryptionMethodForAlgorithm(algorithm);
// Generate salt
byte[] salt;
@ -332,25 +373,38 @@ public class StringEncryptor {
// Generate cipher
try {
Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, true);
byte[] ivBytes = new byte[0];
Cipher cipher;
// Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
// byte[] iv = new byte[0];
// if (cipherProvider instanceof RandomIVPBECipherProvider) {
// iv = cipher.getIV();
// }
// Generate IV if necessary (allows for future use of Argon2, PBKDF2, Bcrypt, or Scrypt)
if (cipherProvider instanceof RandomIVPBECipherProvider) {
// Generating the IV here rather than delegating to the cipher provider suppresses the warning messages
ivBytes = new byte[IV_LENGTH];
new SecureRandom().nextBytes(ivBytes);
cipher = ((RandomIVPBECipherProvider) pbecp).getCipher(encryptionMethod, new String(password.getPassword()), salt, ivBytes, keyLength, true);
} else {
cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, true);
}
// Encrypt the plaintext
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine the output
// byte[] rawBytes = CryptoUtils.concatByteArrays(salt, iv, cipherBytes);
return CryptoUtils.concatByteArrays(salt, cipherBytes);
return CryptoUtils.concatByteArrays(salt, ivBytes, cipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not encrypt sensitive value", e);
}
}
private EncryptionMethod getEncryptionMethodForAlgorithm(String algorithm) {
if (isCustomAlgorithm(algorithm)) {
// We may add more implementations later, but currently all custom algorithms are AES-G/CM
return EncryptionMethod.AES_GCM;
} else {
return EncryptionMethod.forAlgorithm(algorithm);
}
}
private byte[] encryptKeyed(String plaintext) {
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
@ -360,7 +414,7 @@ public class StringEncryptor {
byte[] iv = new byte[16];
sr.nextBytes(iv);
Cipher cipher = keyedcp.getCipher(EncryptionMethod.forAlgorithm(algorithm), key, iv, true);
Cipher cipher = keyedcp.getCipher(getEncryptionMethodForAlgorithm(algorithm), key, iv, true);
// Encrypt the plaintext
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
@ -373,7 +427,7 @@ public class StringEncryptor {
}
private String encode(byte[] rawBytes) {
if (this.encoding.equalsIgnoreCase("HEX")) {
if (this.encoding.equalsIgnoreCase(HEX_ENCODING)) {
return Hex.encodeHexString(rawBytes);
} else {
return Base64.toBase64String(rawBytes);
@ -392,7 +446,8 @@ public class StringEncryptor {
if (isInitialized()) {
byte[] plainBytes;
byte[] cipherBytes = decode(cipherText);
if (CipherUtility.isPBECipher(algorithm)) {
// Currently all custom algorithms are PBE (Argon2)
if (CipherUtility.isPBECipher(algorithm) || isCustomAlgorithm(algorithm)) {
plainBytes = decryptPBE(cipherBytes);
} else {
plainBytes = decryptKeyed(cipherBytes);
@ -408,27 +463,34 @@ public class StringEncryptor {
private byte[] decryptPBE(byte[] cipherBytes) {
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
final EncryptionMethod encryptionMethod = getEncryptionMethodForAlgorithm(algorithm);
// Extract salt
int saltLength = CipherUtility.getSaltLengthForAlgorithm(algorithm);
int saltLength = determineSaltLength(algorithm);
byte[] salt = new byte[saltLength];
System.arraycopy(cipherBytes, 0, salt, 0, saltLength);
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, saltLength, cipherBytes.length);
// Read IV if necessary (allows for future use of Argon2, PBKDF2, Bcrypt, or Scrypt)
byte[] ivBytes = new byte[0];
int cipherBytesStart = saltLength;
if (pbecp instanceof RandomIVPBECipherProvider) {
ivBytes = new byte[16];
System.arraycopy(cipherBytes, saltLength, ivBytes, 0, ivBytes.length);
cipherBytesStart = saltLength + ivBytes.length;
}
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, cipherBytesStart, cipherBytes.length);
// Determine necessary key length
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
// Generate cipher
try {
Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
// Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
// byte[] iv = new byte[0];
// if (cipherProvider instanceof RandomIVPBECipherProvider) {
// iv = cipher.getIV();
// }
Cipher cipher;
if (pbecp instanceof RandomIVPBECipherProvider) {
cipher = ((RandomIVPBECipherProvider) pbecp).getCipher(encryptionMethod, new String(password.getPassword()), salt, ivBytes, keyLength, false);
} else {
cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
}
// Decrypt the plaintext
return cipher.doFinal(actualCipherBytes);
@ -437,6 +499,14 @@ public class StringEncryptor {
}
}
private static int determineSaltLength(String algorithm) {
if (isCustomAlgorithm(algorithm)) {
return CUSTOM_ALGORITHM_SALT_LENGTH;
} else {
return CipherUtility.getSaltLengthForAlgorithm(algorithm);
}
}
private byte[] decryptKeyed(byte[] cipherBytes) {
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
@ -448,7 +518,7 @@ public class StringEncryptor {
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, ivLength, cipherBytes.length);
Cipher cipher = keyedcp.getCipher(EncryptionMethod.forAlgorithm(algorithm), key, iv, false);
Cipher cipher = keyedcp.getCipher(getEncryptionMethodForAlgorithm(algorithm), key, iv, false);
// Encrypt the plaintext
return cipher.doFinal(actualCipherBytes);

View File

@ -21,6 +21,7 @@ import org.apache.nifi.properties.StandardNiFiProperties
import org.apache.nifi.security.kms.CryptoUtils
import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider
import org.apache.nifi.security.util.crypto.Argon2CipherProvider
import org.apache.nifi.security.util.crypto.CipherUtility
import org.apache.nifi.security.util.crypto.KeyedCipherProvider
import org.apache.nifi.util.NiFiProperties
@ -32,7 +33,6 @@ import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@ -46,6 +46,7 @@ import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.security.Security
@ -81,8 +82,11 @@ class StringEncryptorTest {
final Map RAW_PROPERTIES = [(ALGORITHM): DEFAULT_ALGORITHM, (PROVIDER): DEFAULT_PROVIDER, (KEY): DEFAULT_PASSWORD]
private static final NiFiProperties STANDARD_PROPERTIES = new StandardNiFiProperties(new Properties(RAW_PROPERTIES))
private static final byte[] DEFAULT_SALT = new byte[8]
private static final byte[] DEFAULT_IV = new byte[16]
private static final int SALT_LENGTH = 8
private static final int IV_LENGTH = 16
private static final byte[] DEFAULT_SALT = new byte[SALT_LENGTH]
private static final byte[] DEFAULT_IV = new byte[IV_LENGTH]
private static final int DEFAULT_ITERATION_COUNT = 0
@BeforeClass
@ -502,35 +506,13 @@ class StringEncryptorTest {
assert propertiesEncryptor == DEFAULT_ENCRYPTOR
}
/**
* Checks the {@link StringEncryptor#createEncryptor(String, String, String)} method which throws an exception if {@code nifi.sensitive.props.key} is not provided.
*
* @throws Exception
*/
@Ignore("Regression test for old behavior")
@Test
void testStringCreateEncryptorShouldRequireKey() throws Exception {
// Arrange
final StringEncryptor DEFAULT_ENCRYPTOR = new StringEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, DEFAULT_PASSWORD)
logger.info("Created encryptor from constructor using default values: ${DEFAULT_ENCRYPTOR}")
// Act
def constructMsg = shouldFail(EncryptionException) {
StringEncryptor stringEncryptor = StringEncryptor.createEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, "")
}
logger.expected(constructMsg)
// Assert
assert constructMsg =~ "key must be set"
}
/**
* Checks the {@link StringEncryptor#createEncryptor(String, String, String)} method which injects a default {@code nifi.sensitive.props.key} if one is not provided.
*
* @throws Exception
*/
@Test
void testStringCreateEncryptorShouldPopulateDefaultKeyIfMissing() throws Exception {
void testCreateEncryptorShouldPopulateDefaultKeyIfMissing() throws Exception {
// Arrange
final StringEncryptor DEFAULT_ENCRYPTOR = new StringEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, DEFAULT_PASSWORD)
logger.info("Created encryptor from constructor using default values: ${DEFAULT_ENCRYPTOR}")
@ -571,7 +553,7 @@ class StringEncryptorTest {
StringEncryptor passwordEncryptor = new StringEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, DEFAULT_PASSWORD.reverse())
logger.info("Created encryptor with ${DEFAULT_PASSWORD.reverse()} password: ${passwordEncryptor}")
// Act
boolean defaultIsEqual = DEFAULT_ENCRYPTOR.equals(DEFAULT_ENCRYPTOR)
logger.info("[${defaultIsEqual.toString().padLeft(5)}]: default == default")
@ -581,7 +563,7 @@ class StringEncryptorTest {
boolean sameValueIsEqual = DEFAULT_ENCRYPTOR.equals(sameValueEncryptor)
logger.info("[${sameValueIsEqual.toString().padLeft(5)}]: default == same value")
// boolean cloneIsEqual = DEFAULT_ENCRYPTOR.equals(cloneEncryptor)
// logger.info("[${cloneIsEqual.toString().padLeft(5)}]: ${DEFAULT_ENCRYPTOR} | ${cloneEncryptor}")
@ -589,17 +571,17 @@ class StringEncryptorTest {
boolean base64IsEqual = DEFAULT_ENCRYPTOR.equals(base64Encryptor)
logger.info("[${base64IsEqual.toString().padLeft(5)}]: default == base64")
boolean algorithmIsEqual = DEFAULT_ENCRYPTOR.equals(algorithmEncryptor)
logger.info("[${algorithmIsEqual.toString().padLeft(5)}]: default == algorithm")
boolean providerIsEqual = DEFAULT_ENCRYPTOR.equals(providerEncryptor)
logger.info("[${providerIsEqual.toString().padLeft(5)}]: default == provider")
boolean passwordIsEqual = DEFAULT_ENCRYPTOR.equals(passwordEncryptor)
logger.info("[${passwordIsEqual.toString().padLeft(5)}]: default == password")
// Assert
assert defaultIsEqual
assert identityIsEqual
@ -611,4 +593,106 @@ class StringEncryptorTest {
assert !providerIsEqual
assert !passwordIsEqual
}
/**
* Checks the custom algorithm (Argon2+AES-G/CM) created via direct constructor.
*
* @throws Exception
*/
@Test
void testCustomAlgorithmShouldDeriveKeyAndEncrypt() throws Exception {
// Arrange
final String CUSTOM_ALGORITHM = "NIFI_ARGON2_AES_GCM_256"
final String PASSWORD = "nifiPassword123"
final String plaintext = "some sensitive flow value"
StringEncryptor encryptor = StringEncryptor.createEncryptor(CUSTOM_ALGORITHM, DEFAULT_PROVIDER, PASSWORD)
logger.info("Created encryptor: ${encryptor}")
// Act
def ciphertext = encryptor.encrypt(plaintext)
logger.info("Encrypted plaintext to ${ciphertext}")
// Decrypt the ciphertext using a manually-constructed cipher to validate
byte[] saltIvAndCipherBytes = Hex.decodeHex(ciphertext)
int sl = StringEncryptor.CUSTOM_ALGORITHM_SALT_LENGTH
byte[] saltBytes = saltIvAndCipherBytes[0..<sl]
byte[] ivBytes = saltIvAndCipherBytes[sl..<sl + IV_LENGTH]
byte[] cipherBytes = saltIvAndCipherBytes[sl + IV_LENGTH..-1]
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(CUSTOM_ALGORITHM)
// Construct the decryption cipher provider manually
Argon2CipherProvider a2cp = new Argon2CipherProvider()
Cipher decryptCipher = a2cp.getCipher(EncryptionMethod.AES_GCM, PASSWORD, saltBytes, ivBytes, keyLength, false)
// Decrypt a known message with the cipher
byte[] recoveredBytes = decryptCipher.doFinal(cipherBytes)
def recovered = new String(recoveredBytes, StandardCharsets.UTF_8)
logger.info("Decrypted ciphertext to ${recovered}")
// Assert
assert recovered == plaintext
}
/**
* Checks the custom algorithm (Argon2+AES-G/CM) created via direct constructor.
*
* @throws Exception
*/
@Test
void testCustomAlgorithmShouldDeriveKeyAndDecrypt() throws Exception {
// Arrange
final String CUSTOM_ALGORITHM = "NIFI_ARGON2_AES_GCM_256"
final String PASSWORD = "nifiPassword123"
final String plaintext = "some sensitive flow value"
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(CUSTOM_ALGORITHM)
// Manually construct a cipher provider with a key derived from the password using Argon2
Argon2CipherProvider a2cp = new Argon2CipherProvider()
// Generate salt and IV
byte[] ivBytes = new byte[16]
new SecureRandom().nextBytes(ivBytes)
byte[] saltBytes = a2cp.generateSalt()
Cipher encryptCipher = a2cp.getCipher(EncryptionMethod.AES_GCM, PASSWORD, saltBytes, ivBytes, keyLength, true)
// Encrypt a known message with the cipher
byte[] cipherBytes = encryptCipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))
byte[] concatenatedBytes = CryptoUtils.concatByteArrays(saltBytes, ivBytes, cipherBytes)
def ciphertext = Hex.encodeHexString(concatenatedBytes)
logger.info("Encrypted plaintext to ${ciphertext}")
StringEncryptor encryptor = StringEncryptor.createEncryptor(CUSTOM_ALGORITHM, DEFAULT_PROVIDER, PASSWORD)
logger.info("Created encryptor: ${encryptor}")
// Act
def recovered = encryptor.decrypt(ciphertext)
logger.info("Recovered ciphertext to ${recovered}")
// Assert
assert recovered == plaintext
}
/**
* Checks the custom algorithm (Argon2+AES-G/CM) minimum password length.
*
* @throws Exception
*/
@Test
void testCustomAlgorithmShouldRequireMinimumPasswordLength() throws Exception {
// Arrange
final String CUSTOM_ALGORITHM = "NIFI_ARGON2_AES_GCM_256"
final String PASSWORD = "shortPass"
// Act
def msg = shouldFail(EncryptionException) {
StringEncryptor encryptor = StringEncryptor.createEncryptor(CUSTOM_ALGORITHM, DEFAULT_PROVIDER, PASSWORD)
logger.info("Created encryptor: ${encryptor}")
}
logger.expected(msg)
// Assert
assert msg =~ "password provided is invalid for algorithm .* >= 12 characters"
}
}