gresockj commented on a change in pull request #4842: URL: https://github.com/apache/nifi/pull/4842#discussion_r606351601
########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-service/src/main/java/org/apache/nifi/pgp/service/standard/StandardPGPPrivateKeyService.java ########## @@ -0,0 +1,282 @@ +/* + * 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.pgp.service.standard; + +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.context.PropertyContext; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.pgp.service.api.PGPPrivateKeyService; +import org.apache.nifi.pgp.service.standard.exception.PGPConfigurationException; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.util.StringUtils; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Standard Pretty Good Privacy Private Key Service reads Private Keys from configured Keyring files or properties + */ +@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "Private", "Key", "RFC 4880"}) +@CapabilityDescription("PGP Private Key Service provides Private Keys loaded from files or properties") +public class StandardPGPPrivateKeyService extends AbstractControllerService implements PGPPrivateKeyService { + public static final PropertyDescriptor KEYRING_FILE = new PropertyDescriptor.Builder() + .name("keyring-file") + .displayName("Keyring File") + .description("File path for PGP Keyring or Secret Key encoded in binary or ASCII Armor") Review comment: I misread this the first time, thinking it meant the file path could be encoded in binary or ASCII Armor. Perhaps "File path to either PGP Keyring or to Secret Key encoded in either binary or ASCII Armor" ########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-service/src/main/java/org/apache/nifi/pgp/service/standard/StandardPGPPublicKeyService.java ########## @@ -0,0 +1,244 @@ +/* + * 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.pgp.service.standard; + +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.context.PropertyContext; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.pgp.service.api.PGPPublicKeyService; +import org.apache.nifi.pgp.service.standard.exception.PGPConfigurationException; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.util.StringUtils; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Standard Pretty Good Privacy Public Key Service reads Public Keys from configured Keyring files + */ +@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "Private", "Key", "RFC 4880"}) +@CapabilityDescription("PGP Public Key Service providing Public Keys loaded from files") +public class StandardPGPPublicKeyService extends AbstractControllerService implements PGPPublicKeyService { + public static final PropertyDescriptor KEYRING_FILE = new PropertyDescriptor.Builder() + .name("keyring-file") + .displayName("Keyring File") + .description("File path for PGP Keyring or Public Key encoded in binary or ASCII Armor") Review comment: Same comment as in StandardPGPPrivateKeyService. Perhaps "File path to either PGP Keyring or to Public Key encoded in either binary or ASCII Armor" ########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-processors/src/main/java/org/apache/nifi/processors/pgp/EncryptContentPGP.java ########## @@ -0,0 +1,379 @@ +/* + * 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.expression.ExpressionLanguageScope; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.pgp.service.api.PGPPublicKeyService; +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.attributes.CompressionAlgorithm; +import org.apache.nifi.processors.pgp.attributes.FileEncoding; +import org.apache.nifi.processors.pgp.attributes.SymmetricKeyAlgorithm; +import org.apache.nifi.processors.pgp.exception.PGPEncryptionException; +import org.apache.nifi.stream.io.StreamUtils; +import org.apache.nifi.util.StringUtils; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Encrypt Content using Open Pretty Good Privacy encryption methods + */ +@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) +@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "RFC 4880"}) +@CapabilityDescription("Encrypt Contents using OpenPGP") +@WritesAttributes({ + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM, description = "Symmetric-Key Algorithm"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_BLOCK_CIPHER, description = "Symmetric-Key Algorithm Block Cipher"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_KEY_SIZE, description = "Symmetric-Key Algorithm Key Size"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, description = "Symmetric-Key Algorithm Identifier"), + @WritesAttribute(attribute = PGPAttributeKey.FILE_ENCODING, description = "File Encoding"), + @WritesAttribute(attribute = PGPAttributeKey.COMPRESS_ALGORITHM, description = "Compression Algorithm"), + @WritesAttribute(attribute = PGPAttributeKey.COMPRESS_ALGORITHM_ID, description = "Compression Algorithm Identifier"), +}) +public class EncryptContentPGP extends AbstractProcessor { + + public static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .description("Encryption Succeeded") + .build(); + + public static final Relationship FAILURE = new Relationship.Builder() + .name("failure") + .description("Encryption Failed") + .build(); + + public static final PropertyDescriptor SYMMETRIC_KEY_ALGORITHM = new PropertyDescriptor.Builder() + .name("symmetric-key-algorithm") + .displayName("Symmetric-Key Algorithm") + .description("Symmetric-Key Algorithm for encryption") + .required(true) + .defaultValue(SymmetricKeyAlgorithm.AES_256.toString()) + .allowableValues(SymmetricKeyAlgorithm.values()) + .build(); + + public static final PropertyDescriptor COMPRESSION_ALGORITHM = new PropertyDescriptor.Builder() + .name("compression-algorithm") + .displayName("Compression Algorithm") + .description("Compression Algorithm for encryption") + .required(true) + .defaultValue(CompressionAlgorithm.ZIP.toString()) + .allowableValues(CompressionAlgorithm.values()) + .build(); + + public static final PropertyDescriptor FILE_ENCODING = new PropertyDescriptor.Builder() + .name("file-encoding") + .displayName("File Encoding") + .description("File Encoding for encryption") + .required(true) + .defaultValue(FileEncoding.BINARY.toString()) + .allowableValues(FileEncoding.values()) + .build(); + + public static final PropertyDescriptor PASSPHRASE = new PropertyDescriptor.Builder() + .name("passphrase") + .displayName("Passphrase") + .description("Passphrase used for encrypting data with Password-Based Encryption") + .sensitive(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor PUBLIC_KEY_SERVICE = new PropertyDescriptor.Builder() + .name("public-key-service") + .displayName("Public Key Service") + .description("PGP Public Key Service for encrypting data with Public Key Encryption") + .identifiesControllerService(PGPPublicKeyService.class) + .build(); + + public static final PropertyDescriptor PUBLIC_KEY_SEARCH = new PropertyDescriptor.Builder() + .name("public-key-search") + .displayName("Public Key Search") + .description("PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters") + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .addValidator(StandardValidators.ATTRIBUTE_EXPRESSION_LANGUAGE_VALIDATOR) + .dependsOn(PUBLIC_KEY_SERVICE) + .build(); + + /** Enable Integrity Protection as described in RFC 4880 Section 5.13 */ + private static final boolean ENCRYPTION_INTEGRITY_PACKET_ENABLED = true; + + private static final int OUTPUT_BUFFER_SIZE = 8192; + + private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(Arrays.asList(SUCCESS, FAILURE)); + + private static final List<PropertyDescriptor> DESCRIPTORS = Arrays.asList( + SYMMETRIC_KEY_ALGORITHM, + COMPRESSION_ALGORITHM, + FILE_ENCODING, + PASSPHRASE, + PUBLIC_KEY_SERVICE, + PUBLIC_KEY_SEARCH + ); + + /** + * 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 encrypts 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; + } + + try { + final SymmetricKeyAlgorithm symmetricKeyAlgorithm = getSymmetricKeyAlgorithm(context); + final FileEncoding fileEncoding = getFileEncoding(context); + final CompressionAlgorithm compressionAlgorithm = getCompressionAlgorithm(context); + final StreamCallback callback = getEncryptStreamCallback(context, flowFile, symmetricKeyAlgorithm, compressionAlgorithm, fileEncoding); + flowFile = session.write(flowFile, callback); + + final Map<String, String> attributes = getAttributes(symmetricKeyAlgorithm, fileEncoding, compressionAlgorithm); + flowFile = session.putAllAttributes(flowFile, attributes); + + session.transfer(flowFile, SUCCESS); + } catch (final RuntimeException e) { + getLogger().error("Encryption Failed {}", flowFile, e); + session.transfer(flowFile, FAILURE); + } + } + + /** + * Custom Validate requires at least one encryption 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 PGPPublicKeyService publicKeyService = context.getProperty(PUBLIC_KEY_SERVICE).asControllerService(PGPPublicKeyService.class); + if (publicKeyService == null) { + final String explanation = String.format("Neither [%s] nor [%s] configured", PASSPHRASE.getDisplayName(), PUBLIC_KEY_SERVICE.getDisplayName()); + final ValidationResult result = new ValidationResult.Builder() + .valid(false) + .subject(getClass().getSimpleName()) + .explanation(explanation) + .build(); + results.add(result); + } + } + + return results; Review comment: One thing I notice is that if a Public Key Service is specified, but not a Public Key Search value, custom validation doesn't report a problem, but when a flow file goes through the processor, you get an error: "no encryption methods specified". Could we add some custom validation logic here to catch this ahead of time? ########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-processors/src/test/java/org/apache/nifi/processors/pgp/DecryptContentPGPTest.java ########## @@ -0,0 +1,391 @@ +/* + * 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.pgp.service.api.PGPPrivateKeyService; +import org.apache.nifi.pgp.util.PGPSecretKeyGenerator; +import org.apache.nifi.processors.pgp.exception.PGPDecryptionException; +import org.apache.nifi.processors.pgp.exception.PGPProcessException; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.util.LogMessage; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.isA; +import static org.junit.Assert.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DecryptContentPGPTest { + private static final int ENCRYPTION_ALGORITHM = SymmetricKeyAlgorithmTags.AES_256; + + private static final boolean INTEGRITY_ENABLED = true; + + private static final boolean INTEGRITY_DISABLED = false; + + private static final String PASSPHRASE = UUID.randomUUID().toString(); + + private static final String FILE_NAME = String.class.getSimpleName(); + + private static final char FILE_TYPE = PGPLiteralDataGenerator.TEXT; + + private static final long MODIFIED_MILLISECONDS = 86400000; + + private static final Date MODIFIED = new Date(MODIFIED_MILLISECONDS); + + private static final String DATA = String.class.getName(); + + private static final Charset DATA_CHARSET = StandardCharsets.UTF_8; + + private static final int BUFFER_SIZE = 128; + + private static final boolean NESTED_SIGNATURE_DISABLED = false; + + private static final String SERVICE_ID = PGPPrivateKeyService.class.getSimpleName(); + + private static PGPSecretKey rsaSecretKey; + + private static PGPPrivateKey rsaPrivateKey; + + private static PGPPublicKey elGamalPublicKey; + + private static PGPPrivateKey elGamalPrivateKey; + + private TestRunner runner; + + @Mock + private PGPPrivateKeyService privateKeyService; + + @BeforeClass + public static void setKeys() throws Exception { + rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray()); + + final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray()); + rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor); + final PGPSecretKeyRing dsaElGamalSecretKeyRing = PGPSecretKeyGenerator.generateDsaElGamalSecretKeyRing(PASSPHRASE.toCharArray()); + for (final PGPSecretKey secretKey : dsaElGamalSecretKeyRing) { + final PGPPublicKey publicKey = secretKey.getPublicKey(); + if (PGPPublicKey.ELGAMAL_ENCRYPT == publicKey.getAlgorithm()) { + elGamalPrivateKey = secretKey.extractPrivateKey(decryptor); + elGamalPublicKey = publicKey; + } + } + } + + @Before + public void setRunner() { + runner = TestRunners.newTestRunner(new DecryptContentPGP()); + } + + @Test + public void testMissingProperties() { + runner.assertNotValid(); + } + + @Test + public void testFailureEncryptedDataNotFound() { + runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); + runner.enqueue(new byte[]{}); + runner.run(); + + assertFailureExceptionLogged(PGPProcessException.class); + } + Review comment: Should it be possible to configure both Password-based encryption and Private Key Encryption at the same time in this processor? Either way, it would be great to see a unit test showing what happens when you configure the processor both ways. ########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-processors/src/test/java/org/apache/nifi/processors/pgp/EncryptContentPGPTest.java ########## @@ -0,0 +1,293 @@ +/* + * 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.pgp.service.api.PGPPublicKeyService; +import org.apache.nifi.processors.pgp.attributes.CompressionAlgorithm; +import org.apache.nifi.processors.pgp.attributes.FileEncoding; +import org.apache.nifi.processors.pgp.attributes.SymmetricKeyAlgorithm; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.stream.io.StreamUtils; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.apache.nifi.pgp.util.PGPSecretKeyGenerator; + +import org.bouncycastle.bcpg.ArmoredInputStream; +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.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +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 org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class EncryptContentPGPTest { + private static final String PASSPHRASE = UUID.randomUUID().toString(); + + private static final String DATA = String.class.getName(); + + private static final SymmetricKeyAlgorithm DEFAULT_SYMMETRIC_KEY_ALGORITHM = SymmetricKeyAlgorithm.valueOf(EncryptContentPGP.SYMMETRIC_KEY_ALGORITHM.getDefaultValue()); + + private static final String SERVICE_ID = PGPPublicKeyService.class.getName(); + + private static PGPSecretKey rsaSecretKey; + + private static PGPPrivateKey rsaPrivateKey; + + private static PGPPublicKey elGamalPublicKey; + + private static PGPPrivateKey elGamalPrivateKey; + + private TestRunner runner; + + @Mock + private PGPPublicKeyService publicKeyService; + + @BeforeClass + public static void setKeys() throws Exception { + rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray()); + final PGPSecretKeyRing dsaElGamalSecretKeyRing = PGPSecretKeyGenerator.generateDsaElGamalSecretKeyRing(PASSPHRASE.toCharArray()); + + final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray()); + rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor); + for (final PGPSecretKey secretKey : dsaElGamalSecretKeyRing) { + final PGPPublicKey publicKey = secretKey.getPublicKey(); + if (PGPPublicKey.ELGAMAL_ENCRYPT == publicKey.getAlgorithm()) { + elGamalPrivateKey = secretKey.extractPrivateKey(decryptor); + elGamalPublicKey = publicKey; + } + } + } + + @Before + public void setRunner() { + runner = TestRunners.newTestRunner(new EncryptContentPGP()); + } + + @Test + public void testMissingProperties() { + runner.assertNotValid(); + } + + @Test + public void testSuccessPasswordBasedEncryptionDefaultProperties() throws IOException, PGPException { + runner.setProperty(EncryptContentPGP.PASSPHRASE, PASSPHRASE); + runner.enqueue(DATA); + runner.run(); + + assertSuccess(DEFAULT_SYMMETRIC_KEY_ALGORITHM, PASSPHRASE.toCharArray()); + } + + @Test + public void testSuccessPasswordBasedEncryptionSymmetricKeyAlgorithms() throws IOException, PGPException { + for (final SymmetricKeyAlgorithm symmetricKeyAlgorithm : SymmetricKeyAlgorithm.values()) { + runner = TestRunners.newTestRunner(new EncryptContentPGP()); + runner.setProperty(EncryptContentPGP.PASSPHRASE, PASSPHRASE); + runner.setProperty(EncryptContentPGP.SYMMETRIC_KEY_ALGORITHM, symmetricKeyAlgorithm.toString()); + runner.enqueue(DATA); + runner.run(); + assertSuccess(symmetricKeyAlgorithm, PASSPHRASE.toCharArray()); + } + } + + @Test + public void testSuccessPasswordBasedEncryptionCompressionAlgorithms() throws IOException, PGPException { + for (final CompressionAlgorithm compressionAlgorithm : CompressionAlgorithm.values()) { + runner = TestRunners.newTestRunner(new EncryptContentPGP()); + runner.setProperty(EncryptContentPGP.PASSPHRASE, PASSPHRASE); + runner.setProperty(EncryptContentPGP.COMPRESSION_ALGORITHM, compressionAlgorithm.toString()); + runner.enqueue(DATA); + runner.run(); + assertSuccess(DEFAULT_SYMMETRIC_KEY_ALGORITHM, PASSPHRASE.toCharArray()); + } + } + + @Test + public void testSuccessPasswordBasedEncryptionFileEncodingAscii() throws IOException, PGPException { + runner.setProperty(EncryptContentPGP.PASSPHRASE, PASSPHRASE); + runner.setProperty(EncryptContentPGP.FILE_ENCODING, FileEncoding.ASCII.toString()); + runner.enqueue(DATA); + runner.run(); + assertSuccess(DEFAULT_SYMMETRIC_KEY_ALGORITHM, PASSPHRASE.toCharArray()); + } + + @Test + public void testSuccessPublicKeyEncryptionRsaPublicKey() throws IOException, InitializationException, PGPException { + final PGPPublicKey publicKey = rsaSecretKey.getPublicKey(); + setPublicKeyService(publicKey); + final String publicKeyIdSearch = Long.toHexString(publicKey.getKeyID()).toUpperCase(); + when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.of(publicKey)); + + runner.enqueue(DATA); + runner.run(); + assertSuccess(rsaPrivateKey); + } + Review comment: I'd also like to see a unit test here for using both passphrase and public key service. ########## 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: One note here: - When I specify only valid Public/Private Key services in the EncryptContentPGP and DecryptContentPGP processors but no passphrases, both encryption and decryption work. Likewise, when I specify only a matching passphrase in both processors, both encryption and decryption work. - However, when I use the above working configuration of Public/Private Key services, and then add the same matching passphrase to both processors, I get the following error in DecryptContentPGP: `2021-04-02 16:09:17,767 ERROR [Timer-Driven Process Thread-8] o.a.n.processors.pgp.DecryptContentPGP DecryptContentPGP[id=93e3a4e0-0178-1000-a8d0-00468239c9f4] Decryption Failed StandardFlowFileRecord[uuid=9675b046-b929-4b38-81fa-e3e20f8149e8,claim=StandardContentClaim [resourceClaim=StandardResourceClaim[id=1617385030974-1, container=default, section=1], offset=17471, length=672],offset=0,name=9675b046-b929-4b38-81fa-e3e20f8149e8,size=672]: org.apache.nifi.processors.pgp.exception.PGPDecryptionException: PGP Decryption Failed [Public Key Encryption] org.apache.nifi.processors.pgp.exception.PGPDecryptionException: PGP Decryption Failed [Public Key Encryption] at org.apache.nifi.processors.pgp.DecryptContentPGP$DecryptStreamCallback.getLiteralData(DecryptContentPGP.java:262) at org.apache.nifi.processors.pgp.DecryptContentPGP$DecryptStreamCallback.process(DecryptContentPGP.java:238) at org.apache.nifi.controller.repository.StandardProcessSession.write(StandardProcessSession.java:3054) at org.apache.nifi.processors.pgp.DecryptContentPGP.onTrigger(DecryptContentPGP.java:159) at org.apache.nifi.processor.AbstractProcessor.onTrigger(AbstractProcessor.java:27) at org.apache.nifi.controller.StandardProcessorNode.onTrigger(StandardProcessorNode.java:1180) at org.apache.nifi.controller.tasks.ConnectableTask.invoke(ConnectableTask.java:214) at org.apache.nifi.controller.scheduling.TimerDrivenSchedulingAgent$1.run(TimerDrivenSchedulingAgent.java:103) at org.apache.nifi.engine.FlowEngine$2.run(FlowEngine.java:110) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: org.bouncycastle.openpgp.PGPException: Exception starting decryption at org.bouncycastle.openpgp.PGPPublicKeyEncryptedData.getDataStream(Unknown Source) at org.apache.nifi.processors.pgp.DecryptContentPGP$DecryptStreamCallback.getDecryptedDataStream(DecryptContentPGP.java:324) at org.apache.nifi.processors.pgp.DecryptContentPGP$DecryptStreamCallback.getDecryptedDataStream(DecryptContentPGP.java:295) at org.apache.nifi.processors.pgp.DecryptContentPGP$DecryptStreamCallback.getLiteralData(DecryptContentPGP.java:257) ... 15 common frames omitted Caused by: java.io.EOFException: null at org.bouncycastle.openpgp.PGPEncryptedData$TruncatedStream.<init>(Unknown Source) ... 19 common frames omitted` ########## File path: nifi-nar-bundles/nifi-pgp-bundle/nifi-pgp-processors/src/main/java/org/apache/nifi/processors/pgp/EncryptContentPGP.java ########## @@ -0,0 +1,379 @@ +/* + * 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.expression.ExpressionLanguageScope; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.pgp.service.api.PGPPublicKeyService; +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.attributes.CompressionAlgorithm; +import org.apache.nifi.processors.pgp.attributes.FileEncoding; +import org.apache.nifi.processors.pgp.attributes.SymmetricKeyAlgorithm; +import org.apache.nifi.processors.pgp.exception.PGPEncryptionException; +import org.apache.nifi.stream.io.StreamUtils; +import org.apache.nifi.util.StringUtils; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Encrypt Content using Open Pretty Good Privacy encryption methods + */ +@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) +@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "RFC 4880"}) +@CapabilityDescription("Encrypt Contents using OpenPGP") +@WritesAttributes({ + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM, description = "Symmetric-Key Algorithm"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_BLOCK_CIPHER, description = "Symmetric-Key Algorithm Block Cipher"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_KEY_SIZE, description = "Symmetric-Key Algorithm Key Size"), + @WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, description = "Symmetric-Key Algorithm Identifier"), + @WritesAttribute(attribute = PGPAttributeKey.FILE_ENCODING, description = "File Encoding"), + @WritesAttribute(attribute = PGPAttributeKey.COMPRESS_ALGORITHM, description = "Compression Algorithm"), + @WritesAttribute(attribute = PGPAttributeKey.COMPRESS_ALGORITHM_ID, description = "Compression Algorithm Identifier"), +}) +public class EncryptContentPGP extends AbstractProcessor { + + public static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .description("Encryption Succeeded") + .build(); + + public static final Relationship FAILURE = new Relationship.Builder() + .name("failure") + .description("Encryption Failed") + .build(); + + public static final PropertyDescriptor SYMMETRIC_KEY_ALGORITHM = new PropertyDescriptor.Builder() + .name("symmetric-key-algorithm") + .displayName("Symmetric-Key Algorithm") + .description("Symmetric-Key Algorithm for encryption") + .required(true) + .defaultValue(SymmetricKeyAlgorithm.AES_256.toString()) + .allowableValues(SymmetricKeyAlgorithm.values()) + .build(); + + public static final PropertyDescriptor COMPRESSION_ALGORITHM = new PropertyDescriptor.Builder() + .name("compression-algorithm") + .displayName("Compression Algorithm") + .description("Compression Algorithm for encryption") + .required(true) + .defaultValue(CompressionAlgorithm.ZIP.toString()) + .allowableValues(CompressionAlgorithm.values()) + .build(); + + public static final PropertyDescriptor FILE_ENCODING = new PropertyDescriptor.Builder() + .name("file-encoding") + .displayName("File Encoding") + .description("File Encoding for encryption") + .required(true) + .defaultValue(FileEncoding.BINARY.toString()) + .allowableValues(FileEncoding.values()) + .build(); + + public static final PropertyDescriptor PASSPHRASE = new PropertyDescriptor.Builder() + .name("passphrase") + .displayName("Passphrase") + .description("Passphrase used for encrypting data with Password-Based Encryption") + .sensitive(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor PUBLIC_KEY_SERVICE = new PropertyDescriptor.Builder() + .name("public-key-service") + .displayName("Public Key Service") + .description("PGP Public Key Service for encrypting data with Public Key Encryption") + .identifiesControllerService(PGPPublicKeyService.class) + .build(); + + public static final PropertyDescriptor PUBLIC_KEY_SEARCH = new PropertyDescriptor.Builder() + .name("public-key-search") + .displayName("Public Key Search") + .description("PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters") + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .addValidator(StandardValidators.ATTRIBUTE_EXPRESSION_LANGUAGE_VALIDATOR) + .dependsOn(PUBLIC_KEY_SERVICE) + .build(); + + /** Enable Integrity Protection as described in RFC 4880 Section 5.13 */ + private static final boolean ENCRYPTION_INTEGRITY_PACKET_ENABLED = true; + + private static final int OUTPUT_BUFFER_SIZE = 8192; + + private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(Arrays.asList(SUCCESS, FAILURE)); + + private static final List<PropertyDescriptor> DESCRIPTORS = Arrays.asList( + SYMMETRIC_KEY_ALGORITHM, + COMPRESSION_ALGORITHM, + FILE_ENCODING, + PASSPHRASE, + PUBLIC_KEY_SERVICE, + PUBLIC_KEY_SEARCH + ); + + /** + * 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 encrypts 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; + } + + try { + final SymmetricKeyAlgorithm symmetricKeyAlgorithm = getSymmetricKeyAlgorithm(context); + final FileEncoding fileEncoding = getFileEncoding(context); + final CompressionAlgorithm compressionAlgorithm = getCompressionAlgorithm(context); + final StreamCallback callback = getEncryptStreamCallback(context, flowFile, symmetricKeyAlgorithm, compressionAlgorithm, fileEncoding); + flowFile = session.write(flowFile, callback); + + final Map<String, String> attributes = getAttributes(symmetricKeyAlgorithm, fileEncoding, compressionAlgorithm); + flowFile = session.putAllAttributes(flowFile, attributes); + + session.transfer(flowFile, SUCCESS); + } catch (final RuntimeException e) { + getLogger().error("Encryption Failed {}", flowFile, e); + session.transfer(flowFile, FAILURE); + } + } + + /** + * Custom Validate requires at least one encryption 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 PGPPublicKeyService publicKeyService = context.getProperty(PUBLIC_KEY_SERVICE).asControllerService(PGPPublicKeyService.class); + if (publicKeyService == null) { + final String explanation = String.format("Neither [%s] nor [%s] configured", PASSPHRASE.getDisplayName(), PUBLIC_KEY_SERVICE.getDisplayName()); + final ValidationResult result = new ValidationResult.Builder() + .valid(false) + .subject(getClass().getSimpleName()) + .explanation(explanation) + .build(); + results.add(result); + } + } + + return results; + } + + private StreamCallback getEncryptStreamCallback(final ProcessContext context, final FlowFile flowFile, + final SymmetricKeyAlgorithm symmetricKeyAlgorithm, + final CompressionAlgorithm compressionAlgorithm, + final FileEncoding fileEncoding) { + final SecureRandom secureRandom = new SecureRandom(); + final PGPDataEncryptorBuilder dataEncryptorBuilder = new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getId()) + .setSecureRandom(secureRandom) + .setWithIntegrityPacket(ENCRYPTION_INTEGRITY_PACKET_ENABLED); + final PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(dataEncryptorBuilder); + final List<PGPKeyEncryptionMethodGenerator> methodGenerators = getEncryptionMethodGenerators(context, flowFile, secureRandom); + methodGenerators.forEach(encryptedDataGenerator::addMethod); + + final String filename = flowFile.getAttribute(CoreAttributes.FILENAME.key()); + return new EncryptStreamCallback(filename, fileEncoding, encryptedDataGenerator, compressionAlgorithm); + } + + private List<PGPKeyEncryptionMethodGenerator> getEncryptionMethodGenerators(final ProcessContext context, + final FlowFile flowFile, + final SecureRandom secureRandom) { + final List<PGPKeyEncryptionMethodGenerator> generators = new ArrayList<>(); + + final PropertyValue passphraseProperty = context.getProperty(PASSPHRASE); + if (passphraseProperty.isSet()) { + final char[] passphrase = passphraseProperty.getValue().toCharArray(); + generators.add(new JcePBEKeyEncryptionMethodGenerator(passphrase).setSecureRandom(secureRandom)); + } + + final PropertyValue publicKeySearchProperty = context.getProperty(PUBLIC_KEY_SEARCH); + if (publicKeySearchProperty.isSet()) { + final String publicKeySearch = publicKeySearchProperty.evaluateAttributeExpressions(flowFile).getValue(); + getLogger().debug("Public Key Search [{}]", publicKeySearch); + + final PGPPublicKeyService publicKeyService = context.getProperty(PUBLIC_KEY_SERVICE).asControllerService(PGPPublicKeyService.class); + final Optional<PGPPublicKey> optionalPublicKey = publicKeyService.findPublicKey(publicKeySearch); Review comment: This can throw an NPE if you do the following: 1. Configure the processor with a Public Key Service 2. Specify a Public Key Search value 3. Set the Public Key Service to None 4. Specify a Passphrase 5. Run a flow file through the processor (this gives you the NPE) -- 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]
