[ https://issues.apache.org/jira/browse/NIFI-2961?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15732956#comment-15732956 ]
ASF GitHub Bot commented on NIFI-2961: -------------------------------------- Github user alopresto commented on a diff in the pull request: https://github.com/apache/nifi/pull/1294#discussion_r91572688 --- Diff: nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptAttributes.java --- @@ -0,0 +1,611 @@ +/* + * 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.processors.standard; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +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.SideEffectFree; +import org.apache.nifi.annotation.behavior.SupportsBatching; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.ProcessorInitializationContext; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.processors.standard.util.crypto.CipherUtility; +import org.apache.nifi.processors.standard.util.crypto.KeyedEncryptor; +import org.apache.nifi.processors.standard.util.crypto.OpenPGPKeyBasedEncryptor; +import org.apache.nifi.processors.standard.util.crypto.OpenPGPPasswordBasedEncryptor; +import org.apache.nifi.processors.standard.util.crypto.PasswordBasedEncryptor; +import org.apache.nifi.security.util.EncryptionMethod; +import org.apache.nifi.security.util.KeyDerivationFunction; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Provides functionality of encrypting attributes with various algorithms. + * Note. It'll not modify filename or uuid as they are sensitive and are + * internally used by either Algorithm itself or FlowFile repo. + */ +@EventDriven +@SideEffectFree +@SupportsBatching +@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) +@Tags({"encryption", "decryption", "password", "JCE", "OpenPGP", "PGP", "GPG"}) +@CapabilityDescription("Encrypts or Decrypts a FlowFile attributes using either symmetric encryption with a password " + + "and randomly generated salt, or asymmetric encryption using a public and secret key.") +public class EncryptAttributes extends AbstractProcessor { + + public static final String ENCRYPT_MODE = "Encrypt"; + public static final String DECRYPT_MODE = "Decrypt"; + + public static final String WEAK_CRYPTO_ALLOWED_NAME = "allowed"; + public static final String WEAK_CRYPTO_NOT_ALLOWED_NAME = "not-allowed"; + + public static final PropertyDescriptor ATTRIBUTES_TO_ENCRYPT = new PropertyDescriptor.Builder() + .name("Attributes to encrypt") + .description("Comma separated list of attributes to encrypt, if empty then it'll encrypt all the " + + "attributes including CoreAttributes EXCEPT filename and uuid. " + + "This list is case sensitive and if attribute is not found " + + "then the value will be ignored. " ) + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + 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(); + 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(buildKeyDerivationFunctionAllowableValues()) + .defaultValue(KeyDerivationFunction.BCRYPT.name()) + .build(); + public static final PropertyDescriptor ENCRYPTION_ALGORITHM = new PropertyDescriptor.Builder() + .name("Encryption Algorithm") + .description("The Encryption Algorithm to use") + .required(true) + .allowableValues(buildEncryptionMethodAllowableValues()) + .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(); + 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(); + 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(); + 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(); + 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(); + public static final PropertyDescriptor RAW_KEY_HEX = new PropertyDescriptor.Builder() + .name("raw-key-hex") + .displayName("Raw Key (hexadecimal)") + .description("In keyed encryption, this is the raw key, encoded in hexadecimal") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .sensitive(true) + .build(); + public static final PropertyDescriptor ALLOW_WEAK_CRYPTO = new PropertyDescriptor.Builder() + .name("allow-weak-crypto") + .displayName("Allow insecure cryptographic modes") + .description("Overrides the default behavior to prevent unsafe combinations of encryption algorithms and short passwords on JVMs with limited strength cryptographic jurisdiction policies") + .required(true) + .allowableValues(buildWeakCryptoAllowableValues()) + .defaultValue(buildDefaultWeakCryptoAllowableValue().getValue()) + .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(); + + 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(); + private List<PropertyDescriptor> properties; + + private Set<Relationship> relationships; + + static { + // add BouncyCastle encryption providers + Security.addProvider(new BouncyCastleProvider()); + } + + private static AllowableValue[] buildKeyDerivationFunctionAllowableValues() { + final KeyDerivationFunction[] keyDerivationFunctions = KeyDerivationFunction.values(); + List<AllowableValue> allowableValues = new ArrayList<>(keyDerivationFunctions.length); + for (KeyDerivationFunction kdf : keyDerivationFunctions) { + allowableValues.add(new AllowableValue(kdf.name(), kdf.getName(), kdf.getDescription())); + } + + return allowableValues.toArray(new AllowableValue[0]); + } + + private static AllowableValue[] buildEncryptionMethodAllowableValues() { + final EncryptionMethod[] encryptionMethods = EncryptionMethod.values(); + List<AllowableValue> allowableValues = new ArrayList<>(encryptionMethods.length); + for (EncryptionMethod em : encryptionMethods) { + allowableValues.add(new AllowableValue(em.name(), em.name(), em.toString())); + } + + return allowableValues.toArray(new AllowableValue[0]); + } + + private static AllowableValue[] buildWeakCryptoAllowableValues() { + List<AllowableValue> allowableValues = new ArrayList<>(); + allowableValues.add(new AllowableValue(WEAK_CRYPTO_ALLOWED_NAME, "Allowed", "Operation will not be blocked and no alerts will be presented " + + "when unsafe combinations of encryption algorithms and passwords are provided")); + allowableValues.add(buildDefaultWeakCryptoAllowableValue()); + return allowableValues.toArray(new AllowableValue[0]); + } + + private static AllowableValue buildDefaultWeakCryptoAllowableValue() { + return new AllowableValue(WEAK_CRYPTO_NOT_ALLOWED_NAME, "Not Allowed", "When set, operation will be blocked and alerts will be presented to the user " + + "if unsafe combinations of encryption algorithms and passwords are provided on a JVM with limited strength crypto. To fix this, see the Admin Guide."); + } + + @Override + protected void init(final ProcessorInitializationContext context) { + final List<PropertyDescriptor> properties = new ArrayList<>(); + properties.add(ATTRIBUTES_TO_ENCRYPT); + properties.add(MODE); + properties.add(KEY_DERIVATION_FUNCTION); + properties.add(ENCRYPTION_ALGORITHM); + properties.add(ALLOW_WEAK_CRYPTO); + properties.add(PASSWORD); + properties.add(RAW_KEY_HEX); + properties.add(PUBLIC_KEYRING); + properties.add(PUBLIC_KEY_USERID); + properties.add(PRIVATE_KEYRING); + properties.add(PRIVATE_KEYRING_PASSPHRASE); + this.properties = Collections.unmodifiableList(properties); + + final Set<Relationship> relationships = new HashSet<>(); + relationships.add(REL_SUCCESS); + relationships.add(REL_FAILURE); + this.relationships = Collections.unmodifiableSet(relationships); + } + + @Override + public Set<Relationship> getRelationships() { + return relationships; + } + + @Override + protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { + return properties; + } + + public static boolean isPGPAlgorithm(final String algorithm) { + return algorithm.startsWith("PGP"); + } + + public static boolean isPGPArmoredAlgorithm(final String algorithm) { + return isPGPAlgorithm(algorithm) && algorithm.endsWith("ASCII-ARMOR"); + } + + @Override + protected Collection<ValidationResult> customValidate(final ValidationContext context) { + final List<ValidationResult> validationResults = new ArrayList<>(super.customValidate(context)); + final String methodValue = context.getProperty(ENCRYPTION_ALGORITHM).getValue(); + final EncryptionMethod encryptionMethod = EncryptionMethod.valueOf(methodValue); + final String algorithm = encryptionMethod.getAlgorithm(); + final String password = context.getProperty(PASSWORD).getValue(); + final KeyDerivationFunction kdf = KeyDerivationFunction.valueOf(context.getProperty(KEY_DERIVATION_FUNCTION).getValue()); + final String keyHex = context.getProperty(RAW_KEY_HEX).getValue(); + if (isPGPAlgorithm(algorithm)) { + final boolean encrypt = context.getProperty(MODE).getValue().equalsIgnoreCase(ENCRYPT_MODE); + final String publicKeyring = context.getProperty(PUBLIC_KEYRING).getValue(); + final String publicUserId = context.getProperty(PUBLIC_KEY_USERID).getValue(); + final String privateKeyring = context.getProperty(PRIVATE_KEYRING).getValue(); + final String privateKeyringPassphrase = context.getProperty(PRIVATE_KEYRING_PASSPHRASE).getValue(); + validationResults.addAll(validatePGP(encryptionMethod, password, encrypt, publicKeyring, publicUserId, privateKeyring, privateKeyringPassphrase)); + } else { // Not PGP + if (encryptionMethod.isKeyedCipher()) { // Raw key + validationResults.addAll(validateKeyed(encryptionMethod, kdf, keyHex)); + } else { // PBE + boolean allowWeakCrypto = context.getProperty(ALLOW_WEAK_CRYPTO).getValue().equalsIgnoreCase(WEAK_CRYPTO_ALLOWED_NAME); + validationResults.addAll(validatePBE(encryptionMethod, kdf, password, allowWeakCrypto)); + } + } + return validationResults; + } + + private List<ValidationResult> validatePGP(EncryptionMethod encryptionMethod, String password, boolean encrypt, String publicKeyring, String publicUserId, String privateKeyring, + String privateKeyringPassphrase) { + List<ValidationResult> validationResults = new ArrayList<>(); + + if (password == null) { + if (encrypt) { + // If encrypting without a password, require both public-keyring-file and public-key-user-id + if (publicKeyring == null || publicUserId == null) { + validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName()) + .explanation(encryptionMethod.getAlgorithm() + " 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()); + } + } catch (final Exception e) { + validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName()) + .explanation("Invalid " + PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring + + " because " + e.toString()) + .build()); + } + } + } else { // Decrypt + // Require both private-keyring-file and private-keyring-passphrase + if (privateKeyring == null || privateKeyringPassphrase == null) { + validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getName()) + .explanation(encryptionMethod.getAlgorithm() + " decryption without a " + PASSWORD.getDisplayName() + " requires both " + + PRIVATE_KEYRING.getDisplayName() + " and " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName()) + .build()); + } else { + final String providerName = encryptionMethod.getProvider(); + // Verify the passphrase works on the private keyring + try { + if (!OpenPGPKeyBasedEncryptor.validateKeyring(providerName, privateKeyring, privateKeyringPassphrase.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()); + } + } catch (final Exception e) { + validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName()) + .explanation("Invalid " + PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring + + " because " + e.toString()) + .build()); + } + } + } + } + + return validationResults; + } + + private List<ValidationResult> validatePBE(EncryptionMethod encryptionMethod, KeyDerivationFunction kdf, String password, boolean allowWeakCrypto) { + List<ValidationResult> validationResults = new ArrayList<>(); + boolean limitedStrengthCrypto = !PasswordBasedEncryptor.supportsUnlimitedStrength(); + + // Password required (short circuits validation because other conditions depend on password presence) + if (StringUtils.isEmpty(password)) { + validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName()) + .explanation(PASSWORD.getDisplayName() + " is required when using algorithm " + encryptionMethod.getAlgorithm()).build()); + return validationResults; + } + + // If weak crypto is not explicitly allowed via override, check the password length and algorithm + final int passwordBytesLength = password.getBytes(StandardCharsets.UTF_8).length; + if (!allowWeakCrypto) { + final int minimumSafePasswordLength = PasswordBasedEncryptor.getMinimumSafePasswordLength(); + if (passwordBytesLength < minimumSafePasswordLength) { + validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName()) + .explanation("Password length less than " + minimumSafePasswordLength + " characters is potentially unsafe. See Admin Guide.").build()); + } + } + + // Multiple checks on machine with limited strength crypto + if (limitedStrengthCrypto) { + // Cannot use unlimited strength ciphers on machine that lacks policies + if (encryptionMethod.isUnlimitedStrength()) { + validationResults.add(new ValidationResult.Builder().subject(ENCRYPTION_ALGORITHM.getName()) + .explanation(encryptionMethod.name() + " (" + encryptionMethod.getAlgorithm() + ") is not supported by this JVM due to lacking JCE Unlimited " + + "Strength Jurisdiction Policy files. See Admin Guide.").build()); + } + + // Check if the password exceeds the limit + final boolean passwordLongerThanLimit = !CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(passwordBytesLength, encryptionMethod); + if (passwordLongerThanLimit) { + int maxPasswordLength = CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod); + validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName()) + .explanation("Password length greater than " + maxPasswordLength + " characters is not supported by this JVM" + + " due to lacking JCE Unlimited Strength Jurisdiction Policy files. See Admin Guide.").build()); + } + } + + // Check the KDF for compatibility with this algorithm + List<String> kdfsForPBECipher = getKDFsForPBECipher(encryptionMethod); + if (kdf == null || !kdfsForPBECipher.contains(kdf.name())) { + final String displayName = KEY_DERIVATION_FUNCTION.getDisplayName(); + validationResults.add(new ValidationResult.Builder().subject(displayName) + .explanation(displayName + " is required to be " + StringUtils.join(kdfsForPBECipher, + ", ") + " when using algorithm " + encryptionMethod.getAlgorithm() + ". See Admin Guide.").build()); + } + + return validationResults; + } + + private List<ValidationResult> validateKeyed(EncryptionMethod encryptionMethod, KeyDerivationFunction kdf, String keyHex) { + List<ValidationResult> validationResults = new ArrayList<>(); + boolean limitedStrengthCrypto = !PasswordBasedEncryptor.supportsUnlimitedStrength(); + + if (limitedStrengthCrypto) { + if (encryptionMethod.isUnlimitedStrength()) { + validationResults.add(new ValidationResult.Builder().subject(ENCRYPTION_ALGORITHM.getName()) + .explanation(encryptionMethod.name() + " (" + encryptionMethod.getAlgorithm() + ") is not supported by this JVM due to lacking JCE Unlimited " + + "Strength Jurisdiction Policy files. See Admin Guide.").build()); + } + } + int allowedKeyLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(ENCRYPTION_ALGORITHM.getName()); + + if (StringUtils.isEmpty(keyHex)) { + validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName()) + .explanation(RAW_KEY_HEX.getDisplayName() + " is required when using algorithm " + encryptionMethod.getAlgorithm() + ". See Admin Guide.").build()); + } else { + byte[] keyBytes = new byte[0]; + try { + keyBytes = Hex.decodeHex(keyHex.toCharArray()); + } catch (DecoderException e) { + validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName()) + .explanation("Key must be valid hexadecimal string. See Admin Guide.").build()); + } + if (keyBytes.length * 8 > allowedKeyLength) { + validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName()) + .explanation("Key length greater than " + allowedKeyLength + " bits is not supported by this JVM" + + " due to lacking JCE Unlimited Strength Jurisdiction Policy files. See Admin Guide.").build()); + } + if (!CipherUtility.isValidKeyLengthForAlgorithm(keyBytes.length * 8, encryptionMethod.getAlgorithm())) { + List<Integer> validKeyLengths = CipherUtility.getValidKeyLengthsForAlgorithm(encryptionMethod.getAlgorithm()); + validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName()) + .explanation("Key must be valid length [" + StringUtils.join(validKeyLengths, ", ") + "]. See Admin Guide.").build()); + } + } + + // Perform some analysis on the selected encryption algorithm to ensure the JVM can support it and the associated key + + List<String> kdfsForKeyedCipher = getKDFsForKeyedCipher(); + if (kdf == null || !kdfsForKeyedCipher.contains(kdf.name())) { + validationResults.add(new ValidationResult.Builder().subject(KEY_DERIVATION_FUNCTION.getName()) + .explanation(KEY_DERIVATION_FUNCTION.getDisplayName() + " is required to be " + StringUtils.join(kdfsForKeyedCipher, ", ") + " when using algorithm " + + encryptionMethod.getAlgorithm()).build()); + } + + return validationResults; + } + + private List<String> getKDFsForKeyedCipher() { + List<String> kdfsForKeyedCipher = new ArrayList<>(); + kdfsForKeyedCipher.add(KeyDerivationFunction.NONE.name()); + for (KeyDerivationFunction k : KeyDerivationFunction.values()) { + if (k.isStrongKDF()) { + kdfsForKeyedCipher.add(k.name()); + } + } + return kdfsForKeyedCipher; + } + + private List<String> getKDFsForPBECipher(EncryptionMethod encryptionMethod) { + List<String> kdfsForPBECipher = new ArrayList<>(); + for (KeyDerivationFunction k : KeyDerivationFunction.values()) { + // Add all weak (legacy) KDFs except NONE + if (!k.isStrongKDF() && !k.equals(KeyDerivationFunction.NONE)) { + kdfsForPBECipher.add(k.name()); + // If this algorithm supports strong KDFs, add them as well + } else if ((encryptionMethod.isCompatibleWithStrongKDFs() && k.isStrongKDF())) { + kdfsForPBECipher.add(k.name()); + } + } + return kdfsForPBECipher; + } + + private Map<String, String> buildNewAttributes(FlowFile file, String atrList, + EncryptContent.Encryptor encryptor, + boolean isToBeEncrypted) throws Exception { + + Map<String, String> oldAttributes = file.getAttributes(); + Map<String, String> atrToWrite = new HashMap<>(); + final String filenameAttr = CoreAttributes.FILENAME.key(); + final String uuidAttr = CoreAttributes.UUID.key(); + + if (!StringUtils.isEmpty(atrList)) { + //TODO: check for spaces also + // Traverse comma separated list if provided + String[] attrs = atrList.split(","); + Set<String> atrSet = new HashSet<>(Arrays.asList(attrs)); + for (String atr : atrSet) { + if (oldAttributes.containsKey(atr) && !atr.equals(filenameAttr) && !atr.equals(uuidAttr)) { + String atrValue = oldAttributes.get(atr); + String newAtrVal = (isToBeEncrypted) ? EncryptString.performEncryption(atrValue, encryptor) : EncryptString.performDecryption(atrValue, encryptor); + atrToWrite.put(atr, newAtrVal); + } + } + } else { + //TODO: filename encryption check only for PGP algos. + // encrypt all attributes except filename and uuid. + for (String atr : oldAttributes.keySet()) { + if (!atr.equals(filenameAttr) && !atr.equals(uuidAttr)) { + String atrValue = oldAttributes.get(atr); + String newAtrVal = (isToBeEncrypted) ? EncryptString.performEncryption(atrValue, encryptor) : EncryptString.performDecryption(atrValue, encryptor); + atrToWrite.put(atr, newAtrVal); + } + } + } + return atrToWrite; + } + + @Override + public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { + + FlowFile flowFile = session.get(); + if (flowFile == null) { + return; + } + + final ComponentLog logger = getLogger(); + final String atrList = context.getProperty(ATTRIBUTES_TO_ENCRYPT).getValue(); + final String method = context.getProperty(ENCRYPTION_ALGORITHM).getValue(); + final EncryptionMethod encryptionMethod = EncryptionMethod.valueOf(method); + 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); + + EncryptContent.Encryptor encryptor; + Map<String, String> newAtrList; + + try { --- End diff -- It could be called `EncryptionProcessorUtils` or something similar. Yes, it needs separate unit tests, but this should be easy as there is existing application code which exercises the methods, and the methods should be specific and contained enough to quickly capture the positive/negative cases. > Create EncryptAttribute processor > --------------------------------- > > Key: NIFI-2961 > URL: https://issues.apache.org/jira/browse/NIFI-2961 > Project: Apache NiFi > Issue Type: Improvement > Components: Extensions > Affects Versions: 1.0.0 > Reporter: Andy LoPresto > Labels: attributes, encryption, security > > Similar to {{EncryptContent}}, the {{EncryptAttribute}} processor would allow > individual (and multiple) flowfile attributes to be encrypted (either > in-place or to a new attribute key) with various encryption algorithms (AES, > RSA, PBE, and PGP). > Specific compatibility with the {{OpenSSL EVP_BytesToKey}}, {{PBKDF2}}, > {{scrypt}}, and {{bcrypt}} key derivation functions should be included. > The processor should provide the boolean option to encrypt or decrypt (only > one operation per instance of the processor). The processor should also allow > Base64 encoding (aka ASCII armor) for the encrypted attributes to prevent > byte escaping/data loss. > If [dangerous processor > annotations|https://cwiki.apache.org/confluence/display/NIFI/Security+Feature+Roadmap] > are introduced, this processor should be marked as such and the > corresponding attribute protection (i.e. provenance before/after, etc.) > should be applied. > Originally requested in this [Stack Overflow > question|https://stackoverflow.com/questions/40294945/nifi-encrypt-json]. -- This message was sent by Atlassian JIRA (v6.3.4#6332)