This is an automated email from the ASF dual-hosted git repository.
alopresto pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 7d20c03 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.
7d20c03 is described below
commit 7d20c03f89358a5d5c6db63e631013e1c4be4bc4
Author: Andy LoPresto <[email protected]>
AuthorDate: Fri Jul 17 15:33:47 2020 -0700
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.
---
.../util/crypto/RandomIVPBECipherProvider.java | 2 +-
.../src/main/asciidoc/administration-guide.adoc | 14 +-
.../org/apache/nifi/encrypt/StringEncryptor.java | 148 ++++++++++++++------
.../apache/nifi/encrypt/StringEncryptorTest.groovy | 150 ++++++++++++++++-----
4 files changed, 240 insertions(+), 74 deletions(-)
diff --git
a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/RandomIVPBECipherProvider.java
b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/RandomIVPBECipherProvider.java
index 99ad9c6..f536770 100644
---
a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/RandomIVPBECipherProvider.java
+++
b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/RandomIVPBECipherProvider.java
@@ -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();
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 86777d2..3c1ebbd 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -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 pass [...]
+
+* `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 [...]
[[encrypt-config_tool]]
== Encrypted Passwords in Configuration Files
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
index 15750f5..b1b601f 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
@@ -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);
-
- // 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();
- // }
+ byte[] ivBytes = new byte[0];
+ Cipher cipher;
+
+ // 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);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
index c07e0ec..31325ae 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
@@ -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
@@ -503,34 +507,12 @@ class StringEncryptorTest {
}
/**
- * 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"
+ }
}