commons-codec
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java
index 50397c8544..8cad3cb725 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/EncryptContent.java
@@ -16,16 +16,6 @@
*/
package org.apache.nifi.processors.standard;
-import java.security.Security;
-import java.text.Normalizer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.EventDriven;
import org.apache.nifi.annotation.behavior.InputRequirement;
@@ -56,6 +46,16 @@ import org.apache.nifi.security.util.KeyDerivationFunction;
import org.apache.nifi.util.StopWatch;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import java.security.Security;
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
@EventDriven
@SideEffectFree
@SupportsBatching
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java
index 1931c720a6..b97e416101 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java
@@ -16,17 +16,6 @@
*/
package org.apache.nifi.processors.standard.util;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.security.NoSuchProviderException;
-import java.security.SecureRandom;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.zip.Deflater;
-
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.io.StreamCallback;
import org.apache.nifi.processors.standard.EncryptContent;
@@ -41,6 +30,7 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
@@ -50,18 +40,41 @@ import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.zip.Deflater;
+
+import static org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE;
+import static org.apache.nifi.processors.standard.util.PGPUtil.BUFFER_SIZE;
public class OpenPGPKeyBasedEncryptor implements Encryptor {
+ private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class);
private String algorithm;
private String provider;
+ // TODO: This can hold either the secret or public keyring path
private String keyring;
private String userId;
private char[] passphrase;
private String filename;
- public static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
-
public OpenPGPKeyBasedEncryptor(final String algorithm, final String provider, final String keyring, final String userId, final char[] passphrase, final String filename) {
this.algorithm = algorithm;
this.provider = provider;
@@ -81,75 +94,137 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
return new OpenPGPDecryptCallback(provider, keyring, passphrase);
}
- /*
- * Validate secret keyring passphrase
+ /**
+ * Returns true if the passphrase is valid.
+ *
+ * This is used in the EncryptContent custom validation to check if the passphrase can extract a private key from the secret key ring. After BC was upgraded from 1.46 to 1.53, the API changed so this is performed differently but the functionality is equivalent.
+ *
+ * @param provider the provider name
+ * @param secretKeyringFile the file path to the keyring
+ * @param passphrase the passphrase
+ * @return true if the passphrase can successfully extract any private key
+ * @throws IOException if there is a problem reading the keyring file
+ * @throws PGPException if there is a problem parsing/extracting the private key
+ * @throws NoSuchProviderException if the provider is not available
*/
public static boolean validateKeyring(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException, NoSuchProviderException {
- try (InputStream fin = Files.newInputStream(Paths.get(secretKeyringFile)); InputStream pin = PGPUtil.getDecoderStream(fin)) {
- PGPSecretKeyRingCollection pgpsec = new PGPSecretKeyRingCollection(pin);
- Iterator ringit = pgpsec.getKeyRings();
- while (ringit.hasNext()) {
- PGPSecretKeyRing secretkeyring = (PGPSecretKeyRing) ringit.next();
- PGPSecretKey secretkey = secretkeyring.getSecretKey();
- secretkey.extractPrivateKey(passphrase, provider);
- return true;
- }
+ try {
+ getDecryptedPrivateKey(provider, secretKeyringFile, passphrase);
+ return true;
+ } catch (Exception e) {
+ // If this point is reached, no private key could be extracted with the given passphrase
return false;
}
+ }
+ private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException {
+ // TODO: Verify that key IDs cannot be 0
+ return getDecryptedPrivateKey(provider, secretKeyringFile, 0L, passphrase);
+ }
+
+ private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, long keyId, char[] passphrase) throws IOException, PGPException {
+ // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated
+
+ // Read in from the secret keyring file
+ try (FileInputStream keyInputStream = new FileInputStream(secretKeyringFile)) {
+
+ // Form the SecretKeyRing collection (1.53 way with fingerprint calculator)
+ PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator());
+
+ // The decryptor is identical for all keys
+ final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(provider).build(passphrase);
+
+ // Iterate over all secret keyrings
+ Iterator keyringIterator = pgpSecretKeyRingCollection.getKeyRings();
+ PGPSecretKeyRing keyRing;
+ PGPSecretKey secretKey;
+
+ while (keyringIterator.hasNext()) {
+ keyRing = keyringIterator.next();
+
+ // If keyId exists, get a specific secret key; else, iterate over all
+ if (keyId != 0) {
+ secretKey = keyRing.getSecretKey(keyId);
+ try {
+ return secretKey.extractPrivateKey(decryptor);
+ } catch (Exception e) {
+ throw new PGPException("No private key available using passphrase", e);
+ }
+ } else {
+ Iterator keyIterator = keyRing.getSecretKeys();
+
+ while (keyIterator.hasNext()) {
+ secretKey = keyIterator.next();
+ try {
+ return secretKey.extractPrivateKey(decryptor);
+ } catch (Exception e) {
+ // TODO: Log (expected) failures?
+ }
+ }
+ }
+ }
+ }
+
+ // If this point is reached, no private key could be extracted with the given passphrase
+ throw new PGPException("No private key available using passphrase");
}
/*
* Get the public key for a specific user id from a keyring.
*/
@SuppressWarnings("rawtypes")
- public static PGPPublicKey getPublicKey(String userId, String publicKeyring) throws IOException, PGPException {
- PGPPublicKey pubkey = null;
- try (InputStream fin = Files.newInputStream(Paths.get(publicKeyring)); InputStream pin = PGPUtil.getDecoderStream(fin)) {
- PGPPublicKeyRingCollection pgppub = new PGPPublicKeyRingCollection(pin);
+ public static PGPPublicKey getPublicKey(String userId, String publicKeyringFile) throws IOException, PGPException {
+ // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated
- Iterator ringit = pgppub.getKeyRings();
- while (ringit.hasNext()) {
- PGPPublicKeyRing kring = (PGPPublicKeyRing) ringit.next();
+ // Read in from the public keyring file
+ try (FileInputStream keyInputStream = new FileInputStream(publicKeyringFile)) {
- Iterator keyit = kring.getPublicKeys();
- while (keyit.hasNext()) {
- pubkey = (PGPPublicKey) keyit.next();
- boolean userIdMatch = false;
+ // Form the PublicKeyRing collection (1.53 way with fingerprint calculator)
+ PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator());
- Iterator userit = pubkey.getUserIDs();
- while (userit.hasNext()) {
- String id = userit.next().toString();
- if (id.contains(userId)) {
- userIdMatch = true;
- break;
+ // Iterate over all public keyrings
+ Iterator iter = pgpPublicKeyRingCollection.getKeyRings();
+ PGPPublicKeyRing keyRing;
+ while (iter.hasNext()) {
+ keyRing = iter.next();
+
+ // Iterate over each public key in this keyring
+ Iterator keyIter = keyRing.getPublicKeys();
+ while (keyIter.hasNext()) {
+ PGPPublicKey publicKey = keyIter.next();
+
+ // Iterate over each userId attached to the public key
+ Iterator userIdIterator = publicKey.getUserIDs();
+ while (userIdIterator.hasNext()) {
+ String id = (String) userIdIterator.next();
+ if (userId.equalsIgnoreCase(id)) {
+ return publicKey;
}
}
- if (pubkey.isEncryptionKey() && userIdMatch) {
- return pubkey;
- }
}
}
}
- return null;
+
+ // If this point is reached, no public key could be extracted with the given userId
+ throw new PGPException("Could not find a public key with the given userId");
}
private static class OpenPGPDecryptCallback implements StreamCallback {
private String provider;
- private String secretKeyring;
+ private String secretKeyringFile;
private char[] passphrase;
- OpenPGPDecryptCallback(final String provider, final String keyring, final char[] passphrase) {
+ OpenPGPDecryptCallback(final String provider, final String secretKeyringFile, final char[] passphrase) {
this.provider = provider;
- this.secretKeyring = keyring;
+ this.secretKeyringFile = secretKeyringFile;
this.passphrase = passphrase;
}
@Override
public void process(InputStream in, OutputStream out) throws IOException {
try (InputStream pgpin = PGPUtil.getDecoderStream(in)) {
- PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin);
+ PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin, new BcKeyFingerprintCalculator());
Object obj = pgpFactory.nextObject();
if (!(obj instanceof PGPEncryptedDataList)) {
@@ -160,19 +235,11 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
}
PGPEncryptedDataList encList = (PGPEncryptedDataList) obj;
- PGPSecretKeyRingCollection pgpSecretKeyring;
- try (InputStream secretKeyringIS = Files.newInputStream(Paths.get(secretKeyring)); InputStream pgpIS = PGPUtil.getDecoderStream(secretKeyringIS)) {
- // open secret keyring file
- pgpSecretKeyring = new PGPSecretKeyRingCollection(pgpIS);
- } catch (Exception e) {
- throw new ProcessException("Invalid secret keyring - " + e.getMessage());
- }
-
try {
PGPPrivateKey privateKey = null;
PGPPublicKeyEncryptedData encData = null;
- // find the secret key in the encrypted data
+ // Find the secret key in the encrypted data
Iterator it = encList.getEncryptedDataObjects();
while (privateKey == null && it.hasNext()) {
obj = it.next();
@@ -180,32 +247,60 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
throw new ProcessException("Invalid OpenPGP data");
}
encData = (PGPPublicKeyEncryptedData) obj;
- PGPSecretKey secretkey = pgpSecretKeyring.getSecretKey(encData.getKeyID());
- if (secretkey != null) {
- privateKey = secretkey.extractPrivateKey(passphrase, provider);
+
+ // Check each encrypted data object to see if it contains the key ID for the secret key -> private key
+ try {
+ privateKey = getDecryptedPrivateKey(provider, secretKeyringFile, encData.getKeyID(), passphrase);
+ } catch (PGPException e) {
+ // TODO: Log (expected) exception?
}
}
if (privateKey == null) {
throw new ProcessException("Secret keyring does not contain the key required to decrypt");
}
- try (InputStream clearData = encData.getDataStream(privateKey, provider)) {
- PGPObjectFactory clearFactory = new PGPObjectFactory(clearData);
+ // Read in the encrypted data stream and decrypt it
+ final PublicKeyDataDecryptorFactory dataDecryptor = new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(provider).build(privateKey);
+ try (InputStream clear = encData.getDataStream(dataDecryptor)) {
+ // Create a plain object factory
+ JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);
- obj = clearFactory.nextObject();
- if (obj instanceof PGPCompressedData) {
- PGPCompressedData compData = (PGPCompressedData) obj;
- clearFactory = new PGPObjectFactory(compData.getDataStream());
- obj = clearFactory.nextObject();
+ Object message = plainFact.nextObject();
+
+ // Check the message type and act accordingly
+
+ // If compressed, decompress
+ if (message instanceof PGPCompressedData) {
+ PGPCompressedData cData = (PGPCompressedData) message;
+ JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream());
+
+ message = pgpFact.nextObject();
}
- PGPLiteralData literal = (PGPLiteralData) obj;
- try (InputStream lis = literal.getInputStream()) {
- final byte[] buffer = new byte[4096];
- int len;
- while ((len = lis.read(buffer)) >= 0) {
- out.write(buffer, 0, len);
+ // If the message is literal data, read it and process to the out stream
+ if (message instanceof PGPLiteralData) {
+ PGPLiteralData literalData = (PGPLiteralData) message;
+
+ try (InputStream lis = literalData.getInputStream()) {
+ final byte[] buffer = new byte[BLOCK_SIZE];
+ int len;
+ while ((len = lis.read(buffer)) >= 0) {
+ out.write(buffer, 0, len);
+ }
}
+ } else if (message instanceof PGPOnePassSignatureList) {
+ // TODO: This is legacy code but should verify signature list here
+ throw new PGPException("encrypted message contains a signed message - not literal data.");
+ } else {
+ throw new PGPException("message is not a simple encrypted file - type unknown.");
+ }
+
+ if (encData.isIntegrityProtected()) {
+ if (!encData.verify()) {
+ throw new PGPException("Failed message integrity check");
+ }
+ } else {
+ logger.warn("No message integrity check");
}
}
} catch (Exception e) {
@@ -235,6 +330,8 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
@Override
public void process(InputStream in, OutputStream out) throws IOException {
PGPPublicKey publicKey;
+ final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm);
+
try {
publicKey = getPublicKey(userId, publicKeyring);
} catch (Exception e) {
@@ -242,35 +339,35 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
}
try {
- SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM);
-
OutputStream output = out;
- if (EncryptContent.isPGPArmoredAlgorithm(algorithm)) {
+ if (isArmored) {
output = new ArmoredOutputStream(out);
}
try {
- PGPEncryptedDataGenerator encGenerator = new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, false, secureRandom, provider);
- encGenerator.addMethod(publicKey);
- try (OutputStream encOut = encGenerator.open(output, new byte[65536])) {
+ // TODO: Refactor internal symmetric encryption algorithm to be customizable
+ PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
+ new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider));
- PGPCompressedDataGenerator compData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED);
- try (OutputStream compOut = compData.open(encOut, new byte[65536])) {
+ encryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider(provider));
- PGPLiteralDataGenerator literal = new PGPLiteralDataGenerator();
- try (OutputStream literalOut = literal.open(compOut, PGPLiteralData.BINARY, filename, new Date(), new byte[65536])) {
+ // TODO: Refactor shared encryption code to utility
+ try (OutputStream encryptedOut = encryptedDataGenerator.open(output, new byte[BUFFER_SIZE])) {
+ PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED);
+ try (OutputStream compressedOut = compressedDataGenerator.open(encryptedOut, new byte[BUFFER_SIZE])) {
+ PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
+ try (OutputStream literalOut = literalDataGenerator.open(compressedOut, PGPLiteralData.BINARY, filename, new Date(), new byte[BUFFER_SIZE])) {
- final byte[] buffer = new byte[4096];
+ final byte[] buffer = new byte[BLOCK_SIZE];
int len;
while ((len = in.read(buffer)) >= 0) {
literalOut.write(buffer, 0, len);
}
-
}
}
}
} finally {
- if (EncryptContent.isPGPArmoredAlgorithm(algorithm)) {
+ if (isArmored) {
output.close();
}
}
@@ -278,6 +375,5 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
throw new ProcessException(e.getMessage());
}
}
-
}
}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java
index e0c398c069..1ffe9e4659 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java
@@ -16,38 +16,39 @@
*/
package org.apache.nifi.processors.standard.util;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
+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.PGPPBEEncryptedData;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.security.SecureRandom;
-import java.util.Date;
-import java.util.zip.Deflater;
-import org.apache.nifi.processor.exception.ProcessException;
-import org.apache.nifi.processor.io.StreamCallback;
-import org.apache.nifi.processors.standard.EncryptContent;
-import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
-import org.bouncycastle.bcpg.ArmoredOutputStream;
-import org.bouncycastle.openpgp.PGPCompressedData;
-import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
-import org.bouncycastle.openpgp.PGPEncryptedData;
-import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
-import org.bouncycastle.openpgp.PGPEncryptedDataList;
-import org.bouncycastle.openpgp.PGPLiteralData;
-import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
-import org.bouncycastle.openpgp.PGPObjectFactory;
-import org.bouncycastle.openpgp.PGPPBEEncryptedData;
-import org.bouncycastle.openpgp.PGPUtil;
+import static org.bouncycastle.openpgp.PGPUtil.getDecoderStream;
public class OpenPGPPasswordBasedEncryptor implements Encryptor {
+ private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class);
private String algorithm;
private String provider;
private char[] password;
private String filename;
- public static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
-
public OpenPGPPasswordBasedEncryptor(final String algorithm, final String provider, final char[] passphrase, final String filename) {
this.algorithm = algorithm;
this.provider = provider;
@@ -77,8 +78,8 @@ public class OpenPGPPasswordBasedEncryptor implements Encryptor {
@Override
public void process(InputStream in, OutputStream out) throws IOException {
- InputStream pgpin = PGPUtil.getDecoderStream(in);
- PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin);
+ InputStream pgpin = getDecoderStream(in);
+ JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(pgpin);
Object obj = pgpFactory.nextObject();
if (!(obj instanceof PGPEncryptedDataList)) {
@@ -93,31 +94,41 @@ public class OpenPGPPasswordBasedEncryptor implements Encryptor {
if (!(obj instanceof PGPPBEEncryptedData)) {
throw new ProcessException("Invalid OpenPGP data");
}
- PGPPBEEncryptedData encData = (PGPPBEEncryptedData) obj;
+ PGPPBEEncryptedData encryptedData = (PGPPBEEncryptedData) obj;
try {
- InputStream clearData = encData.getDataStream(password, provider);
- PGPObjectFactory clearFactory = new PGPObjectFactory(clearData);
+ final PGPDigestCalculatorProvider digestCalculatorProvider = new JcaPGPDigestCalculatorProviderBuilder().setProvider(provider).build();
+ final PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(digestCalculatorProvider).setProvider(provider).build(password);
+ InputStream clear = encryptedData.getDataStream(decryptorFactory);
- obj = clearFactory.nextObject();
+ JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(clear);
+
+ obj = pgpObjectFactory.nextObject();
if (obj instanceof PGPCompressedData) {
- PGPCompressedData compData = (PGPCompressedData) obj;
- clearFactory = new PGPObjectFactory(compData.getDataStream());
- obj = clearFactory.nextObject();
+ PGPCompressedData compressedData = (PGPCompressedData) obj;
+ pgpObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream());
+ obj = pgpObjectFactory.nextObject();
}
- PGPLiteralData literal = (PGPLiteralData) obj;
- InputStream lis = literal.getInputStream();
- final byte[] buffer = new byte[4096];
+ PGPLiteralData literalData = (PGPLiteralData) obj;
+ InputStream plainIn = literalData.getInputStream();
+ final byte[] buffer = new byte[org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE];
int len;
- while ((len = lis.read(buffer)) >= 0) {
+ while ((len = plainIn.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
+
+ if (encryptedData.isIntegrityProtected()) {
+ if (!encryptedData.verify()) {
+ throw new PGPException("Integrity check failed");
+ }
+ } else {
+ logger.warn("No message integrity check");
+ }
} catch (Exception e) {
throw new ProcessException(e.getMessage());
}
}
-
}
private static class OpenPGPEncryptCallback implements StreamCallback {
@@ -137,39 +148,11 @@ public class OpenPGPPasswordBasedEncryptor implements Encryptor {
@Override
public void process(InputStream in, OutputStream out) throws IOException {
try {
- SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM);
-
- OutputStream output = out;
- if (EncryptContent.isPGPArmoredAlgorithm(algorithm)) {
- output = new ArmoredOutputStream(out);
- }
-
- PGPEncryptedDataGenerator encGenerator = new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, false,
- secureRandom, provider);
- encGenerator.addMethod(password);
- OutputStream encOut = encGenerator.open(output, new byte[65536]);
-
- PGPCompressedDataGenerator compData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED);
- OutputStream compOut = compData.open(encOut, new byte[65536]);
-
- PGPLiteralDataGenerator literal = new PGPLiteralDataGenerator();
- OutputStream literalOut = literal.open(compOut, PGPLiteralData.BINARY, filename, new Date(), new byte[65536]);
-
- final byte[] buffer = new byte[4096];
- int len;
- while ((len = in.read(buffer)) >= 0) {
- literalOut.write(buffer, 0, len);
- }
-
- literalOut.close();
- compOut.close();
- encOut.close();
- output.close();
+ PGPKeyEncryptionMethodGenerator encryptionMethodGenerator = new JcePBEKeyEncryptionMethodGenerator(password).setProvider(provider);
+ org.apache.nifi.processors.standard.util.PGPUtil.encrypt(in, out, algorithm, provider, PGPEncryptedData.AES_128, filename, encryptionMethodGenerator);
} catch (Exception e) {
throw new ProcessException(e.getMessage());
}
-
}
-
}
}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PGPUtil.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PGPUtil.java
new file mode 100644
index 0000000000..b1ee11f650
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PGPUtil.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+import org.apache.nifi.processors.standard.EncryptContent;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
+import org.bouncycastle.openpgp.PGPEncryptedData;
+import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPLiteralData;
+import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
+import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
+import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SecureRandom;
+import java.util.Date;
+import java.util.zip.Deflater;
+
+/**
+ * This class contains static utility methods to assist with common PGP operations.
+ */
+public class PGPUtil {
+ private static final Logger logger = LoggerFactory.getLogger(PGPUtil.class);
+
+ public static final int BUFFER_SIZE = 65536;
+ public static final int BLOCK_SIZE = 4096;
+
+ public static void encrypt(InputStream in, OutputStream out, String algorithm, String provider, int cipher, String filename, PGPKeyEncryptionMethodGenerator encryptionMethodGenerator) throws IOException, PGPException {
+ final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm);
+ OutputStream output = out;
+ if (isArmored) {
+ output = new ArmoredOutputStream(out);
+ }
+
+ // Default value, do not allow null encryption
+ if (cipher == PGPEncryptedData.NULL) {
+ logger.warn("Null encryption not allowed; defaulting to AES-128");
+ cipher = PGPEncryptedData.AES_128;
+ }
+
+ try {
+ // TODO: Can probably hardcode provider to BC and remove one method parameter
+ PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
+ new JcePGPDataEncryptorBuilder(cipher).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider));
+
+ encryptedDataGenerator.addMethod(encryptionMethodGenerator);
+
+ try (OutputStream encryptedOut = encryptedDataGenerator.open(output, new byte[BUFFER_SIZE])) {
+ PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED);
+ try (OutputStream compressedOut = compressedDataGenerator.open(encryptedOut, new byte[BUFFER_SIZE])) {
+ PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
+ try (OutputStream literalOut = literalDataGenerator.open(compressedOut, PGPLiteralData.BINARY, filename, new Date(), new byte[BUFFER_SIZE])) {
+
+ final byte[] buffer = new byte[BLOCK_SIZE];
+ int len;
+ while ((len = in.read(buffer)) >= 0) {
+ literalOut.write(buffer, 0, len);
+ }
+ }
+ }
+ }
+ } finally {
+ if (isArmored) {
+ output.close();
+ }
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java
index ee21a50c8e..3be29760a5 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java
@@ -16,12 +16,6 @@
*/
package org.apache.nifi.processors.standard;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.security.Security;
-import java.util.Collection;
-
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor;
@@ -39,6 +33,12 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.security.Security;
+import java.util.Collection;
+
public class TestEncryptContent {
private static final Logger logger = LoggerFactory.getLogger(TestEncryptContent.class);
@@ -203,6 +203,128 @@ public class TestEncryptContent {
flowFile.assertContentEquals(Paths.get("src/test/resources/TestEncryptContent/text.txt"));
}
+ @Test
+ public void testShouldValidatePGPPublicKeyringRequiresUserId() {
+ // Arrange
+ final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
+ Collection results;
+ MockProcessContext pc;
+
+ runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
+ runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
+ runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg");
+ runner.enqueue(new byte[0]);
+ pc = (MockProcessContext) runner.getProcessContext();
+
+ // Act
+ results = pc.validate();
+
+ // Assert
+ Assert.assertEquals(1, results.size());
+ ValidationResult vr = (ValidationResult) results.toArray()[0];
+ String expectedResult = " encryption without a " + EncryptContent.PASSWORD.getDisplayName() + " requires both "
+ + EncryptContent.PUBLIC_KEYRING.getDisplayName() + " and "
+ + EncryptContent.PUBLIC_KEY_USERID.getDisplayName();
+ String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
+ Assert.assertTrue(message, vr.toString().contains(expectedResult));
+ }
+
+ @Test
+ public void testShouldValidatePGPPublicKeyringExists() {
+ // Arrange
+ final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
+ Collection results;
+ MockProcessContext pc;
+
+ runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
+ runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
+ runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg.missing");
+ runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
+ runner.enqueue(new byte[0]);
+ pc = (MockProcessContext) runner.getProcessContext();
+
+ // Act
+ results = pc.validate();
+
+ // Assert
+ Assert.assertEquals(1, results.size());
+ ValidationResult vr = (ValidationResult) results.toArray()[0];
+ String expectedResult = " (No such file or directory)";
+ String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
+ Assert.assertTrue(message, vr.toString().contains(expectedResult));
+ }
+
+ @Test
+ public void testShouldValidatePGPPublicKeyringIsProperFormat() {
+ // Arrange
+ final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
+ Collection results;
+ MockProcessContext pc;
+
+ runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
+ runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
+ runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/text.txt");
+ runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
+ runner.enqueue(new byte[0]);
+ pc = (MockProcessContext) runner.getProcessContext();
+
+ // Act
+ results = pc.validate();
+
+ // Assert
+ Assert.assertEquals(1, results.size());
+ ValidationResult vr = (ValidationResult) results.toArray()[0];
+ String expectedResult = " java.io.IOException: invalid header encountered";
+ String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
+ Assert.assertTrue(message, vr.toString().contains(expectedResult));
+ }
+
+ @Test
+ public void testShouldValidatePGPPublicKeyringContainsUserId() {
+ // Arrange
+ final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
+ Collection results;
+ MockProcessContext pc;
+
+ runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
+ runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
+ runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg");
+ runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
+ runner.enqueue(new byte[0]);
+ pc = (MockProcessContext) runner.getProcessContext();
+
+ // Act
+ results = pc.validate();
+
+ // Assert
+ Assert.assertEquals(1, results.size());
+ ValidationResult vr = (ValidationResult) results.toArray()[0];
+ String expectedResult = "PGPException: Could not find a public key with the given userId";
+ String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
+ Assert.assertTrue(message, vr.toString().contains(expectedResult));
+ }
+
+ @Test
+ public void testShouldExtractPGPPublicKeyFromKeyring() {
+ // Arrange
+ final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
+ Collection results;
+ MockProcessContext pc;
+
+ runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
+ runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
+ runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg");
+ runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) ");
+ runner.enqueue(new byte[0]);
+ pc = (MockProcessContext) runner.getProcessContext();
+
+ // Act
+ results = pc.validate();
+
+ // Assert
+ Assert.assertEquals(0, results.size());
+ }
+
@Test
public void testValidation() {
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
@@ -249,20 +371,15 @@ public class TestEncryptContent {
+ EncryptContent.PUBLIC_KEY_USERID.getDisplayName()));
}
- runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
- runner.enqueue(new byte[0]);
- pc = (MockProcessContext) runner.getProcessContext();
- results = pc.validate();
- Assert.assertEquals(1, results.size());
- for (final ValidationResult vr : results) {
- Assert.assertTrue(vr.toString().contains("does not contain user id USERID"));
- }
+ // Legacy tests moved to individual tests to comply with new library
+
+ // TODO: Move secring tests out to individual as well
runner.removeProperty(EncryptContent.PUBLIC_KEYRING);
runner.removeProperty(EncryptContent.PUBLIC_KEY_USERID);
runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE);
- runner.setProperty(EncryptContent.PRIVATE_KEYRING, "src/test/resources/TestEncryptContent/text.txt");
+ runner.setProperty(EncryptContent.PRIVATE_KEYRING, "src/test/resources/TestEncryptContent/secring.gpg");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
results = pc.validate();
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptorTest.java
new file mode 100644
index 0000000000..fc7a2f6d4e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptorTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.util;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.Security;
+
+public class OpenPGPKeyBasedEncryptorTest {
+ private static final Logger logger = LoggerFactory.getLogger(OpenPGPKeyBasedEncryptorTest.class);
+
+ private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
+ private final File unsignedFile = new File("src/test/resources/TestEncryptContent/text.txt.unsigned.gpg");
+ private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.gpg");
+
+ private static final String SECRET_KEYRING_PATH = "src/test/resources/TestEncryptContent/secring.gpg";
+ private static final String PUBLIC_KEYRING_PATH = "src/test/resources/TestEncryptContent/pubring.gpg";
+ private static final String USER_ID = "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) ";
+
+ private static final String PASSWORD = "thisIsABadPassword";
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void testShouldEncryptAndDecrypt() throws Exception {
+ // Arrange
+ final String PLAINTEXT = "This is a plaintext message.";
+ logger.info("Plaintext: {}", PLAINTEXT);
+ InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"));
+ OutputStream cipherStream = new ByteArrayOutputStream();
+ OutputStream recoveredStream = new ByteArrayOutputStream();
+
+ // No file, just streams
+ String filename = "tempFile.txt";
+
+ // Encryptor does not require password
+ OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], filename);
+ StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
+
+ OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
+ StreamCallback decryptionCallback = decryptor.getDecryptionCallback();
+
+ // Act
+ encryptionCallback.process(plainStream, cipherStream);
+
+ final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray();
+ logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
+ InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes);
+
+ decryptionCallback.process(cipherInputStream, recoveredStream);
+
+ // Assert
+ byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: {}", recovered);
+ assert PLAINTEXT.equals(recovered);
+ }
+
+ @Test
+ public void testShouldDecryptExternalFile() throws Exception {
+ // Arrange
+ byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
+ final String PLAINTEXT = new String(plainBytes, "UTF-8");
+
+ InputStream cipherStream = new FileInputStream(unsignedFile);
+ OutputStream recoveredStream = new ByteArrayOutputStream();
+
+ // No file, just streams
+ String filename = unsignedFile.getName();
+
+ OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
+
+ StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
+
+ // Act
+ decryptionCallback.process(cipherStream, recoveredStream);
+
+ // Assert
+ byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: {}", recovered);
+ Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
+ }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptorTest.java
new file mode 100644
index 0000000000..ddd10ba956
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptorTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.util;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.Security;
+
+public class OpenPGPPasswordBasedEncryptorTest {
+ private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptorTest.class);
+
+ private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
+ private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.asc");
+
+ private static final String PASSWORD = "thisIsABadPassword";
+ private static final String LEGACY_PASSWORD = "Hello, World!";
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void testShouldEncryptAndDecrypt() throws Exception {
+ // Arrange
+ final String PLAINTEXT = "This is a plaintext message.";
+ logger.info("Plaintext: {}", PLAINTEXT);
+ InputStream plainStream = new java.io.ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"));
+ OutputStream cipherStream = new ByteArrayOutputStream();
+ OutputStream recoveredStream = new ByteArrayOutputStream();
+
+ // No file, just streams
+ String filename = "tempFile.txt";
+
+ OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), PASSWORD.toCharArray(), filename);
+
+ StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
+ StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
+
+ // Act
+ encryptionCallback.process(plainStream, cipherStream);
+
+ final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray();
+ logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
+ InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes);
+
+ decryptionCallback.process(cipherInputStream, recoveredStream);
+
+ // Assert
+ byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: {}", recovered);
+ assert PLAINTEXT.equals(recovered);
+ }
+
+ @Test
+ public void testShouldDecryptExternalFile() throws Exception {
+ // Arrange
+ byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
+ final String PLAINTEXT = new String(plainBytes, "UTF-8");
+
+ InputStream cipherStream = new FileInputStream(encryptedFile);
+ OutputStream recoveredStream = new ByteArrayOutputStream();
+
+ // No file, just streams
+ String filename = encryptedFile.getName();
+
+ OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), LEGACY_PASSWORD.toCharArray(), filename);
+
+ StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
+
+ // Act
+ decryptionCallback.process(cipherStream, recoveredStream);
+
+ // Assert
+ byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: {}", recovered);
+ Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
+ }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/pubring.gpg b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/pubring.gpg
new file mode 100644
index 0000000000..c32ef145f1
Binary files /dev/null and b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/pubring.gpg differ
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/secring.gpg b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/secring.gpg
new file mode 100644
index 0000000000..26af32ab31
Binary files /dev/null and b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/secring.gpg differ
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/text.txt.gpg b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/text.txt.gpg
new file mode 100644
index 0000000000..0b78a99daf
Binary files /dev/null and b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/text.txt.gpg differ
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/text.txt.unsigned.gpg b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/text.txt.unsigned.gpg
new file mode 100644
index 0000000000..c7bba1cb89
Binary files /dev/null and b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestEncryptContent/text.txt.unsigned.gpg differ
diff --git a/pom.xml b/pom.xml
index 2af004d6d3..3e3aa5902d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -241,13 +241,18 @@ language governing permissions and limitations under the License. -->