From f83e6d33c592e3a061b0bf6c41884c42f4971379 Mon Sep 17 00:00:00 2001 From: Andy LoPresto Date: Thu, 3 Dec 2015 12:23:45 -0800 Subject: [PATCH] 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 --- .../nifi/security/util/EncryptionMethod.java | 24 +-- .../security/util/KeyDerivationFunction.java | 55 +++++ .../nifi-standard-processors/pom.xml | 3 + .../processors/standard/EncryptContent.java | 192 +++++++++++------- .../standard/util/PasswordBasedEncryptor.java | 133 +++++++++++- .../standard/TestEncryptContent.java | 142 ++++++++++++- .../resources/TestEncryptContent/plain.txt | 1 + .../TestEncryptContent/salted_raw.enc | 1 + .../TestEncryptContent/unsalted_raw.enc | Bin 0 -> 32 bytes 9 files changed, 443 insertions(+), 108 deletions(-) create mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/plain.txt create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/salted_raw.enc create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/unsalted_raw.enc diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java index 55f5986e4b..ce0c50ee84 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java @@ -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); diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java new file mode 100644 index 0000000000..f8b4731c41 --- /dev/null +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyDerivationFunction.java @@ -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(); + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml index 0427927ba7..64c2597ee8 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml @@ -310,6 +310,9 @@ language governing permissions and limitations under the License. --> src/test/resources/TestIdentifyMimeType/flowfilev1.tar src/test/resources/TestUnpackContent/data.tar src/test/resources/TestUnpackContent/data.zip + src/test/resources/TestEncryptContent/plain.txt + src/test/resources/TestEncryptContent/salted_raw.enc + src/test/resources/TestEncryptContent/unsalted_raw.enc diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java index c63edc91b8..50397c8544 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java @@ -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 properties; private Set relationships; @@ -133,6 +143,7 @@ public class EncryptContent extends AbstractProcessor { protected void init(final ProcessorInitializationContext context) { final List 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 customValidate(final ValidationContext context) { final List 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; } } \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java index 1f45d6f504..d3d50b8cbf 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java @@ -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 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]; diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java index 8e7bc05d41..ee21a50c8e 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java @@ -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 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(); diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/plain.txt b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/plain.txt new file mode 100644 index 0000000000..aae2266a1b --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/plain.txt @@ -0,0 +1 @@ +This is a plaintext message. \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/salted_raw.enc b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/salted_raw.enc new file mode 100644 index 0000000000..8dad319d8d --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/salted_raw.enc @@ -0,0 +1 @@ +Salted__10l{ ak]gPMzx2!sҏY \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/unsalted_raw.enc b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/unsalted_raw.enc new file mode 100644 index 0000000000000000000000000000000000000000..9b0c75aef3e86878607ca8e21d4d8e0d63827285 GIT binary patch literal 32 ocmXRotJ(5*H($*G=_0m{joATlub+H&Fe|raICr