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" + } }