exceptionfactory commented on a change in pull request #4842: URL: https://github.com/apache/nifi/pull/4842#discussion_r607378275
########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-processors/src/main/java/org/apache/nifi/processors/pgp/DecryptContentPGP.java ########## @@ -0,0 +1,386 @@ +/* + * 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.pgp; + +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.PropertyValue; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.pgp.service.api.PGPPrivateKeyService; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.io.StreamCallback; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.processors.pgp.exception.PGPDecryptionException; +import org.apache.nifi.processors.pgp.exception.PGPProcessException; +import org.apache.nifi.stream.io.StreamUtils; + +import org.apache.nifi.util.StringUtils; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Decrypt Content using Open Pretty Good Privacy decryption methods + */ +@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) +@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "RFC 4880"}) +@CapabilityDescription("Decrypt Contents of OpenPGP Messages") +@WritesAttributes({ + @WritesAttribute(attribute = PGPAttributeKey.LITERAL_DATA_FILENAME, description = "Filename from decrypted Literal Data"), + @WritesAttribute(attribute = PGPAttributeKey.LITERAL_DATA_MODIFIED, description = "Modified Date from decrypted Literal Data"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, description = "Symmetric-Key Algorithm Identifier") +}) +public class DecryptContentPGP extends AbstractProcessor { + + public static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .description("Decryption Succeeded") + .build(); + + public static final Relationship FAILURE = new Relationship.Builder() + .name("failure") + .description("Decryption Failed") + .build(); + + public static final PropertyDescriptor PASSPHRASE = new PropertyDescriptor.Builder() + .name("passphrase") + .displayName("Passphrase") + .description("Passphrase used for decrypting data encrypted with Password-Based Encryption") + .sensitive(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor PRIVATE_KEY_SERVICE = new PropertyDescriptor.Builder() + .name("private-key-service") + .displayName("Private Key Service") + .description("PGP Private Key Service for decrypting data encrypted with Public Key Encryption") + .identifiesControllerService(PGPPrivateKeyService.class) + .build(); + + private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(Arrays.asList(SUCCESS, FAILURE)); + + private static final List<PropertyDescriptor> DESCRIPTORS = Arrays.asList( + PASSPHRASE, + PRIVATE_KEY_SERVICE + ); + + private static final String PASSWORD_BASED_ENCRYPTION = "Password-Based Encryption"; + + private static final String PUBLIC_KEY_ENCRYPTION = "Public Key Encryption"; + + /** + * Get Relationships + * + * @return Processor Relationships + */ + @Override + public Set<Relationship> getRelationships() { + return RELATIONSHIPS; + } + + /** + * Get Supported Property Descriptors + * + * @return Processor Supported Property Descriptors + */ + @Override + public final List<PropertyDescriptor> getSupportedPropertyDescriptors() { + return DESCRIPTORS; + } + + /** + * On Trigger decrypts Flow File contents using configured properties + * + * @param context Process Context + * @param session Process Session + */ + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) { + FlowFile flowFile = session.get(); + if (flowFile == null) { + return; + } + + final char[] passphrase = getPassphrase(context); + final PGPPrivateKeyService privateKeyService = getPrivateKeyService(context); + final DecryptStreamCallback callback = new DecryptStreamCallback(passphrase, privateKeyService); + + try { + flowFile = session.write(flowFile, callback); + flowFile = session.putAllAttributes(flowFile, callback.attributes); + session.transfer(flowFile, SUCCESS); + } catch (final RuntimeException e) { + getLogger().error("Decryption Failed {}", flowFile, e); + session.transfer(flowFile, FAILURE); + } + } + + /** + * Custom Validate requires at least one decryption property to be configured + * + * @param context Validation Context + * @return Collection of Validation Results + */ + @Override + protected Collection<ValidationResult> customValidate(final ValidationContext context) { + final Collection<ValidationResult> results = new ArrayList<>(); + + final String passphrase = context.getProperty(PASSPHRASE).getValue(); + if (StringUtils.isBlank(passphrase)) { + final PGPPrivateKeyService privateKeyService = context.getProperty(PRIVATE_KEY_SERVICE).asControllerService(PGPPrivateKeyService.class); + if (privateKeyService == null) { + final String explanation = String.format("Neither [%s] nor [%s] configured", PASSPHRASE.getDisplayName(), PRIVATE_KEY_SERVICE.getDisplayName()); + final ValidationResult result = new ValidationResult.Builder() + .valid(false) + .subject(getClass().getSimpleName()) + .explanation(explanation) + .build(); + results.add(result); + } + } + + return results; + } + + private char[] getPassphrase(final ProcessContext context) { + char[] passphrase = null; + final PropertyValue passphraseProperty = context.getProperty(PASSPHRASE); + if (passphraseProperty.isSet()) { + passphrase = passphraseProperty.getValue().toCharArray(); + } + return passphrase; + } + + private PGPPrivateKeyService getPrivateKeyService(final ProcessContext context) { + PGPPrivateKeyService privateKeyService = null; + final PropertyValue privateKeyServiceProperty = context.getProperty(PRIVATE_KEY_SERVICE); + if (privateKeyServiceProperty.isSet()) { + privateKeyService = privateKeyServiceProperty.asControllerService(PGPPrivateKeyService.class); + } + return privateKeyService; + } + + private class DecryptStreamCallback implements StreamCallback { + private final char[] passphrase; + + private final PGPPrivateKeyService privateKeyService; + + private final Map<String, String> attributes = new HashMap<>(); + + public DecryptStreamCallback(final char[] passphrase, final PGPPrivateKeyService privateKeyService) { + this.passphrase = passphrase; + this.privateKeyService = privateKeyService; + } + + /** + * Process Input Stream containing encrypted data and write decrypted contents to Output Stream + * + * @param inputStream Input Stream containing encrypted data + * @param outputStream Output Stream for decrypted contents + * @throws IOException Thrown when unable to read or write streams + */ + @Override + public void process(final InputStream inputStream, final OutputStream outputStream) throws IOException { + final InputStream decoderInputStream = PGPUtil.getDecoderStream(inputStream); + final PGPEncryptedDataList encryptedDataList = getEncryptedDataList(decoderInputStream); + + for (final PGPEncryptedData encryptedData : encryptedDataList) { + final PGPLiteralData literalData = getLiteralData(encryptedData); + + attributes.put(PGPAttributeKey.LITERAL_DATA_FILENAME, literalData.getFileName()); + attributes.put(PGPAttributeKey.LITERAL_DATA_MODIFIED, Long.toString(literalData.getModificationTime().getTime())); + + getLogger().debug("PGP Decrypted File Name [{}] Modified [{}]", literalData.getFileName(), literalData.getModificationTime()); + StreamUtils.copy(literalData.getInputStream(), outputStream); + + if (isVerified(encryptedData)) { + getLogger().debug("PGP Encrypted Data Verified"); + } else { + final String message = String.format("PGP Encrypted Data [%s] Not Verified", encryptedData.getClass().getSimpleName()); + throw new PGPDecryptionException(message); + } + } + } + + private PGPLiteralData getLiteralData(final PGPEncryptedData encryptedData) { + try { + final InputStream decryptedDataStream = getDecryptedDataStream(encryptedData); + final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(decryptedDataStream); + return getLiteralData(objectFactory); + } catch (final PGPException e) { + final String message = String.format("PGP Decryption Failed [%s]", getEncryptedDataType(encryptedData)); + throw new PGPDecryptionException(message, e); + } + } + + private PGPLiteralData getLiteralData(final PGPObjectFactory objectFactory) throws PGPException { + PGPLiteralData literalData = null; + + for (final Object object : objectFactory) { + if (object instanceof PGPCompressedData) { + final PGPCompressedData compressedData = (PGPCompressedData) object; + getLogger().debug("PGP Compressed Data Algorithm [{}] Found", compressedData.getAlgorithm()); + final PGPObjectFactory compressedObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream()); + literalData = getLiteralData(compressedObjectFactory); + break; + } else if (object instanceof PGPLiteralData) { + literalData = (PGPLiteralData) object; + break; + } + } + + if (literalData == null) { + throw new PGPProcessException("PGP Literal Data not found"); + } + + return literalData; + } + + private InputStream getDecryptedDataStream(final PGPEncryptedData encryptedData) throws PGPException { + getLogger().debug("PGP Encrypted Data [{}] Found", getEncryptedDataType(encryptedData)); + + if (encryptedData instanceof PGPPBEEncryptedData) { + return getDecryptedDataStream((PGPPBEEncryptedData) encryptedData); + } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { + return getDecryptedDataStream((PGPPublicKeyEncryptedData) encryptedData); + } else { + final String message = String.format("PGP Encrypted Data [%s] Not Supported", getEncryptedDataType(encryptedData)); + throw new UnsupportedOperationException(message); + } + } + + private InputStream getDecryptedDataStream(final PGPPBEEncryptedData passwordBasedEncryptedData) throws PGPException { + if (passphrase == null) { + throw new PGPProcessException("PGP Password-Based Encryption Found: Passphrase not configured"); + } else { + final PBEDataDecryptorFactory decryptorFactory = new BcPBEDataDecryptorFactory(passphrase, new BcPGPDigestCalculatorProvider()); + final int symmetricAlgorithm = passwordBasedEncryptedData.getSymmetricAlgorithm(decryptorFactory); + attributes.put(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, Integer.toString(symmetricAlgorithm)); + return passwordBasedEncryptedData.getDataStream(decryptorFactory); + } + } + + private InputStream getDecryptedDataStream(final PGPPublicKeyEncryptedData publicKeyEncryptedData) throws PGPException { + if (privateKeyService == null) { + throw new PGPProcessException("PGP Public Key Encryption Found: Private Key Service not configured"); + } else { + final long keyId = publicKeyEncryptedData.getKeyID(); + final Optional<PGPPrivateKey> foundPrivateKey = privateKeyService.findPrivateKey(keyId); + if (foundPrivateKey.isPresent()) { + final PGPPrivateKey privateKey = foundPrivateKey.get(); + final PublicKeyDataDecryptorFactory decryptorFactory = new BcPublicKeyDataDecryptorFactory(privateKey); + final int symmetricAlgorithm = publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); + attributes.put(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, Integer.toString(symmetricAlgorithm)); + return publicKeyEncryptedData.getDataStream(decryptorFactory); Review comment: Although this is somewhat of an edge case, it is a great example of a message containing multiple Encrypted Data Packets as described in RFC 4880 Section 5.3. This uncovered an issue with the handling of multiple Encrypted Data elements in the callback. The `DecryptContentPGP` processor should support decryption using either a Public Key or Passphrase, depending on property configuration and data received, so I will push an update to correct data handling in this scenario. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: [email protected]
