mirror of https://github.com/apache/nifi.git
NIFI-1242:
Added logic and test resources to debug JCE unlimited strength cryptography policy issues and incorporated into processor property validation. Excluded test resources from RAT check Added KeyDerivationFunction enum. Added kdf property in EncryptContent processor and provided to PasswordBasedEncryptor. Added logic in PasswordBasedEncryptor to handle variable KDF. Added unit tests for EncryptContent processor. Added test resources and excluded from RAT check. plain.txt: This is a plaintext message. 0s @ 12:20:32 $ openssl enc -aes-256-cbc -e -in plain.txt -out salted_raw.enc -k thisIsABadPassword -p salt=31DC301A6C7B8A0B key=CB878A6E167A5B530B8F2BD175E6359E3092AFF7C83274A22A5B421D79E599AC iv =0C614A72FC06B454B84E035B3FA8F877 0s @ 12:20:44 $ xxd salted_raw.enc 0000000: 5361 6c74 6564 5f5f 31dc 301a 6c7b 8a0b Salted__1.0.l{.. 0000010: 616b c65d f767 504d c085 ba7a c517 d0cb ak.].gPM...z.... 0000020: 7832 211e f573 b6f1 ded2 8f59 88e8 088f x2!..s.....Y.... 0s @ 20:14:00 $ openssl enc -aes-256-cbc -e -in plain.txt -out unsalted_raw.enc -k thisIsABadPassword -p -nosalt key=711E85689CE7AFF6F410AEA43ABC5446842F685B84879B2E00F977C22B9E9A7D iv =0C90ABF8ECE84B92BAA2CD448EC760F0 0s @ 20:14:17 $ xxd unsalted_raw.enc 0000000: 70cd 2984 fdbb 0e7c c01b 7206 88b1 6b50 p.)....|..r...kP 0000010: 5eeb e4f3 4036 773b 00ce dd8e 85d8 f90a ^...@6w;........ This closes #140 Signed-off-by: Aldrin Piri <aldrin@apache.org>
This commit is contained in:
parent
ee14d8f9dd
commit
f83e6d33c5
|
@ -27,25 +27,25 @@ import org.apache.commons.lang3.builder.ToStringStyle;
|
|||
public enum EncryptionMethod {
|
||||
|
||||
MD5_128AES("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC", false),
|
||||
MD5_256AES("PBEWITHMD5AND256BITAES-CBC-OPENSSL", "BC", false),
|
||||
SHA1_RC2("PBEWITHSHA1ANDRC2", "BC", false),
|
||||
SHA1_DES("PBEWITHSHA1ANDDES", "BC", false),
|
||||
MD5_192AES("PBEWITHMD5AND192BITAES-CBC-OPENSSL", "BC", false),
|
||||
MD5_256AES("PBEWITHMD5AND256BITAES-CBC-OPENSSL", "BC", false),
|
||||
MD5_DES("PBEWITHMD5ANDDES", "BC", false),
|
||||
MD5_RC2("PBEWITHMD5ANDRC2", "BC", false),
|
||||
SHA_192AES("PBEWITHSHAAND192BITAES-CBC-BC", "BC", true),
|
||||
SHA_40RC4("PBEWITHSHAAND40BITRC4", "BC", true),
|
||||
SHA256_128AES("PBEWITHSHA256AND128BITAES-CBC-BC", "BC", true),
|
||||
SHA_128RC2("PBEWITHSHAAND128BITRC2-CBC", "BC", true),
|
||||
SHA1_RC2("PBEWITHSHA1ANDRC2", "BC", false),
|
||||
SHA1_DES("PBEWITHSHA1ANDDES", "BC", false),
|
||||
SHA_128AES("PBEWITHSHAAND128BITAES-CBC-BC", "BC", true),
|
||||
SHA256_192AES("PBEWITHSHA256AND192BITAES-CBC-BC", "BC", true),
|
||||
SHA_2KEYTRIPLEDES("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", "BC", true),
|
||||
SHA256_256AES("PBEWITHSHA256AND256BITAES-CBC-BC", "BC", true),
|
||||
SHA_40RC2("PBEWITHSHAAND40BITRC2-CBC", "BC", true),
|
||||
SHA_192AES("PBEWITHSHAAND192BITAES-CBC-BC", "BC", true),
|
||||
SHA_256AES("PBEWITHSHAAND256BITAES-CBC-BC", "BC", true),
|
||||
SHA_40RC2("PBEWITHSHAAND40BITRC2-CBC", "BC", true),
|
||||
SHA_128RC2("PBEWITHSHAAND128BITRC2-CBC", "BC", true),
|
||||
SHA_40RC4("PBEWITHSHAAND40BITRC4", "BC", true),
|
||||
SHA_128RC4("PBEWITHSHAAND128BITRC4", "BC", true),
|
||||
SHA256_128AES("PBEWITHSHA256AND128BITAES-CBC-BC", "BC", true),
|
||||
SHA256_192AES("PBEWITHSHA256AND192BITAES-CBC-BC", "BC", true),
|
||||
SHA256_256AES("PBEWITHSHA256AND256BITAES-CBC-BC", "BC", true),
|
||||
SHA_2KEYTRIPLEDES("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", "BC", true),
|
||||
SHA_3KEYTRIPLEDES("PBEWITHSHAAND3-KEYTRIPLEDES-CBC", "BC", true),
|
||||
SHA_TWOFISH("PBEWITHSHAANDTWOFISH-CBC", "BC", true),
|
||||
SHA_128RC4("PBEWITHSHAAND128BITRC4", "BC", true),
|
||||
PGP("PGP", "BC", false),
|
||||
PGP_ASCII_ARMOR("PGP-ASCII-ARMOR", "BC", false);
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.security.util;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
/**
|
||||
* Enumeration capturing essential information about the various key derivation functions that might be supported.
|
||||
*/
|
||||
public enum KeyDerivationFunction {
|
||||
|
||||
NIFI_LEGACY("NiFi legacy KDF", "MD5 @ 1000 iterations"),
|
||||
OPENSSL_EVP_BYTES_TO_KEY("OpenSSL EVP_BytesToKey", "Single iteration MD5 compatible with PKCS#5 v1.5");
|
||||
// TODO: Implement bcrypt, scrypt, and PBKDF2
|
||||
|
||||
private final String name;
|
||||
private final String description;
|
||||
|
||||
KeyDerivationFunction(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final ToStringBuilder builder = new ToStringBuilder(this);
|
||||
ToStringBuilder.setDefaultStyle(ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
builder.append("KDF Name", name);
|
||||
builder.append("Description", description);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -310,6 +310,9 @@ language governing permissions and limitations under the License. -->
|
|||
<exclude>src/test/resources/TestIdentifyMimeType/flowfilev1.tar</exclude>
|
||||
<exclude>src/test/resources/TestUnpackContent/data.tar</exclude>
|
||||
<exclude>src/test/resources/TestUnpackContent/data.zip</exclude>
|
||||
<exclude>src/test/resources/TestEncryptContent/plain.txt</exclude>
|
||||
<exclude>src/test/resources/TestEncryptContent/salted_raw.enc</exclude>
|
||||
<exclude>src/test/resources/TestEncryptContent/unsalted_raw.enc</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.annotation.behavior.EventDriven;
|
||||
import org.apache.nifi.annotation.behavior.InputRequirement;
|
||||
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
|
||||
|
@ -51,6 +52,7 @@ import org.apache.nifi.processors.standard.util.OpenPGPKeyBasedEncryptor;
|
|||
import org.apache.nifi.processors.standard.util.OpenPGPPasswordBasedEncryptor;
|
||||
import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor;
|
||||
import org.apache.nifi.security.util.EncryptionMethod;
|
||||
import org.apache.nifi.security.util.KeyDerivationFunction;
|
||||
import org.apache.nifi.util.StopWatch;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
|
@ -72,12 +74,20 @@ public class EncryptContent extends AbstractProcessor {
|
|||
.allowableValues(ENCRYPT_MODE, DECRYPT_MODE)
|
||||
.defaultValue(ENCRYPT_MODE)
|
||||
.build();
|
||||
public static final PropertyDescriptor KEY_DERIVATION_FUNCTION = new PropertyDescriptor.Builder()
|
||||
.name("key-derivation-function")
|
||||
.displayName("Key Derivation Function")
|
||||
.description("Specifies the key derivation function to generate the key from the password (and salt)")
|
||||
.required(true)
|
||||
.allowableValues(KeyDerivationFunction.values())
|
||||
.defaultValue(KeyDerivationFunction.NIFI_LEGACY.name())
|
||||
.build();
|
||||
public static final PropertyDescriptor ENCRYPTION_ALGORITHM = new PropertyDescriptor.Builder()
|
||||
.name("Encryption Algorithm")
|
||||
.description("The Encryption Algorithm to use")
|
||||
.required(true)
|
||||
.allowableValues(EncryptionMethod.values())
|
||||
.defaultValue(EncryptionMethod.MD5_256AES.name())
|
||||
.defaultValue(EncryptionMethod.MD5_128AES.name())
|
||||
.build();
|
||||
public static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder()
|
||||
.name("Password")
|
||||
|
@ -133,6 +143,7 @@ public class EncryptContent extends AbstractProcessor {
|
|||
protected void init(final ProcessorInitializationContext context) {
|
||||
final List<PropertyDescriptor> properties = new ArrayList<>();
|
||||
properties.add(MODE);
|
||||
properties.add(KEY_DERIVATION_FUNCTION);
|
||||
properties.add(ENCRYPTION_ALGORITHM);
|
||||
properties.add(PASSWORD);
|
||||
properties.add(PUBLIC_KEYRING);
|
||||
|
@ -168,9 +179,11 @@ public class EncryptContent extends AbstractProcessor {
|
|||
@Override
|
||||
protected Collection<ValidationResult> customValidate(final ValidationContext context) {
|
||||
final List<ValidationResult> validationResults = new ArrayList<>(super.customValidate(context));
|
||||
final String method = context.getProperty(ENCRYPTION_ALGORITHM).getValue();
|
||||
final String algorithm = EncryptionMethod.valueOf(method).getAlgorithm();
|
||||
final String methodValue = context.getProperty(ENCRYPTION_ALGORITHM).getValue();
|
||||
final EncryptionMethod method = EncryptionMethod.valueOf(methodValue);
|
||||
final String algorithm = method.getAlgorithm();
|
||||
final String password = context.getProperty(PASSWORD).getValue();
|
||||
final String kdf = context.getProperty(KEY_DERIVATION_FUNCTION).getValue();
|
||||
if (isPGPAlgorithm(algorithm)) {
|
||||
if (password == null) {
|
||||
final boolean encrypt = context.getProperty(MODE).getValue().equalsIgnoreCase(ENCRYPT_MODE);
|
||||
|
@ -209,7 +222,7 @@ public class EncryptContent extends AbstractProcessor {
|
|||
+ PRIVATE_KEYRING.getDisplayName() + " and " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
|
||||
.build());
|
||||
} else {
|
||||
final String providerName = EncryptionMethod.valueOf(method).getProvider();
|
||||
final String providerName = EncryptionMethod.valueOf(methodValue).getProvider();
|
||||
// verify the passphrase works on the private keyring
|
||||
try {
|
||||
if (!OpenPGPKeyBasedEncryptor.validateKeyring(providerName, privateKeyring, keyringPassphrase.toCharArray())) {
|
||||
|
@ -227,9 +240,33 @@ public class EncryptContent extends AbstractProcessor {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (password == null) {
|
||||
} else { // PBE
|
||||
if (!PasswordBasedEncryptor.supportsUnlimitedStrength()) {
|
||||
if (method.isUnlimitedStrength()) {
|
||||
validationResults.add(new ValidationResult.Builder().subject(ENCRYPTION_ALGORITHM.getName())
|
||||
.explanation(methodValue + " (" + algorithm + ") is not supported by this JVM due to lacking JCE Unlimited " +
|
||||
"Strength Jurisdiction Policy files.").build());
|
||||
}
|
||||
}
|
||||
int allowedKeyLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(ENCRYPTION_ALGORITHM.getName());
|
||||
|
||||
if (StringUtils.isEmpty(password)) {
|
||||
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
|
||||
.explanation(PASSWORD.getDisplayName() + " is required when using algorithm " + algorithm).build());
|
||||
} else {
|
||||
if (password.getBytes().length * 8 > allowedKeyLength) {
|
||||
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
|
||||
.explanation("Password length greater than " + allowedKeyLength + " bits is not supported by this JVM" +
|
||||
" due to lacking JCE Unlimited Strength Jurisdiction Policy files.").build());
|
||||
}
|
||||
}
|
||||
|
||||
// Perform some analysis on the selected encryption algorithm to ensure the JVM can support it and the associated key
|
||||
|
||||
if (StringUtils.isEmpty(kdf)) {
|
||||
validationResults.add(new ValidationResult.Builder().subject(KEY_DERIVATION_FUNCTION.getName())
|
||||
.explanation(KEY_DERIVATION_FUNCTION.getDisplayName() + " is required when using algorithm " + algorithm).build());
|
||||
}
|
||||
}
|
||||
return validationResults;
|
||||
}
|
||||
|
@ -247,6 +284,7 @@ public class EncryptContent extends AbstractProcessor {
|
|||
final String providerName = encryptionMethod.getProvider();
|
||||
final String algorithm = encryptionMethod.getAlgorithm();
|
||||
final String password = context.getProperty(PASSWORD).getValue();
|
||||
final KeyDerivationFunction kdf = KeyDerivationFunction.valueOf(context.getProperty(KEY_DERIVATION_FUNCTION).getValue());
|
||||
final boolean encrypt = context.getProperty(MODE).getValue().equalsIgnoreCase(ENCRYPT_MODE);
|
||||
|
||||
Encryptor encryptor;
|
||||
|
@ -267,9 +305,9 @@ public class EncryptContent extends AbstractProcessor {
|
|||
final char[] passphrase = Normalizer.normalize(password, Normalizer.Form.NFC).toCharArray();
|
||||
encryptor = new OpenPGPPasswordBasedEncryptor(algorithm, providerName, passphrase, filename);
|
||||
}
|
||||
} else {
|
||||
} else { // PBE
|
||||
final char[] passphrase = Normalizer.normalize(password, Normalizer.Form.NFC).toCharArray();
|
||||
encryptor = new PasswordBasedEncryptor(algorithm, providerName, passphrase);
|
||||
encryptor = new PasswordBasedEncryptor(algorithm, providerName, passphrase, kdf);
|
||||
}
|
||||
|
||||
if (encrypt) {
|
||||
|
@ -298,10 +336,10 @@ public class EncryptContent extends AbstractProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
public static interface Encryptor {
|
||||
public StreamCallback getEncryptionCallback() throws Exception;
|
||||
public interface Encryptor {
|
||||
StreamCallback getEncryptionCallback() throws Exception;
|
||||
|
||||
public StreamCallback getDecryptionCallback() throws Exception;
|
||||
StreamCallback getDecryptionCallback() throws Exception;
|
||||
}
|
||||
|
||||
}
|
|
@ -20,7 +20,11 @@ import java.io.EOFException;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
|
@ -30,9 +34,11 @@ import javax.crypto.SecretKeyFactory;
|
|||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.PBEParameterSpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.processor.exception.ProcessException;
|
||||
import org.apache.nifi.processor.io.StreamCallback;
|
||||
import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
|
||||
import org.apache.nifi.security.util.KeyDerivationFunction;
|
||||
import org.apache.nifi.stream.io.StreamUtils;
|
||||
|
||||
public class PasswordBasedEncryptor implements Encryptor {
|
||||
|
@ -40,28 +46,99 @@ public class PasswordBasedEncryptor implements Encryptor {
|
|||
private Cipher cipher;
|
||||
private int saltSize;
|
||||
private SecretKey secretKey;
|
||||
private KeyDerivationFunction kdf;
|
||||
private int iterationsCount = LEGACY_KDF_ITERATIONS;
|
||||
|
||||
@Deprecated
|
||||
public static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
|
||||
public static final int DEFAULT_SALT_SIZE = 8;
|
||||
private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
|
||||
private static final int DEFAULT_SALT_SIZE = 8;
|
||||
// TODO: Eventually KDF-specific values should be refactored into injectable interface impls
|
||||
private static final int LEGACY_KDF_ITERATIONS = 1000;
|
||||
private static final int OPENSSL_EVP_HEADER_SIZE = 8;
|
||||
private static final int OPENSSL_EVP_SALT_SIZE = 8;
|
||||
private static final String OPENSSL_EVP_HEADER_MARKER = "Salted__";
|
||||
private static final int OPENSSL_EVP_KDF_ITERATIONS = 0;
|
||||
private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128;
|
||||
|
||||
public PasswordBasedEncryptor(final String algorithm, final String providerName, final char[] password) {
|
||||
private static boolean isUnlimitedStrengthCryptographyEnabled;
|
||||
|
||||
// Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system
|
||||
static {
|
||||
try {
|
||||
isUnlimitedStrengthCryptographyEnabled = (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// if there are issues with this, we default back to the value established
|
||||
isUnlimitedStrengthCryptographyEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public PasswordBasedEncryptor(final String algorithm, final String providerName, final char[] password, KeyDerivationFunction kdf) {
|
||||
super();
|
||||
try {
|
||||
// initialize cipher
|
||||
this.cipher = Cipher.getInstance(algorithm, providerName);
|
||||
this.kdf = kdf;
|
||||
|
||||
if (isOpenSSLKDF()) {
|
||||
this.saltSize = OPENSSL_EVP_SALT_SIZE;
|
||||
this.iterationsCount = OPENSSL_EVP_KDF_ITERATIONS;
|
||||
} else {
|
||||
int algorithmBlockSize = cipher.getBlockSize();
|
||||
this.saltSize = (algorithmBlockSize > 0) ? algorithmBlockSize : DEFAULT_SALT_SIZE;
|
||||
}
|
||||
|
||||
// initialize SecretKey from password
|
||||
PBEKeySpec pbeKeySpec = new PBEKeySpec(password);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, providerName);
|
||||
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password);
|
||||
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, providerName);
|
||||
this.secretKey = factory.generateSecret(pbeKeySpec);
|
||||
} catch (Exception e) {
|
||||
throw new ProcessException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMaxAllowedKeyLength(final String algorithm) {
|
||||
if (StringUtils.isEmpty(algorithm)) {
|
||||
return DEFAULT_MAX_ALLOWED_KEY_LENGTH;
|
||||
}
|
||||
String parsedCipher = parseCipherFromAlgorithm(algorithm);
|
||||
try {
|
||||
return Cipher.getMaxAllowedKeyLength(parsedCipher);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Default algorithm max key length on unmodified JRE
|
||||
return DEFAULT_MAX_ALLOWED_KEY_LENGTH;
|
||||
}
|
||||
}
|
||||
|
||||
private static String parseCipherFromAlgorithm(final String algorithm) {
|
||||
// This is not optimal but the algorithms do not have a standard format
|
||||
final String AES = "AES";
|
||||
final String TDES = "TRIPLEDES";
|
||||
final String DES = "DES";
|
||||
final String RC4 = "RC4";
|
||||
final String RC2 = "RC2";
|
||||
final String TWOFISH = "TWOFISH";
|
||||
final List<String> SYMMETRIC_CIPHERS = Arrays.asList(AES, TDES, DES, RC4, RC2, TWOFISH);
|
||||
|
||||
// The algorithms contain "TRIPLEDES" but the cipher name is "DESede"
|
||||
final String ACTUAL_TDES_CIPHER = "DESede";
|
||||
|
||||
for (String cipher : SYMMETRIC_CIPHERS) {
|
||||
if (algorithm.contains(cipher)) {
|
||||
if (cipher.equals(TDES)) {
|
||||
return ACTUAL_TDES_CIPHER;
|
||||
} else {
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public static boolean supportsUnlimitedStrength() {
|
||||
return isUnlimitedStrengthCryptographyEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamCallback getEncryptionCallback() throws ProcessException {
|
||||
try {
|
||||
|
@ -79,21 +156,50 @@ public class PasswordBasedEncryptor implements Encryptor {
|
|||
return new DecryptCallback();
|
||||
}
|
||||
|
||||
private int getIterationsCount() {
|
||||
return iterationsCount;
|
||||
}
|
||||
|
||||
private boolean isOpenSSLKDF() {
|
||||
return KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY.equals(kdf);
|
||||
}
|
||||
|
||||
private class DecryptCallback implements StreamCallback {
|
||||
|
||||
public DecryptCallback() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(final InputStream in, final OutputStream out) throws IOException {
|
||||
final byte[] salt = new byte[saltSize];
|
||||
byte[] salt = new byte[saltSize];
|
||||
|
||||
try {
|
||||
// If the KDF is OpenSSL, try to read the salt from the input stream
|
||||
if (isOpenSSLKDF()) {
|
||||
// The header and salt format is "Salted__salt x8b" in ASCII
|
||||
|
||||
// Try to read the header and salt from the input
|
||||
byte[] header = new byte[PasswordBasedEncryptor.OPENSSL_EVP_HEADER_SIZE];
|
||||
|
||||
// Mark the stream in case there is no salt
|
||||
in.mark(OPENSSL_EVP_HEADER_SIZE + 1);
|
||||
StreamUtils.fillBuffer(in, header);
|
||||
|
||||
final byte[] headerMarkerBytes = OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
if (!Arrays.equals(headerMarkerBytes, header)) {
|
||||
// No salt present
|
||||
salt = new byte[0];
|
||||
// Reset the stream because we skipped 8 bytes of cipher text
|
||||
in.reset();
|
||||
}
|
||||
}
|
||||
|
||||
StreamUtils.fillBuffer(in, salt);
|
||||
} catch (final EOFException e) {
|
||||
throw new ProcessException("Cannot decrypt because file size is smaller than salt size", e);
|
||||
}
|
||||
|
||||
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 1000);
|
||||
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationsCount());
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
|
||||
} catch (final Exception e) {
|
||||
|
@ -127,13 +233,18 @@ public class PasswordBasedEncryptor implements Encryptor {
|
|||
|
||||
@Override
|
||||
public void process(final InputStream in, final OutputStream out) throws IOException {
|
||||
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 1000);
|
||||
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationsCount());
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
|
||||
} catch (final Exception e) {
|
||||
throw new ProcessException(e);
|
||||
}
|
||||
|
||||
// If this is OpenSSL EVP, the salt must be preceded by the header
|
||||
if (isOpenSSLKDF()) {
|
||||
out.write(OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
out.write(salt);
|
||||
|
||||
final byte[] buffer = new byte[65536];
|
||||
|
|
|
@ -19,20 +19,35 @@ package org.apache.nifi.processors.standard;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.Security;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.nifi.components.ValidationResult;
|
||||
import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor;
|
||||
import org.apache.nifi.security.util.EncryptionMethod;
|
||||
import org.apache.nifi.security.util.KeyDerivationFunction;
|
||||
import org.apache.nifi.util.MockFlowFile;
|
||||
import org.apache.nifi.util.MockProcessContext;
|
||||
import org.apache.nifi.util.TestRunner;
|
||||
import org.apache.nifi.util.TestRunners;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TestEncryptContent {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TestEncryptContent.class);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRoundTrip() throws IOException {
|
||||
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent());
|
||||
|
@ -60,11 +75,109 @@ public class TestEncryptContent {
|
|||
testRunner.run();
|
||||
testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1);
|
||||
|
||||
logger.info("Successfully decrypted {}", method.name());
|
||||
|
||||
flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0);
|
||||
flowFile.assertContentEquals(new File("src/test/resources/hello.txt"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldDetermineMaxKeySizeForAlgorithms() throws IOException {
|
||||
// Arrange
|
||||
final String AES_ALGORITHM = EncryptionMethod.MD5_256AES.getAlgorithm();
|
||||
final String DES_ALGORITHM = EncryptionMethod.MD5_DES.getAlgorithm();
|
||||
|
||||
final int AES_MAX_LENGTH = PasswordBasedEncryptor.supportsUnlimitedStrength() ? Integer.MAX_VALUE : 128;
|
||||
final int DES_MAX_LENGTH = PasswordBasedEncryptor.supportsUnlimitedStrength() ? Integer.MAX_VALUE : 64;
|
||||
|
||||
// Act
|
||||
int determinedAESMaxLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(AES_ALGORITHM);
|
||||
int determinedTDESMaxLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(DES_ALGORITHM);
|
||||
|
||||
// Assert
|
||||
assert determinedAESMaxLength == AES_MAX_LENGTH;
|
||||
assert determinedTDESMaxLength == DES_MAX_LENGTH;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldDecryptOpenSSLRawSalted() throws IOException {
|
||||
// Arrange
|
||||
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
|
||||
PasswordBasedEncryptor.supportsUnlimitedStrength());
|
||||
|
||||
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent());
|
||||
|
||||
final String password = "thisIsABadPassword";
|
||||
final EncryptionMethod method = EncryptionMethod.MD5_256AES;
|
||||
final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY;
|
||||
|
||||
testRunner.setProperty(EncryptContent.PASSWORD, password);
|
||||
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, kdf.name());
|
||||
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, method.name());
|
||||
testRunner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE);
|
||||
|
||||
// Act
|
||||
testRunner.enqueue(Paths.get("src/test/resources/TestEncryptContent/salted_raw.enc"));
|
||||
testRunner.clearTransferState();
|
||||
testRunner.run();
|
||||
|
||||
// Assert
|
||||
testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1);
|
||||
testRunner.assertQueueEmpty();
|
||||
|
||||
MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0);
|
||||
logger.info("Decrypted contents (hex): {}", Hex.encodeHexString(flowFile.toByteArray()));
|
||||
logger.info("Decrypted contents: {}", new String(flowFile.toByteArray(), "UTF-8"));
|
||||
|
||||
// Assert
|
||||
flowFile.assertContentEquals(new File("src/test/resources/TestEncryptContent/plain.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldDecryptOpenSSLRawUnsalted() throws IOException {
|
||||
// Arrange
|
||||
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
|
||||
PasswordBasedEncryptor.supportsUnlimitedStrength());
|
||||
|
||||
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent());
|
||||
|
||||
final String password = "thisIsABadPassword";
|
||||
final EncryptionMethod method = EncryptionMethod.MD5_256AES;
|
||||
final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY;
|
||||
|
||||
testRunner.setProperty(EncryptContent.PASSWORD, password);
|
||||
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, kdf.name());
|
||||
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, method.name());
|
||||
testRunner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE);
|
||||
|
||||
// Act
|
||||
testRunner.enqueue(Paths.get("src/test/resources/TestEncryptContent/unsalted_raw.enc"));
|
||||
testRunner.clearTransferState();
|
||||
testRunner.run();
|
||||
|
||||
// Assert
|
||||
testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1);
|
||||
testRunner.assertQueueEmpty();
|
||||
|
||||
MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0);
|
||||
logger.info("Decrypted contents (hex): {}", Hex.encodeHexString(flowFile.toByteArray()));
|
||||
logger.info("Decrypted contents: {}", new String(flowFile.toByteArray(), "UTF-8"));
|
||||
|
||||
// Assert
|
||||
flowFile.assertContentEquals(new File("src/test/resources/TestEncryptContent/plain.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptShouldDefaultToLegacyKDF() throws IOException {
|
||||
// Arrange
|
||||
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent());
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals("Decrypt should default to Legacy KDF", testRunner.getProcessor().getPropertyDescriptor(EncryptContent.KEY_DERIVATION_FUNCTION
|
||||
.getName()).getDefaultValue(), KeyDerivationFunction.NIFI_LEGACY.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptSmallerThanSaltSize() {
|
||||
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
|
||||
|
@ -96,7 +209,6 @@ public class TestEncryptContent {
|
|||
Collection<ValidationResult> results;
|
||||
MockProcessContext pc;
|
||||
|
||||
results = new HashSet<>();
|
||||
runner.enqueue(new byte[0]);
|
||||
pc = (MockProcessContext) runner.getProcessContext();
|
||||
results = pc.validate();
|
||||
|
@ -106,7 +218,24 @@ public class TestEncryptContent {
|
|||
.contains(EncryptContent.PASSWORD.getDisplayName() + " is required when using algorithm"));
|
||||
}
|
||||
|
||||
results = new HashSet<>();
|
||||
runner.enqueue(new byte[0]);
|
||||
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.MD5_256AES.name());
|
||||
runner.setProperty(EncryptContent.PASSWORD, "ThisIsAPasswordThatIsLongerThanSixteenCharacters");
|
||||
pc = (MockProcessContext) runner.getProcessContext();
|
||||
results = pc.validate();
|
||||
if (!PasswordBasedEncryptor.supportsUnlimitedStrength()) {
|
||||
Assert.assertEquals(1, results.size());
|
||||
for (final ValidationResult vr : results) {
|
||||
Assert.assertTrue(
|
||||
"Did not successfully catch validation error of a long password in a non-JCE Unlimited Strength environment",
|
||||
vr.toString().contains("Password length greater than " + PasswordBasedEncryptor.getMaxAllowedKeyLength(EncryptionMethod.MD5_256AES.getAlgorithm())
|
||||
+ " bits is not supported by this JVM due to lacking JCE Unlimited Strength Jurisdiction Policy files."));
|
||||
}
|
||||
} else {
|
||||
Assert.assertEquals(0, results.size());
|
||||
}
|
||||
runner.removeProperty(EncryptContent.PASSWORD);
|
||||
|
||||
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
|
||||
runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/text.txt");
|
||||
runner.enqueue(new byte[0]);
|
||||
|
@ -120,7 +249,6 @@ public class TestEncryptContent {
|
|||
+ EncryptContent.PUBLIC_KEY_USERID.getDisplayName()));
|
||||
}
|
||||
|
||||
results = new HashSet<>();
|
||||
runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
|
||||
runner.enqueue(new byte[0]);
|
||||
pc = (MockProcessContext) runner.getProcessContext();
|
||||
|
@ -133,7 +261,6 @@ public class TestEncryptContent {
|
|||
runner.removeProperty(EncryptContent.PUBLIC_KEYRING);
|
||||
runner.removeProperty(EncryptContent.PUBLIC_KEY_USERID);
|
||||
|
||||
results = new HashSet<>();
|
||||
runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE);
|
||||
runner.setProperty(EncryptContent.PRIVATE_KEYRING, "src/test/resources/TestEncryptContent/text.txt");
|
||||
runner.enqueue(new byte[0]);
|
||||
|
@ -148,7 +275,6 @@ public class TestEncryptContent {
|
|||
|
||||
}
|
||||
|
||||
results = new HashSet<>();
|
||||
runner.setProperty(EncryptContent.PRIVATE_KEYRING_PASSPHRASE, "PASSWORD");
|
||||
runner.enqueue(new byte[0]);
|
||||
pc = (MockProcessContext) runner.getProcessContext();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
This is a plaintext message.
|
|
@ -0,0 +1 @@
|
|||
Salted__1Ü0l{ŠakÆ]÷gPMÀ…ºzÅÐËx2!õs¶ñÞÒ<C39E>Yˆè<08>
|
Binary file not shown.
Loading…
Reference in New Issue