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 <alopre...@apache.org>
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"
+    }
 }

Reply via email to