mirror of https://github.com/apache/nifi.git
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:
parent
5cb8d24689
commit
7d20c03f89
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue