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:
Andy LoPresto 2015-12-03 12:23:45 -08:00 committed by Aldrin Piri
parent ee14d8f9dd
commit f83e6d33c5
9 changed files with 443 additions and 108 deletions

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
@ -66,60 +68,68 @@ public class EncryptContent extends AbstractProcessor {
public static final String DECRYPT_MODE = "Decrypt";
public static final PropertyDescriptor MODE = new PropertyDescriptor.Builder()
.name("Mode")
.description("Specifies whether the content should be encrypted or decrypted")
.required(true)
.allowableValues(ENCRYPT_MODE, DECRYPT_MODE)
.defaultValue(ENCRYPT_MODE)
.build();
.name("Mode")
.description("Specifies whether the content should be encrypted or decrypted")
.required(true)
.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())
.build();
.name("Encryption Algorithm")
.description("The Encryption Algorithm to use")
.required(true)
.allowableValues(EncryptionMethod.values())
.defaultValue(EncryptionMethod.MD5_128AES.name())
.build();
public static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder()
.name("Password")
.description("The Password to use for encrypting or decrypting the data")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
.name("Password")
.description("The Password to use for encrypting or decrypting the data")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
public static final PropertyDescriptor PUBLIC_KEYRING = new PropertyDescriptor.Builder()
.name("public-keyring-file")
.displayName("Public Keyring File")
.description("In a PGP encrypt mode, this keyring contains the public key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
.name("public-keyring-file")
.displayName("Public Keyring File")
.description("In a PGP encrypt mode, this keyring contains the public key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor PUBLIC_KEY_USERID = new PropertyDescriptor.Builder()
.name("public-key-user-id")
.displayName("Public Key User Id")
.description("In a PGP encrypt mode, this user id of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
.name("public-key-user-id")
.displayName("Public Key User Id")
.description("In a PGP encrypt mode, this user id of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor PRIVATE_KEYRING = new PropertyDescriptor.Builder()
.name("private-keyring-file")
.displayName("Private Keyring File")
.description("In a PGP decrypt mode, this keyring contains the private key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
.name("private-keyring-file")
.displayName("Private Keyring File")
.description("In a PGP decrypt mode, this keyring contains the private key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor PRIVATE_KEYRING_PASSPHRASE = new PropertyDescriptor.Builder()
.name("private-keyring-passphrase")
.displayName("Private Keyring Passphrase")
.description("In a PGP decrypt mode, this is the private keyring passphrase")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
.name("private-keyring-passphrase")
.displayName("Private Keyring Passphrase")
.description("In a PGP decrypt mode, this is the private keyring passphrase")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
public static final Relationship REL_SUCCESS = new Relationship.Builder().name("success")
.description("Any FlowFile that is successfully encrypted or decrypted will be routed to success").build();
.description("Any FlowFile that is successfully encrypted or decrypted will be routed to success").build();
public static final Relationship REL_FAILURE = new Relationship.Builder().name("failure")
.description("Any FlowFile that cannot be encrypted or decrypted will be routed to failure").build();
.description("Any FlowFile that cannot be encrypted or decrypted will be routed to failure").build();
private List<PropertyDescriptor> properties;
private Set<Relationship> relationships;
@ -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);
@ -180,23 +193,23 @@ public class EncryptContent extends AbstractProcessor {
final String publicUserId = context.getProperty(PUBLIC_KEY_USERID).getValue();
if (publicKeyring == null || publicUserId == null) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation(algorithm + " encryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PUBLIC_KEYRING.getDisplayName() + " and " + PUBLIC_KEY_USERID.getDisplayName())
.build());
.explanation(algorithm + " encryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PUBLIC_KEYRING.getDisplayName() + " and " + PUBLIC_KEY_USERID.getDisplayName())
.build());
} else {
// verify the public keyring contains the user id
try {
if (OpenPGPKeyBasedEncryptor.getPublicKey(publicUserId, publicKeyring) == null) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation(PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " does not contain user id " + publicUserId)
.build());
.explanation(PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " does not contain user id " + publicUserId)
.build());
}
} catch (final Exception e) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation("Invalid " + PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " because " + e.toString())
.build());
.explanation("Invalid " + PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " because " + e.toString())
.build());
}
}
} else {
@ -205,31 +218,55 @@ public class EncryptContent extends AbstractProcessor {
final String keyringPassphrase = context.getProperty(PRIVATE_KEYRING_PASSPHRASE).getValue();
if (privateKeyring == null || keyringPassphrase == null) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getName())
.explanation(algorithm + " decryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PRIVATE_KEYRING.getDisplayName() + " and " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
.explanation(algorithm + " decryption without a " + PASSWORD.getDisplayName() + " requires both "
+ 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())) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName())
.explanation(PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " could not be opened with the provided " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
.explanation(PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " could not be opened with the provided " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
}
} catch (final Exception e) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName())
.explanation("Invalid " + PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " because " + e.toString())
.build());
.explanation("Invalid " + PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " because " + e.toString())
.build());
}
}
}
}
} else if (password == null) {
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
} 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;
@ -262,14 +300,14 @@ public class EncryptContent extends AbstractProcessor {
} else if (!encrypt && privateKeyring != null) {
final char[] keyringPassphrase = context.getProperty(PRIVATE_KEYRING_PASSPHRASE).getValue().toCharArray();
encryptor = new OpenPGPKeyBasedEncryptor(algorithm, providerName, privateKeyring, null, keyringPassphrase,
filename);
filename);
} else {
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) {
@ -279,7 +317,7 @@ public class EncryptContent extends AbstractProcessor {
}
} catch (final Exception e) {
logger.error("Failed to initialize {}cryption algorithm because - ", new Object[] { encrypt ? "en" : "de", e });
logger.error("Failed to initialize {}cryption algorithm because - ", new Object[]{encrypt ? "en" : "de", e});
session.rollback();
context.yield();
return;
@ -288,20 +326,20 @@ public class EncryptContent extends AbstractProcessor {
try {
final StopWatch stopWatch = new StopWatch(true);
flowFile = session.write(flowFile, callback);
logger.info("successfully {}crypted {}", new Object[] { encrypt ? "en" : "de", flowFile });
logger.info("successfully {}crypted {}", new Object[]{encrypt ? "en" : "de", flowFile});
session.getProvenanceReporter().modifyContent(flowFile, stopWatch.getElapsed(TimeUnit.MILLISECONDS));
session.transfer(flowFile, REL_SUCCESS);
} catch (final ProcessException e) {
logger.error("Cannot {}crypt {} - ", new Object[] { encrypt ? "en" : "de", flowFile, e });
logger.error("Cannot {}crypt {} - ", new Object[]{encrypt ? "en" : "de", flowFile, e});
session.transfer(flowFile, REL_FAILURE);
return;
}
}
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;
}
}

View File

@ -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);
int algorithmBlockSize = cipher.getBlockSize();
this.saltSize = (algorithmBlockSize > 0) ? algorithmBlockSize : DEFAULT_SALT_SIZE;
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];

View File

@ -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]);
@ -143,12 +270,11 @@ public class TestEncryptContent {
for (final ValidationResult vr : results) {
Assert.assertTrue(vr.toString().contains(
" decryption without a " + EncryptContent.PASSWORD.getDisplayName() + " requires both "
+ EncryptContent.PRIVATE_KEYRING.getDisplayName() + " and "
+ EncryptContent.PRIVATE_KEYRING_PASSPHRASE.getDisplayName()));
+ EncryptContent.PRIVATE_KEYRING.getDisplayName() + " and "
+ EncryptContent.PRIVATE_KEYRING_PASSPHRASE.getDisplayName()));
}
results = new HashSet<>();
runner.setProperty(EncryptContent.PRIVATE_KEYRING_PASSPHRASE, "PASSWORD");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();

View File

@ -0,0 +1 @@
Salted__1Ü0l{Š akÆ]÷gPMÀ…ºzÅÐËx2!õs¶ñÞÒ<C39E>Yˆè<08>