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
|
* @return the initialized cipher
|
||||||
* @throws Exception if there is a problem initializing the 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();
|
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.
|
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]]
|
[[encrypt-config_tool]]
|
||||||
== Encrypted Passwords in Configuration Files
|
== 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.CipherUtility;
|
||||||
import org.apache.nifi.security.util.crypto.KeyedCipherProvider;
|
import org.apache.nifi.security.util.crypto.KeyedCipherProvider;
|
||||||
import org.apache.nifi.security.util.crypto.PBECipherProvider;
|
import org.apache.nifi.security.util.crypto.PBECipherProvider;
|
||||||
|
import org.apache.nifi.security.util.crypto.RandomIVPBECipherProvider;
|
||||||
import org.apache.nifi.util.NiFiProperties;
|
import org.apache.nifi.util.NiFiProperties;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.util.encoders.Base64;
|
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_ALGORITHMS = new ArrayList<>();
|
||||||
private static final List<String> SUPPORTED_PROVIDERS = 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 algorithm;
|
||||||
private final String provider;
|
private final String provider;
|
||||||
private final PBEKeySpec password;
|
private final PBEKeySpec password;
|
||||||
private final SecretKeySpec key;
|
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;
|
private CipherProvider cipherProvider;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
|
||||||
|
SUPPORTED_ALGORITHMS.addAll(CUSTOM_ALGORITHMS);
|
||||||
for (EncryptionMethod em : EncryptionMethod.values()) {
|
for (EncryptionMethod em : EncryptionMethod.values()) {
|
||||||
SUPPORTED_ALGORITHMS.add(em.getAlgorithm());
|
SUPPORTED_ALGORITHMS.add(em.getAlgorithm());
|
||||||
}
|
}
|
||||||
|
@ -110,8 +123,8 @@ public class StringEncryptor {
|
||||||
* <p>
|
* <p>
|
||||||
* For actual raw key provision, see {@link #StringEncryptor(String, String, byte[])}.
|
* For actual raw key provision, see {@link #StringEncryptor(String, String, byte[])}.
|
||||||
*
|
*
|
||||||
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
|
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#getAlgorithm()})
|
||||||
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
|
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
|
||||||
* @param key the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
|
* @param key the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
|
||||||
*/
|
*/
|
||||||
public StringEncryptor(final String algorithm, final String provider, final String 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
|
* 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).
|
* (usually expressed for human-readability/transmission in hexadecimal or Base64 encoded format).
|
||||||
*
|
*
|
||||||
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
|
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#getAlgorithm()})
|
||||||
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
|
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
|
||||||
* @param key a raw encryption key in bytes
|
* @param key a raw encryption key in bytes
|
||||||
*/
|
*/
|
||||||
public StringEncryptor(final String algorithm, final String provider, final byte[] key) {
|
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.
|
* 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
|
* @return the cipher family
|
||||||
* @throws EncryptionException if the algorithm is null/empty or not supported
|
* @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.
|
* 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 algorithm the encryption (and key derivation) algorithm ({@link EncryptionMethod#getAlgorithm()})
|
||||||
* @param provider the JCA Security provider ({@link EncryptionMethod#provider})
|
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
|
||||||
* @param password the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
|
* @param password the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
|
||||||
* @return the initialized encryptor
|
* @return the initialized encryptor
|
||||||
*/
|
*/
|
||||||
|
@ -245,7 +258,10 @@ public class StringEncryptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paramsAreValid()) {
|
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);
|
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NIFI_LEGACY);
|
||||||
} else {
|
} else {
|
||||||
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
|
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() {
|
private boolean paramsAreValid() {
|
||||||
boolean algorithmAndProviderValid = algorithmIsValid(algorithm) && providerIsValid(provider);
|
boolean algorithmAndProviderValid = algorithmIsValid(algorithm) && providerIsValid(provider);
|
||||||
boolean secretIsValid = false;
|
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);
|
secretIsValid = passwordIsValid(password);
|
||||||
} else if (CipherUtility.isKeyedCipher(algorithm)) {
|
} else if (CipherUtility.isKeyedCipher(algorithm)) {
|
||||||
secretIsValid = keyIsValid(key, algorithm);
|
secretIsValid = keyIsValid(key, algorithm);
|
||||||
|
@ -267,6 +300,13 @@ public class StringEncryptor {
|
||||||
return algorithmAndProviderValid && secretIsValid;
|
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) {
|
private boolean keyIsValid(SecretKeySpec key, String algorithm) {
|
||||||
return key != null && CipherUtility.getValidKeyLengthsForAlgorithm(algorithm).contains(key.getEncoded().length * 8);
|
return key != null && CipherUtility.getValidKeyLengthsForAlgorithm(algorithm).contains(key.getEncoded().length * 8);
|
||||||
}
|
}
|
||||||
|
@ -280,10 +320,10 @@ public class StringEncryptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEncoding(String base) {
|
public void setEncoding(String base) {
|
||||||
if ("HEX".equalsIgnoreCase(base)) {
|
if (HEX_ENCODING.equalsIgnoreCase(base)) {
|
||||||
this.encoding = "HEX";
|
this.encoding = HEX_ENCODING;
|
||||||
} else if ("BASE64".equalsIgnoreCase(base)) {
|
} else if (B64_ENCODING.equalsIgnoreCase(base)) {
|
||||||
this.encoding = "BASE64";
|
this.encoding = B64_ENCODING;
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("The encoding base must be 'HEX' or 'BASE64'");
|
throw new IllegalArgumentException("The encoding base must be 'HEX' or 'BASE64'");
|
||||||
}
|
}
|
||||||
|
@ -300,7 +340,8 @@ public class StringEncryptor {
|
||||||
try {
|
try {
|
||||||
if (isInitialized()) {
|
if (isInitialized()) {
|
||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
if (CipherUtility.isPBECipher(algorithm)) {
|
// Currently all custom algorithms are PBE (Argon2)
|
||||||
|
if (CipherUtility.isPBECipher(algorithm) || isCustomAlgorithm(algorithm)) {
|
||||||
rawBytes = encryptPBE(clearText);
|
rawBytes = encryptPBE(clearText);
|
||||||
} else {
|
} else {
|
||||||
rawBytes = encryptKeyed(clearText);
|
rawBytes = encryptKeyed(clearText);
|
||||||
|
@ -316,7 +357,7 @@ public class StringEncryptor {
|
||||||
|
|
||||||
private byte[] encryptPBE(String plaintext) {
|
private byte[] encryptPBE(String plaintext) {
|
||||||
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
|
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
|
||||||
final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
|
final EncryptionMethod encryptionMethod = getEncryptionMethodForAlgorithm(algorithm);
|
||||||
|
|
||||||
// Generate salt
|
// Generate salt
|
||||||
byte[] salt;
|
byte[] salt;
|
||||||
|
@ -332,25 +373,38 @@ public class StringEncryptor {
|
||||||
|
|
||||||
// Generate cipher
|
// Generate cipher
|
||||||
try {
|
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)
|
// Generate IV if necessary (allows for future use of Argon2, PBKDF2, Bcrypt, or Scrypt)
|
||||||
// byte[] iv = new byte[0];
|
if (cipherProvider instanceof RandomIVPBECipherProvider) {
|
||||||
// if (cipherProvider instanceof RandomIVPBECipherProvider) {
|
// Generating the IV here rather than delegating to the cipher provider suppresses the warning messages
|
||||||
// iv = cipher.getIV();
|
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
|
// Encrypt the plaintext
|
||||||
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
// Combine the output
|
// Combine the output
|
||||||
// byte[] rawBytes = CryptoUtils.concatByteArrays(salt, iv, cipherBytes);
|
return CryptoUtils.concatByteArrays(salt, ivBytes, cipherBytes);
|
||||||
return CryptoUtils.concatByteArrays(salt, cipherBytes);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new EncryptionException("Could not encrypt sensitive value", 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) {
|
private byte[] encryptKeyed(String plaintext) {
|
||||||
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
|
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
|
||||||
|
|
||||||
|
@ -360,7 +414,7 @@ public class StringEncryptor {
|
||||||
byte[] iv = new byte[16];
|
byte[] iv = new byte[16];
|
||||||
sr.nextBytes(iv);
|
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
|
// Encrypt the plaintext
|
||||||
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||||
|
@ -373,7 +427,7 @@ public class StringEncryptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String encode(byte[] rawBytes) {
|
private String encode(byte[] rawBytes) {
|
||||||
if (this.encoding.equalsIgnoreCase("HEX")) {
|
if (this.encoding.equalsIgnoreCase(HEX_ENCODING)) {
|
||||||
return Hex.encodeHexString(rawBytes);
|
return Hex.encodeHexString(rawBytes);
|
||||||
} else {
|
} else {
|
||||||
return Base64.toBase64String(rawBytes);
|
return Base64.toBase64String(rawBytes);
|
||||||
|
@ -392,7 +446,8 @@ public class StringEncryptor {
|
||||||
if (isInitialized()) {
|
if (isInitialized()) {
|
||||||
byte[] plainBytes;
|
byte[] plainBytes;
|
||||||
byte[] cipherBytes = decode(cipherText);
|
byte[] cipherBytes = decode(cipherText);
|
||||||
if (CipherUtility.isPBECipher(algorithm)) {
|
// Currently all custom algorithms are PBE (Argon2)
|
||||||
|
if (CipherUtility.isPBECipher(algorithm) || isCustomAlgorithm(algorithm)) {
|
||||||
plainBytes = decryptPBE(cipherBytes);
|
plainBytes = decryptPBE(cipherBytes);
|
||||||
} else {
|
} else {
|
||||||
plainBytes = decryptKeyed(cipherBytes);
|
plainBytes = decryptKeyed(cipherBytes);
|
||||||
|
@ -408,27 +463,34 @@ public class StringEncryptor {
|
||||||
|
|
||||||
private byte[] decryptPBE(byte[] cipherBytes) {
|
private byte[] decryptPBE(byte[] cipherBytes) {
|
||||||
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
|
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
|
||||||
final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
|
final EncryptionMethod encryptionMethod = getEncryptionMethodForAlgorithm(algorithm);
|
||||||
|
|
||||||
// Extract salt
|
// Extract salt
|
||||||
int saltLength = CipherUtility.getSaltLengthForAlgorithm(algorithm);
|
int saltLength = determineSaltLength(algorithm);
|
||||||
byte[] salt = new byte[saltLength];
|
byte[] salt = new byte[saltLength];
|
||||||
System.arraycopy(cipherBytes, 0, salt, 0, 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
|
// Determine necessary key length
|
||||||
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
|
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
|
||||||
|
|
||||||
// Generate cipher
|
// Generate cipher
|
||||||
try {
|
try {
|
||||||
Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
|
Cipher cipher;
|
||||||
|
if (pbecp instanceof RandomIVPBECipherProvider) {
|
||||||
// Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
|
cipher = ((RandomIVPBECipherProvider) pbecp).getCipher(encryptionMethod, new String(password.getPassword()), salt, ivBytes, keyLength, false);
|
||||||
// byte[] iv = new byte[0];
|
} else {
|
||||||
// if (cipherProvider instanceof RandomIVPBECipherProvider) {
|
cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
|
||||||
// iv = cipher.getIV();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// Decrypt the plaintext
|
// Decrypt the plaintext
|
||||||
return cipher.doFinal(actualCipherBytes);
|
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) {
|
private byte[] decryptKeyed(byte[] cipherBytes) {
|
||||||
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
|
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
|
||||||
|
|
||||||
|
@ -448,7 +518,7 @@ public class StringEncryptor {
|
||||||
|
|
||||||
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, ivLength, cipherBytes.length);
|
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
|
// Encrypt the plaintext
|
||||||
return cipher.doFinal(actualCipherBytes);
|
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.kms.CryptoUtils
|
||||||
import org.apache.nifi.security.util.EncryptionMethod
|
import org.apache.nifi.security.util.EncryptionMethod
|
||||||
import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider
|
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.CipherUtility
|
||||||
import org.apache.nifi.security.util.crypto.KeyedCipherProvider
|
import org.apache.nifi.security.util.crypto.KeyedCipherProvider
|
||||||
import org.apache.nifi.util.NiFiProperties
|
import org.apache.nifi.util.NiFiProperties
|
||||||
|
@ -32,7 +33,6 @@ import org.junit.After
|
||||||
import org.junit.Assume
|
import org.junit.Assume
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.JUnit4
|
import org.junit.runners.JUnit4
|
||||||
|
@ -46,6 +46,7 @@ import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.PBEKeySpec
|
import javax.crypto.spec.PBEKeySpec
|
||||||
import javax.crypto.spec.PBEParameterSpec
|
import javax.crypto.spec.PBEParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
|
||||||
|
@ -81,8 +82,11 @@ class StringEncryptorTest {
|
||||||
final Map RAW_PROPERTIES = [(ALGORITHM): DEFAULT_ALGORITHM, (PROVIDER): DEFAULT_PROVIDER, (KEY): DEFAULT_PASSWORD]
|
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 NiFiProperties STANDARD_PROPERTIES = new StandardNiFiProperties(new Properties(RAW_PROPERTIES))
|
||||||
|
|
||||||
private static final byte[] DEFAULT_SALT = new byte[8]
|
private static final int SALT_LENGTH = 8
|
||||||
private static final byte[] DEFAULT_IV = new byte[16]
|
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
|
private static final int DEFAULT_ITERATION_COUNT = 0
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
|
@ -502,35 +506,13 @@ class StringEncryptorTest {
|
||||||
assert propertiesEncryptor == DEFAULT_ENCRYPTOR
|
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.
|
* 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
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testStringCreateEncryptorShouldPopulateDefaultKeyIfMissing() throws Exception {
|
void testCreateEncryptorShouldPopulateDefaultKeyIfMissing() throws Exception {
|
||||||
// Arrange
|
// Arrange
|
||||||
final StringEncryptor DEFAULT_ENCRYPTOR = new StringEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, DEFAULT_PASSWORD)
|
final StringEncryptor DEFAULT_ENCRYPTOR = new StringEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, DEFAULT_PASSWORD)
|
||||||
logger.info("Created encryptor from constructor using default values: ${DEFAULT_ENCRYPTOR}")
|
logger.info("Created encryptor from constructor using default values: ${DEFAULT_ENCRYPTOR}")
|
||||||
|
@ -611,4 +593,106 @@ class StringEncryptorTest {
|
||||||
assert !providerIsEqual
|
assert !providerIsEqual
|
||||||
assert !passwordIsEqual
|
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