mirror of https://github.com/apache/nifi.git
NIFI-7322 Added SignContentPGP and VerifyContentPGP
- Added Decryption Strategy property to DecryptContentPGP - Added OpenPGP Packet detection to EncryptContentPGP to avoid unnecessary packaging - Refactored shared processing to EncodingStreamCallback Signed-off-by: Joe Gresock <jgresock@gmail.com> This closes #5457.
This commit is contained in:
parent
70274ebcfc
commit
da1b1dfd4b
|
@ -22,6 +22,7 @@ import org.apache.nifi.annotation.behavior.WritesAttributes;
|
|||
import org.apache.nifi.annotation.documentation.CapabilityDescription;
|
||||
import org.apache.nifi.annotation.documentation.SeeAlso;
|
||||
import org.apache.nifi.annotation.documentation.Tags;
|
||||
import org.apache.nifi.components.AllowableValue;
|
||||
import org.apache.nifi.components.PropertyDescriptor;
|
||||
import org.apache.nifi.components.PropertyValue;
|
||||
import org.apache.nifi.components.ValidationContext;
|
||||
|
@ -34,8 +35,10 @@ 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.DecryptionStrategy;
|
||||
import org.apache.nifi.processors.pgp.exception.PGPDecryptionException;
|
||||
import org.apache.nifi.processors.pgp.exception.PGPProcessException;
|
||||
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
|
||||
import org.apache.nifi.stream.io.StreamUtils;
|
||||
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
|
@ -76,11 +79,12 @@ import java.util.Set;
|
|||
*/
|
||||
@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
|
||||
@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "RFC 4880"})
|
||||
@CapabilityDescription("Decrypt Contents of OpenPGP Messages")
|
||||
@SeeAlso(EncryptContentPGP.class)
|
||||
@CapabilityDescription("Decrypt contents of OpenPGP messages. Using the Packaged Decryption Strategy preserves OpenPGP encoding to support subsequent signature verification.")
|
||||
@SeeAlso({EncryptContentPGP.class, SignContentPGP.class, VerifyContentPGP.class})
|
||||
@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_BLOCK_CIPHER, description = "Symmetric-Key Algorithm Block Cipher"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, description = "Symmetric-Key Algorithm Identifier")
|
||||
})
|
||||
public class DecryptContentPGP extends AbstractProcessor {
|
||||
|
@ -95,6 +99,19 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
.description("Decryption Failed")
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor DECRYPTION_STRATEGY = new PropertyDescriptor.Builder()
|
||||
.name("decryption-strategy")
|
||||
.displayName("Decryption Strategy")
|
||||
.description("Strategy for writing files to success after decryption")
|
||||
.required(true)
|
||||
.defaultValue(DecryptionStrategy.DECRYPTED.name())
|
||||
.allowableValues(
|
||||
Arrays.stream(DecryptionStrategy.values()).map(strategy ->
|
||||
new AllowableValue(strategy.name(), strategy.name(), strategy.getDescription())
|
||||
).toArray(AllowableValue[]::new)
|
||||
)
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor PASSPHRASE = new PropertyDescriptor.Builder()
|
||||
.name("passphrase")
|
||||
.displayName("Passphrase")
|
||||
|
@ -113,6 +130,7 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(Arrays.asList(SUCCESS, FAILURE));
|
||||
|
||||
private static final List<PropertyDescriptor> DESCRIPTORS = Arrays.asList(
|
||||
DECRYPTION_STRATEGY,
|
||||
PASSPHRASE,
|
||||
PRIVATE_KEY_SERVICE
|
||||
);
|
||||
|
@ -156,7 +174,8 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
|
||||
final char[] passphrase = getPassphrase(context);
|
||||
final PGPPrivateKeyService privateKeyService = getPrivateKeyService(context);
|
||||
final DecryptStreamCallback callback = new DecryptStreamCallback(passphrase, privateKeyService);
|
||||
final DecryptionStrategy decryptionStrategy = getDecryptionStrategy(context);
|
||||
final DecryptStreamCallback callback = new DecryptStreamCallback(passphrase, privateKeyService, decryptionStrategy);
|
||||
|
||||
try {
|
||||
flowFile = session.write(flowFile, callback);
|
||||
|
@ -213,16 +232,26 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
return privateKeyService;
|
||||
}
|
||||
|
||||
private DecryptionStrategy getDecryptionStrategy(final ProcessContext context) {
|
||||
final String strategy = context.getProperty(DECRYPTION_STRATEGY).getValue();
|
||||
return DecryptionStrategy.valueOf(strategy);
|
||||
}
|
||||
|
||||
private class DecryptStreamCallback implements StreamCallback {
|
||||
private final char[] passphrase;
|
||||
|
||||
private final PGPPrivateKeyService privateKeyService;
|
||||
|
||||
private final DecryptionStrategy decryptionStrategy;
|
||||
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
public DecryptStreamCallback(final char[] passphrase, final PGPPrivateKeyService privateKeyService) {
|
||||
public DecryptStreamCallback(final char[] passphrase,
|
||||
final PGPPrivateKeyService privateKeyService,
|
||||
final DecryptionStrategy decryptionStrategy) {
|
||||
this.passphrase = passphrase;
|
||||
this.privateKeyService = privateKeyService;
|
||||
this.decryptionStrategy = decryptionStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,13 +265,23 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
public void process(final InputStream inputStream, final OutputStream outputStream) throws IOException {
|
||||
final PGPEncryptedDataList encryptedDataList = getEncryptedDataList(inputStream);
|
||||
final PGPEncryptedData encryptedData = findSupportedEncryptedData(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()));
|
||||
if (DecryptionStrategy.PACKAGED == decryptionStrategy) {
|
||||
try {
|
||||
final InputStream decryptedDataStream = getDecryptedDataStream(encryptedData);
|
||||
StreamUtils.copy(decryptedDataStream, outputStream);
|
||||
} catch (final PGPException e) {
|
||||
final String message = String.format("PGP Decryption Failed [%s]", getEncryptedDataType(encryptedData));
|
||||
throw new PGPDecryptionException(message, e);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
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");
|
||||
|
@ -286,7 +325,7 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
final Optional<PGPPrivateKey> privateKey = privateKeyService.findPrivateKey(keyId);
|
||||
if (privateKey.isPresent()) {
|
||||
supportedEncryptedData = publicKeyEncryptedData;
|
||||
final String keyIdentifier = Long.toHexString(keyId).toUpperCase();
|
||||
final String keyIdentifier = KeyIdentifierConverter.format(keyId);
|
||||
getLogger().debug("PGP Private Key [{}] Found for Public Key Encrypted Data", keyIdentifier);
|
||||
break;
|
||||
}
|
||||
|
@ -354,7 +393,7 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
} 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));
|
||||
setSymmetricKeyAlgorithmAttributes(symmetricAlgorithm);
|
||||
return passwordBasedEncryptedData.getDataStream(decryptorFactory);
|
||||
}
|
||||
}
|
||||
|
@ -369,16 +408,22 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
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));
|
||||
setSymmetricKeyAlgorithmAttributes(symmetricAlgorithm);
|
||||
return publicKeyEncryptedData.getDataStream(decryptorFactory);
|
||||
} else {
|
||||
final String keyIdentifier = Long.toHexString(keyId).toUpperCase();
|
||||
final String keyIdentifier = KeyIdentifierConverter.format(keyId);
|
||||
final String message = String.format("PGP Private Key [%s] not found for Public Key Encryption", keyIdentifier);
|
||||
throw new PGPDecryptionException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setSymmetricKeyAlgorithmAttributes(final int symmetricAlgorithm) {
|
||||
final String blockCipher = PGPUtil.getSymmetricCipherName(symmetricAlgorithm);
|
||||
attributes.put(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_BLOCK_CIPHER, blockCipher);
|
||||
attributes.put(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, Integer.toString(symmetricAlgorithm));
|
||||
}
|
||||
|
||||
private boolean isVerified(final PGPEncryptedData encryptedData) {
|
||||
boolean verified;
|
||||
|
||||
|
@ -399,8 +444,7 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
|
||||
private PGPEncryptedDataList getEncryptedDataList(final InputStream inputStream) throws IOException {
|
||||
final InputStream decoderInputStream = PGPUtil.getDecoderStream(inputStream);
|
||||
final PGPObjectFactory encryptedObjectFactory = new JcaPGPObjectFactory(decoderInputStream);
|
||||
final PGPEncryptedDataList encryptedDataList = findEncryptedDataList(encryptedObjectFactory);
|
||||
final PGPEncryptedDataList encryptedDataList = findEncryptedDataList(decoderInputStream);
|
||||
if (encryptedDataList == null) {
|
||||
throw new PGPProcessException("PGP Encrypted Data Packets not found");
|
||||
} else {
|
||||
|
@ -409,9 +453,10 @@ public class DecryptContentPGP extends AbstractProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private PGPEncryptedDataList findEncryptedDataList(final PGPObjectFactory objectFactory) {
|
||||
private PGPEncryptedDataList findEncryptedDataList(final InputStream inputStream) {
|
||||
PGPEncryptedDataList encryptedDataList = null;
|
||||
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(inputStream);
|
||||
for (final Object object : objectFactory) {
|
||||
getLogger().debug("PGP Object Read [{}]", object.getClass().getSimpleName());
|
||||
if (object instanceof PGPEncryptedDataList) {
|
||||
|
|
|
@ -34,22 +34,23 @@ 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.InputStreamCallback;
|
||||
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.processors.pgp.io.EncodingStreamCallback;
|
||||
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.bcpg.BCPGInputStream;
|
||||
import org.bouncycastle.bcpg.Packet;
|
||||
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.PGPUtil;
|
||||
import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder;
|
||||
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder;
|
||||
|
@ -63,7 +64,6 @@ 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;
|
||||
|
@ -76,8 +76,8 @@ import java.util.Set;
|
|||
*/
|
||||
@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
|
||||
@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "RFC 4880"})
|
||||
@CapabilityDescription("Encrypt Contents using OpenPGP")
|
||||
@SeeAlso(DecryptContentPGP.class)
|
||||
@CapabilityDescription("Encrypt contents using OpenPGP. The processor reads input and detects OpenPGP messages to avoid unnecessary additional wrapping in Literal Data packets.")
|
||||
@SeeAlso({ DecryptContentPGP.class, SignContentPGP.class, VerifyContentPGP.class })
|
||||
@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"),
|
||||
|
@ -153,8 +153,6 @@ public class EncryptContentPGP extends AbstractProcessor {
|
|||
/** 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(
|
||||
|
@ -200,10 +198,14 @@ public class EncryptContentPGP extends AbstractProcessor {
|
|||
}
|
||||
|
||||
try {
|
||||
final PacketReadInputStreamCallback packetCallback = new PacketReadInputStreamCallback();
|
||||
session.read(flowFile, packetCallback);
|
||||
|
||||
final SymmetricKeyAlgorithm symmetricKeyAlgorithm = getSymmetricKeyAlgorithm(context);
|
||||
final FileEncoding fileEncoding = getFileEncoding(context);
|
||||
final CompressionAlgorithm compressionAlgorithm = getCompressionAlgorithm(context);
|
||||
final StreamCallback callback = getEncryptStreamCallback(context, flowFile, symmetricKeyAlgorithm, compressionAlgorithm, fileEncoding);
|
||||
final StreamCallback callback = getEncryptStreamCallback(context, flowFile, symmetricKeyAlgorithm,
|
||||
compressionAlgorithm, fileEncoding, packetCallback.packetFound);
|
||||
flowFile = session.write(flowFile, callback);
|
||||
|
||||
final Map<String, String> attributes = getAttributes(symmetricKeyAlgorithm, fileEncoding, compressionAlgorithm);
|
||||
|
@ -261,10 +263,12 @@ public class EncryptContentPGP extends AbstractProcessor {
|
|||
return results;
|
||||
}
|
||||
|
||||
private StreamCallback getEncryptStreamCallback(final ProcessContext context, final FlowFile flowFile,
|
||||
private StreamCallback getEncryptStreamCallback(final ProcessContext context,
|
||||
final FlowFile flowFile,
|
||||
final SymmetricKeyAlgorithm symmetricKeyAlgorithm,
|
||||
final CompressionAlgorithm compressionAlgorithm,
|
||||
final FileEncoding fileEncoding) {
|
||||
final FileEncoding fileEncoding,
|
||||
final boolean packetFound) {
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final PGPDataEncryptorBuilder dataEncryptorBuilder = new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getId())
|
||||
.setSecureRandom(secureRandom)
|
||||
|
@ -274,7 +278,7 @@ public class EncryptContentPGP extends AbstractProcessor {
|
|||
methodGenerators.forEach(encryptedDataGenerator::addMethod);
|
||||
|
||||
final String filename = flowFile.getAttribute(CoreAttributes.FILENAME.key());
|
||||
return new EncryptStreamCallback(filename, fileEncoding, encryptedDataGenerator, compressionAlgorithm);
|
||||
return new EncryptStreamCallback(fileEncoding, compressionAlgorithm, filename, packetFound, encryptedDataGenerator);
|
||||
}
|
||||
|
||||
private List<PGPKeyEncryptionMethodGenerator> getEncryptionMethodGenerators(final ProcessContext context,
|
||||
|
@ -334,65 +338,65 @@ public class EncryptContentPGP extends AbstractProcessor {
|
|||
return attributes;
|
||||
}
|
||||
|
||||
private static class EncryptStreamCallback implements StreamCallback {
|
||||
private static final char DATA_FORMAT = PGPLiteralData.BINARY;
|
||||
private class PacketReadInputStreamCallback implements InputStreamCallback {
|
||||
private boolean packetFound;
|
||||
|
||||
private final String filename;
|
||||
/**
|
||||
* Process Input Stream and attempt to read OpenPGP Packet for content detection
|
||||
*
|
||||
* @param inputStream Input Stream to be read
|
||||
*/
|
||||
@Override
|
||||
public void process(final InputStream inputStream) {
|
||||
try {
|
||||
final InputStream decodedInputStream = PGPUtil.getDecoderStream(inputStream);
|
||||
final BCPGInputStream packetInputStream = new BCPGInputStream(decodedInputStream);
|
||||
final Packet packet = packetInputStream.readPacket();
|
||||
if (packet == null) {
|
||||
getLogger().debug("PGP Packet not found");
|
||||
} else {
|
||||
packetFound = true;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
getLogger().debug("PGP Packet read failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final FileEncoding fileEncoding;
|
||||
private static class EncryptStreamCallback extends EncodingStreamCallback {
|
||||
private final boolean packetFound;
|
||||
|
||||
private final PGPEncryptedDataGenerator encryptedDataGenerator;
|
||||
|
||||
private final CompressionAlgorithm compressionAlgorithm;
|
||||
|
||||
public EncryptStreamCallback(final String filename, final FileEncoding fileEncoding, final PGPEncryptedDataGenerator encryptedDataGenerator, final CompressionAlgorithm compressionAlgorithm) {
|
||||
this.filename = filename;
|
||||
this.fileEncoding = fileEncoding;
|
||||
public EncryptStreamCallback(final FileEncoding fileEncoding,
|
||||
final CompressionAlgorithm compressionAlgorithm,
|
||||
final String filename,
|
||||
final boolean packetFound,
|
||||
final PGPEncryptedDataGenerator encryptedDataGenerator) {
|
||||
super(fileEncoding, compressionAlgorithm, filename);
|
||||
this.packetFound = packetFound;
|
||||
this.encryptedDataGenerator = encryptedDataGenerator;
|
||||
this.compressionAlgorithm = compressionAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Input Stream and write encrypted contents to Output Stream
|
||||
* Process Encoding Output Stream using Encrypted Data Generator with for subsequent processing
|
||||
*
|
||||
* @param inputStream Input Stream
|
||||
* @param outputStream Output Stream for encrypted contents
|
||||
* @throws IOException Thrown when unable to read or write streams
|
||||
* @param inputStream Input Stream
|
||||
* @param encodingOutputStream Output Stream to be processed for encryption
|
||||
* @throws IOException Thrown when failing to read or write streams
|
||||
* @throws PGPException Thrown when failing to perform encryption operations
|
||||
*/
|
||||
@Override
|
||||
public void process(final InputStream inputStream, final OutputStream outputStream) throws IOException {
|
||||
try (final OutputStream encodingOutputStream = getEncodingOutputStream(outputStream)) {
|
||||
processEncoding(inputStream, encodingOutputStream);
|
||||
} catch (final PGPException e) {
|
||||
throw new PGPEncryptionException("PGP Encryption Stream Processing Failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getEncodingOutputStream(final OutputStream outputStream) throws PGPException {
|
||||
OutputStream encodingOutputStream = outputStream;
|
||||
if (FileEncoding.ASCII.equals(fileEncoding)) {
|
||||
encodingOutputStream = new ArmoredOutputStream(outputStream);
|
||||
}
|
||||
return encodingOutputStream;
|
||||
}
|
||||
|
||||
private void processEncoding(final InputStream inputStream, final OutputStream encodingOutputStream) throws IOException, PGPException {
|
||||
try (final OutputStream encryptedOutputStream = encryptedDataGenerator.open(encodingOutputStream, createBuffer())) {
|
||||
final PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm.getId());
|
||||
try (final OutputStream compressedOutputStream = compressedDataGenerator.open(encryptedOutputStream, createBuffer())) {
|
||||
final PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
|
||||
try (final OutputStream literalOutputStream = literalDataGenerator.open(compressedOutputStream, DATA_FORMAT, filename, new Date(), createBuffer())) {
|
||||
StreamUtils.copy(inputStream, literalOutputStream);
|
||||
}
|
||||
literalDataGenerator.close();
|
||||
protected void processEncoding(final InputStream inputStream, final OutputStream encodingOutputStream) throws IOException, PGPException {
|
||||
try (final OutputStream encryptedOutputStream = encryptedDataGenerator.open(encodingOutputStream, createOutputBuffer())) {
|
||||
if (packetFound) {
|
||||
// Write OpenPGP packets to encrypted stream without additional encoding
|
||||
StreamUtils.copy(inputStream, encryptedOutputStream);
|
||||
} else {
|
||||
super.processEncoding(inputStream, encryptedOutputStream);
|
||||
}
|
||||
compressedDataGenerator.close();
|
||||
}
|
||||
encryptedDataGenerator.close();
|
||||
}
|
||||
|
||||
private byte[] createBuffer() {
|
||||
return new byte[OUTPUT_BUFFER_SIZE];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,4 +37,18 @@ class PGPAttributeKey {
|
|||
static final String COMPRESS_ALGORITHM = "pgp.compression.algorithm";
|
||||
|
||||
static final String COMPRESS_ALGORITHM_ID = "pgp.compression.algorithm.id";
|
||||
|
||||
static final String SIGNATURE_CREATED = "pgp.signature.created";
|
||||
|
||||
static final String SIGNATURE_ALGORITHM = "pgp.signature.algorithm";
|
||||
|
||||
static final String SIGNATURE_HASH_ALGORITHM_ID = "pgp.signature.hash.algorithm.id";
|
||||
|
||||
static final String SIGNATURE_KEY_ALGORITHM_ID = "pgp.signature.key.algorithm.id";
|
||||
|
||||
static final String SIGNATURE_KEY_ID = "pgp.signature.key.id";
|
||||
|
||||
static final String SIGNATURE_TYPE_ID = "pgp.signature.type.id";
|
||||
|
||||
static final String SIGNATURE_VERSION = "pgp.signature.version";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,408 @@
|
|||
/*
|
||||
* 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.SeeAlso;
|
||||
import org.apache.nifi.annotation.documentation.Tags;
|
||||
import org.apache.nifi.components.AllowableValue;
|
||||
import org.apache.nifi.components.PropertyDescriptor;
|
||||
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.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.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.HashAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.attributes.SigningStrategy;
|
||||
import org.apache.nifi.processors.pgp.exception.PGPProcessException;
|
||||
import org.apache.nifi.processors.pgp.io.EncodingStreamCallback;
|
||||
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Sign Content using Open Pretty Good Privacy Private Keys
|
||||
*/
|
||||
@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
|
||||
@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "Signing", "RFC 4880"})
|
||||
@CapabilityDescription("Sign content using OpenPGP Private Keys")
|
||||
@SeeAlso({DecryptContentPGP.class, EncryptContentPGP.class, VerifyContentPGP.class})
|
||||
@WritesAttributes({
|
||||
@WritesAttribute(attribute = PGPAttributeKey.COMPRESS_ALGORITHM, description = "Compression Algorithm"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.COMPRESS_ALGORITHM_ID, description = "Compression Algorithm Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.FILE_ENCODING, description = "File Encoding"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_ALGORITHM, description = "Signature Algorithm including key and hash algorithm names"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, description = "Signature Hash Algorithm Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, description = "Signature Key Algorithm Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_KEY_ID, description = "Signature Public Key Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_TYPE_ID, description = "Signature Type Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_VERSION, description = "Signature Version Number"),
|
||||
})
|
||||
public class SignContentPGP extends AbstractProcessor {
|
||||
|
||||
public static final Relationship SUCCESS = new Relationship.Builder()
|
||||
.name("success")
|
||||
.description("Content signing succeeded")
|
||||
.build();
|
||||
|
||||
public static final Relationship FAILURE = new Relationship.Builder()
|
||||
.name("failure")
|
||||
.description("Content signing failed")
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor COMPRESSION_ALGORITHM = new PropertyDescriptor.Builder()
|
||||
.name("compression-algorithm")
|
||||
.displayName("Compression Algorithm")
|
||||
.description("Compression Algorithm for signing")
|
||||
.required(true)
|
||||
.defaultValue(CompressionAlgorithm.ZIP.name())
|
||||
.allowableValues(CompressionAlgorithm.values())
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor FILE_ENCODING = new PropertyDescriptor.Builder()
|
||||
.name("file-encoding")
|
||||
.displayName("File Encoding")
|
||||
.description("File Encoding for signing")
|
||||
.required(true)
|
||||
.defaultValue(FileEncoding.BINARY.name())
|
||||
.allowableValues(FileEncoding.values())
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor HASH_ALGORITHM = new PropertyDescriptor.Builder()
|
||||
.name("hash-algorithm")
|
||||
.displayName("Hash Algorithm")
|
||||
.description("Hash Algorithm for signing")
|
||||
.required(true)
|
||||
.defaultValue(HashAlgorithm.SHA512.name())
|
||||
.allowableValues(HashAlgorithm.values())
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor SIGNING_STRATEGY = new PropertyDescriptor.Builder()
|
||||
.name("signing-strategy")
|
||||
.displayName("Signing Strategy")
|
||||
.description("Strategy for writing files to success after signing")
|
||||
.required(true)
|
||||
.defaultValue(SigningStrategy.SIGNED.name())
|
||||
.allowableValues(
|
||||
Arrays.stream(SigningStrategy.values()).map(strategy ->
|
||||
new AllowableValue(strategy.name(), strategy.name(), strategy.getDescription())
|
||||
).toArray(AllowableValue[]::new)
|
||||
)
|
||||
.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 generating content signatures")
|
||||
.identifiesControllerService(PGPPrivateKeyService.class)
|
||||
.required(true)
|
||||
.build();
|
||||
|
||||
public static final PropertyDescriptor PRIVATE_KEY_ID = new PropertyDescriptor.Builder()
|
||||
.name("private-key-id")
|
||||
.displayName("Private Key ID")
|
||||
.description("PGP Private Key Identifier formatted as uppercase hexadecimal string of 16 characters used for signing")
|
||||
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||
.addValidator(StandardValidators.NON_EMPTY_EL_VALIDATOR)
|
||||
.required(true)
|
||||
.build();
|
||||
|
||||
private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(Arrays.asList(SUCCESS, FAILURE));
|
||||
|
||||
private static final List<PropertyDescriptor> DESCRIPTORS = Arrays.asList(
|
||||
COMPRESSION_ALGORITHM,
|
||||
FILE_ENCODING,
|
||||
HASH_ALGORITHM,
|
||||
SIGNING_STRATEGY,
|
||||
PRIVATE_KEY_SERVICE,
|
||||
PRIVATE_KEY_ID
|
||||
);
|
||||
|
||||
private static final boolean NESTED_SIGNATURE_DISABLED = false;
|
||||
|
||||
/**
|
||||
* 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 generates signatures for Flow File contents using private keys
|
||||
*
|
||||
* @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 SignatureStreamCallback callback = getStreamCallback(context, flowFile);
|
||||
flowFile = session.write(flowFile, callback);
|
||||
flowFile = session.putAllAttributes(flowFile, callback.attributes);
|
||||
session.transfer(flowFile, SUCCESS);
|
||||
} catch (final RuntimeException e) {
|
||||
getLogger().error("Signing Failed {}", flowFile, e);
|
||||
session.transfer(flowFile, FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
private SignatureStreamCallback getStreamCallback(final ProcessContext context, final FlowFile flowFile) {
|
||||
final FileEncoding fileEncoding = getFileEncoding(context);
|
||||
final CompressionAlgorithm compressionAlgorithm = getCompressionAlgorithm(context);
|
||||
final HashAlgorithm hashAlgorithm = getHashAlgorithm(context);
|
||||
final String filename = flowFile.getAttribute(CoreAttributes.FILENAME.key());
|
||||
final SigningStrategy signingStrategy = getSigningStrategy(context);
|
||||
final PGPPrivateKey privateKey = getPrivateKey(context, flowFile);
|
||||
return SigningStrategy.SIGNED.equals(signingStrategy)
|
||||
? new SignedStreamCallback(fileEncoding, compressionAlgorithm, filename, hashAlgorithm, privateKey)
|
||||
: new DetachedStreamCallback(fileEncoding, compressionAlgorithm, filename, hashAlgorithm, privateKey);
|
||||
}
|
||||
|
||||
private PGPPrivateKey getPrivateKey(final ProcessContext context, final FlowFile flowFile) {
|
||||
final PGPPrivateKeyService privateKeyService = context.getProperty(PRIVATE_KEY_SERVICE).asControllerService(PGPPrivateKeyService.class);
|
||||
final long privateKeyId = getPrivateKeyId(context, flowFile);
|
||||
final Optional<PGPPrivateKey> optionalPrivateKey = privateKeyService.findPrivateKey(privateKeyId);
|
||||
|
||||
return optionalPrivateKey.orElseThrow(() -> {
|
||||
final String message = String.format("Private Key ID [%s] not found", KeyIdentifierConverter.format(privateKeyId));
|
||||
return new PGPProcessException(message);
|
||||
});
|
||||
}
|
||||
|
||||
private long getPrivateKeyId(final ProcessContext context, final FlowFile flowFile) {
|
||||
final String privateKeyId = context.getProperty(PRIVATE_KEY_ID).evaluateAttributeExpressions(flowFile).getValue();
|
||||
try {
|
||||
return KeyIdentifierConverter.parse(privateKeyId);
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new PGPProcessException(String.format("Private Key ID [%s] Hexadecimal Parsing Failed", privateKeyId), e);
|
||||
}
|
||||
}
|
||||
|
||||
private CompressionAlgorithm getCompressionAlgorithm(final ProcessContext context) {
|
||||
final String algorithm = context.getProperty(COMPRESSION_ALGORITHM).getValue();
|
||||
return CompressionAlgorithm.valueOf(algorithm);
|
||||
}
|
||||
|
||||
private FileEncoding getFileEncoding(final ProcessContext context) {
|
||||
final String encoding = context.getProperty(FILE_ENCODING).getValue();
|
||||
return FileEncoding.valueOf(encoding);
|
||||
}
|
||||
|
||||
private HashAlgorithm getHashAlgorithm(final ProcessContext context) {
|
||||
final String algorithm = context.getProperty(HASH_ALGORITHM).getValue();
|
||||
return HashAlgorithm.valueOf(algorithm);
|
||||
}
|
||||
|
||||
private SigningStrategy getSigningStrategy(final ProcessContext context) {
|
||||
final String strategy = context.getProperty(SIGNING_STRATEGY).getValue();
|
||||
return SigningStrategy.valueOf(strategy);
|
||||
}
|
||||
|
||||
private class SignatureStreamCallback extends EncodingStreamCallback {
|
||||
private final PGPPrivateKey privateKey;
|
||||
|
||||
private final HashAlgorithm hashAlgorithm;
|
||||
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
protected SignatureStreamCallback(final FileEncoding fileEncoding,
|
||||
final CompressionAlgorithm compressionAlgorithm,
|
||||
final String filename,
|
||||
final HashAlgorithm hashAlgorithm,
|
||||
final PGPPrivateKey privateKey
|
||||
) {
|
||||
super(fileEncoding, compressionAlgorithm, filename);
|
||||
this.hashAlgorithm = hashAlgorithm;
|
||||
this.privateKey = privateKey;
|
||||
|
||||
attributes.put(PGPAttributeKey.COMPRESS_ALGORITHM, compressionAlgorithm.toString());
|
||||
attributes.put(PGPAttributeKey.COMPRESS_ALGORITHM_ID, Integer.toString(compressionAlgorithm.getId()));
|
||||
attributes.put(PGPAttributeKey.FILE_ENCODING, fileEncoding.toString());
|
||||
}
|
||||
|
||||
/***
|
||||
* Write Signature to Output Stream
|
||||
*
|
||||
* @param signatureGenerator Signature Generator initialized with Private Key
|
||||
* @param outputStream Output Stream for writing encoded signature
|
||||
* @throws PGPException Thrown when failing to generate signature
|
||||
* @throws IOException Thrown when failing to write signature
|
||||
*/
|
||||
protected void writeSignature(final PGPSignatureGenerator signatureGenerator, final OutputStream outputStream) throws PGPException, IOException {
|
||||
final PGPSignature signature = signatureGenerator.generate();
|
||||
signature.encode(outputStream);
|
||||
setSignatureAttributes(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Signature Generator initialized using configuration properties and Private Key
|
||||
* @return Initialized Signature Generator
|
||||
* @throws PGPException Thrown when failing to initialize signature generator
|
||||
*/
|
||||
protected PGPSignatureGenerator getSignatureGenerator() throws PGPException {
|
||||
final int keyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm();
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final JcaPGPContentSignerBuilder builder = new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm.getId()).setSecureRandom(secureRandom);
|
||||
final PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(builder);
|
||||
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
|
||||
return signatureGenerator;
|
||||
}
|
||||
|
||||
private void setSignatureAttributes(final PGPSignature signature) {
|
||||
setSignatureAlgorithm(signature.getKeyAlgorithm(), signature.getHashAlgorithm());
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_CREATED, Long.toString(signature.getCreationTime().getTime()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_KEY_ID, KeyIdentifierConverter.format(signature.getKeyID()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(signature.getSignatureType()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_VERSION, Integer.toString(signature.getVersion()));
|
||||
}
|
||||
|
||||
private void setSignatureAlgorithm(final int keyAlgorithm, final int hashAlgorithm) {
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, Integer.toString(hashAlgorithm));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, Integer.toString(keyAlgorithm));
|
||||
try {
|
||||
final String algorithm = PGPUtil.getSignatureName(keyAlgorithm, hashAlgorithm);
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_ALGORITHM, algorithm);
|
||||
} catch (final PGPException e) {
|
||||
getLogger().debug("Signature Algorithm Key Identifier [{}] Hash Identifier [{}] not found", keyAlgorithm, hashAlgorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DetachedStreamCallback extends SignatureStreamCallback {
|
||||
private DetachedStreamCallback(final FileEncoding fileEncoding,
|
||||
final CompressionAlgorithm compressionAlgorithm,
|
||||
final String filename,
|
||||
final HashAlgorithm hashAlgorithm,
|
||||
final PGPPrivateKey privateKey
|
||||
) {
|
||||
super(fileEncoding, compressionAlgorithm, filename, hashAlgorithm, privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Encoding writes detached signature through Compression Output Stream
|
||||
*
|
||||
* @param inputStream Input Stream
|
||||
* @param encodingOutputStream Output Stream configured according to File Encoding
|
||||
* @throws IOException Thrown when unable to read or write streams
|
||||
* @throws PGPException Thrown when unable to process compression
|
||||
*/
|
||||
@Override
|
||||
protected void processEncoding(final InputStream inputStream, final OutputStream encodingOutputStream) throws IOException, PGPException {
|
||||
processDetached(inputStream, encodingOutputStream);
|
||||
}
|
||||
|
||||
private void processDetached(final InputStream inputStream, final OutputStream outputStream) throws IOException, PGPException {
|
||||
final PGPSignatureGenerator signatureGenerator = getSignatureGenerator();
|
||||
final byte[] buffer = createOutputBuffer();
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) >= 0) {
|
||||
signatureGenerator.update(buffer, 0, read);
|
||||
}
|
||||
writeSignature(signatureGenerator, outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private class SignedStreamCallback extends SignatureStreamCallback {
|
||||
|
||||
private SignedStreamCallback(final FileEncoding fileEncoding,
|
||||
final CompressionAlgorithm compressionAlgorithm,
|
||||
final String filename,
|
||||
final HashAlgorithm hashAlgorithm,
|
||||
final PGPPrivateKey privateKey
|
||||
) {
|
||||
super(fileEncoding, compressionAlgorithm, filename, hashAlgorithm, privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Compression passing Input Stream through Literal Data Output Stream prepended with One-Pass Signature and followed with Signature
|
||||
*
|
||||
* @param inputStream Input Stream
|
||||
* @param compressedOutputStream Output Stream configured according to Compression Algorithm
|
||||
* @throws IOException Thrown when unable to read or write streams
|
||||
* @throws PGPException Thrown when unable to generate signatures
|
||||
*/
|
||||
@Override
|
||||
protected void processCompression(final InputStream inputStream, final OutputStream compressedOutputStream) throws IOException, PGPException {
|
||||
final PGPSignatureGenerator signatureGenerator = getSignatureGenerator();
|
||||
|
||||
final PGPOnePassSignature onePassSignature = signatureGenerator.generateOnePassVersion(NESTED_SIGNATURE_DISABLED);
|
||||
onePassSignature.encode(compressedOutputStream);
|
||||
|
||||
final PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
|
||||
try (final OutputStream literalOutputStream = openLiteralOutputStream(literalDataGenerator, compressedOutputStream)) {
|
||||
processSigned(inputStream, literalOutputStream, signatureGenerator);
|
||||
}
|
||||
literalDataGenerator.close();
|
||||
|
||||
writeSignature(signatureGenerator, compressedOutputStream);
|
||||
}
|
||||
|
||||
private void processSigned(final InputStream inputStream, final OutputStream outputStream, final PGPSignatureGenerator signatureGenerator) throws IOException {
|
||||
final byte[] buffer = createOutputBuffer();
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) >= 0) {
|
||||
outputStream.write(buffer, 0, read);
|
||||
signatureGenerator.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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.SeeAlso;
|
||||
import org.apache.nifi.annotation.documentation.Tags;
|
||||
import org.apache.nifi.components.PropertyDescriptor;
|
||||
import org.apache.nifi.flowfile.FlowFile;
|
||||
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.processors.pgp.exception.PGPProcessException;
|
||||
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
|
||||
import org.apache.nifi.stream.io.StreamUtils;
|
||||
import org.bouncycastle.openpgp.PGPCompressedData;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPLiteralData;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Verify Content using Open Pretty Good Privacy Public Keys
|
||||
*/
|
||||
@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
|
||||
@Tags({"PGP", "GPG", "OpenPGP", "Encryption", "Signing", "RFC 4880"})
|
||||
@CapabilityDescription("Verify signatures using OpenPGP Public Keys")
|
||||
@SeeAlso({ DecryptContentPGP.class, EncryptContentPGP.class, SignContentPGP.class })
|
||||
@WritesAttributes({
|
||||
@WritesAttribute(attribute = PGPAttributeKey.LITERAL_DATA_FILENAME, description = "Filename from Literal Data"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.LITERAL_DATA_MODIFIED, description = "Modified Date Time from Literal Data in milliseconds"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_CREATED, description = "Signature Creation Time in milliseconds"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_ALGORITHM, description = "Signature Algorithm including key and hash algorithm names"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, description = "Signature Hash Algorithm Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, description = "Signature Key Algorithm Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_KEY_ID, description = "Signature Public Key Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_TYPE_ID, description = "Signature Type Identifier"),
|
||||
@WritesAttribute(attribute = PGPAttributeKey.SIGNATURE_VERSION, description = "Signature Version Number"),
|
||||
})
|
||||
public class VerifyContentPGP extends AbstractProcessor {
|
||||
|
||||
public static final Relationship SUCCESS = new Relationship.Builder()
|
||||
.name("success")
|
||||
.description("Signature Verification Succeeded")
|
||||
.build();
|
||||
|
||||
public static final Relationship FAILURE = new Relationship.Builder()
|
||||
.name("failure")
|
||||
.description("Signature Verification Failed")
|
||||
.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 verifying signatures with Public Key Encryption")
|
||||
.identifiesControllerService(PGPPublicKeyService.class)
|
||||
.required(true)
|
||||
.build();
|
||||
|
||||
private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(Arrays.asList(SUCCESS, FAILURE));
|
||||
|
||||
private static final List<PropertyDescriptor> DESCRIPTORS = Collections.singletonList(
|
||||
PUBLIC_KEY_SERVICE
|
||||
);
|
||||
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private static final String KEY_ID_UNKNOWN = "UNKNOWN";
|
||||
|
||||
/**
|
||||
* 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 verifies signatures found in 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 PGPPublicKeyService publicKeyService = context.getProperty(PUBLIC_KEY_SERVICE).asControllerService(PGPPublicKeyService.class);
|
||||
final VerifyStreamCallback callback = new VerifyStreamCallback(publicKeyService);
|
||||
try {
|
||||
flowFile = session.write(flowFile, callback);
|
||||
flowFile = session.putAllAttributes(flowFile, callback.attributes);
|
||||
final String keyId = flowFile.getAttribute(PGPAttributeKey.SIGNATURE_KEY_ID);
|
||||
getLogger().info("Signature Key ID [{}] Verification Completed {}", keyId, flowFile);
|
||||
session.transfer(flowFile, SUCCESS);
|
||||
} catch (final RuntimeException e) {
|
||||
flowFile = session.putAllAttributes(flowFile, callback.attributes);
|
||||
getLogger().error("Processing Failed {}", flowFile, e);
|
||||
session.transfer(flowFile, FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
private class VerifyStreamCallback implements StreamCallback {
|
||||
private final PGPPublicKeyService publicKeyService;
|
||||
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
private boolean verified;
|
||||
|
||||
private VerifyStreamCallback(final PGPPublicKeyService publicKeyService) {
|
||||
this.publicKeyService = publicKeyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Input Stream containing binary or ASCII Armored OpenPGP messages and write literal data after verification
|
||||
*
|
||||
* @param inputStream Input Stream to be read
|
||||
* @param outputStream Output Stream for literal data 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 PGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(decoderInputStream);
|
||||
final Iterator<?> objects = pgpObjectFactory.iterator();
|
||||
if (objects.hasNext()) {
|
||||
processObjectFactory(objects, outputStream);
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
getLogger().debug("One-Pass Signature Algorithm [{}] Verified", attributes.get(PGPAttributeKey.SIGNATURE_ALGORITHM));
|
||||
} else {
|
||||
final String keyId = attributes.getOrDefault(PGPAttributeKey.SIGNATURE_KEY_ID, KEY_ID_UNKNOWN);
|
||||
throw new PGPProcessException(String.format("Signature Key ID [%s] Verification Failed", keyId));
|
||||
}
|
||||
}
|
||||
|
||||
private void processObjectFactory(final Iterator<?> objects, final OutputStream outputStream) throws IOException {
|
||||
PGPOnePassSignature onePassSignature = null;
|
||||
|
||||
while (objects.hasNext()) {
|
||||
final Object object = objects.next();
|
||||
getLogger().debug("PGP Object Read [{}]", object.getClass().getSimpleName());
|
||||
|
||||
if (object instanceof PGPCompressedData) {
|
||||
final PGPCompressedData compressedData = (PGPCompressedData) object;
|
||||
try {
|
||||
final PGPObjectFactory compressedObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream());
|
||||
processObjectFactory(compressedObjectFactory.iterator(), outputStream);
|
||||
} catch (final PGPException e) {
|
||||
throw new PGPProcessException("Read Compressed Data Failed", e);
|
||||
}
|
||||
} else if (object instanceof PGPOnePassSignatureList) {
|
||||
final PGPOnePassSignatureList onePassSignatureList = (PGPOnePassSignatureList) object;
|
||||
onePassSignature = processOnePassSignatures(onePassSignatureList);
|
||||
} else if (object instanceof PGPLiteralData) {
|
||||
final PGPLiteralData literalData = (PGPLiteralData) object;
|
||||
processLiteralData(literalData, outputStream, onePassSignature);
|
||||
} else if (object instanceof PGPSignatureList) {
|
||||
final PGPSignatureList signatureList = (PGPSignatureList) object;
|
||||
processSignatures(signatureList, onePassSignature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PGPOnePassSignature processOnePassSignatures(final PGPOnePassSignatureList onePassSignatureList) {
|
||||
getLogger().debug("One-Pass Signatures Found [{}]", onePassSignatureList.size());
|
||||
|
||||
PGPOnePassSignature initializedOnePassSignature = null;
|
||||
final Iterator<PGPOnePassSignature> onePassSignatures = onePassSignatureList.iterator();
|
||||
if (onePassSignatures.hasNext()) {
|
||||
final PGPOnePassSignature onePassSignature = onePassSignatures.next();
|
||||
setOnePassSignatureAttributes(onePassSignature);
|
||||
|
||||
final String keyId = KeyIdentifierConverter.format(onePassSignature.getKeyID());
|
||||
final Optional<PGPPublicKey> optionalPublicKey = publicKeyService.findPublicKey(keyId);
|
||||
if (optionalPublicKey.isPresent()) {
|
||||
getLogger().debug("One-Pass Signature Key ID [{}] found", keyId);
|
||||
final PGPPublicKey publicKey = optionalPublicKey.get();
|
||||
try {
|
||||
onePassSignature.init(new JcaPGPContentVerifierBuilderProvider(), publicKey);
|
||||
initializedOnePassSignature = onePassSignature;
|
||||
} catch (final PGPException e) {
|
||||
throw new PGPProcessException(String.format("One-Pass Signature Key ID [%s] Initialization Failed", keyId), e);
|
||||
}
|
||||
} else {
|
||||
getLogger().warn("One-Pass Signature Key ID [{}] not found in Public Key Service", keyId);
|
||||
}
|
||||
}
|
||||
return initializedOnePassSignature;
|
||||
}
|
||||
|
||||
private void processLiteralData(final PGPLiteralData literalData,
|
||||
final OutputStream outputStream,
|
||||
final PGPOnePassSignature onePassSignature) throws IOException {
|
||||
setLiteralDataAttributes(literalData);
|
||||
final InputStream literalInputStream = literalData.getInputStream();
|
||||
if (onePassSignature == null) {
|
||||
StreamUtils.copy(literalInputStream, outputStream);
|
||||
} else {
|
||||
processSignedStream(literalInputStream, outputStream, onePassSignature);
|
||||
}
|
||||
}
|
||||
|
||||
private void processSignatures(final PGPSignatureList signatureList, final PGPOnePassSignature onePassSignature) {
|
||||
getLogger().debug("Signatures Found [{}]", signatureList.size());
|
||||
final Iterator<PGPSignature> signatures = signatureList.iterator();
|
||||
if (signatures.hasNext()) {
|
||||
final PGPSignature signature = signatures.next();
|
||||
setSignatureAttributes(signature);
|
||||
|
||||
if (onePassSignature == null) {
|
||||
getLogger().debug("One-Pass Signature not found: Verification Failed");
|
||||
} else {
|
||||
try {
|
||||
verified = onePassSignature.verify(signature);
|
||||
} catch (final PGPException e) {
|
||||
final String keyId = KeyIdentifierConverter.format(onePassSignature.getKeyID());
|
||||
throw new PGPProcessException(String.format("One-Pass Signature Key ID [%s] Verification Failed", keyId), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processSignedStream(final InputStream inputStream, final OutputStream outputStream, final PGPOnePassSignature onePassSignature) throws IOException {
|
||||
final String keyId = KeyIdentifierConverter.format(onePassSignature.getKeyID());
|
||||
getLogger().debug("Processing Data for One-Pass Signature with Key ID [{}]", keyId);
|
||||
final byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) >= 0) {
|
||||
onePassSignature.update(buffer, 0, read);
|
||||
outputStream.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
private void setOnePassSignatureAttributes(final PGPOnePassSignature onePassSignature) {
|
||||
setSignatureAlgorithm(onePassSignature.getKeyAlgorithm(), onePassSignature.getHashAlgorithm());
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_KEY_ID, KeyIdentifierConverter.format(onePassSignature.getKeyID()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(onePassSignature.getSignatureType()));
|
||||
}
|
||||
|
||||
private void setSignatureAttributes(final PGPSignature signature) {
|
||||
setSignatureAlgorithm(signature.getKeyAlgorithm(), signature.getHashAlgorithm());
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_CREATED, Long.toString(signature.getCreationTime().getTime()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_KEY_ID, KeyIdentifierConverter.format(signature.getKeyID()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(signature.getSignatureType()));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_VERSION, Integer.toString(signature.getVersion()));
|
||||
}
|
||||
|
||||
private void setLiteralDataAttributes(final PGPLiteralData literalData) {
|
||||
attributes.put(PGPAttributeKey.LITERAL_DATA_FILENAME, literalData.getFileName());
|
||||
attributes.put(PGPAttributeKey.LITERAL_DATA_MODIFIED, Long.toString(literalData.getModificationTime().getTime()));
|
||||
}
|
||||
|
||||
private void setSignatureAlgorithm(final int keyAlgorithm, final int hashAlgorithm) {
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, Integer.toString(hashAlgorithm));
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, Integer.toString(keyAlgorithm));
|
||||
try {
|
||||
final String algorithm = PGPUtil.getSignatureName(keyAlgorithm, hashAlgorithm);
|
||||
attributes.put(PGPAttributeKey.SIGNATURE_ALGORITHM, algorithm);
|
||||
} catch (final PGPException e) {
|
||||
getLogger().debug("Signature Algorithm Key Identifier [{}] Hash Identifier [{}] not found", keyAlgorithm, hashAlgorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.attributes;
|
||||
|
||||
/**
|
||||
* Pretty Good Privacy Decryption Strategy
|
||||
*/
|
||||
public enum DecryptionStrategy {
|
||||
DECRYPTED("Produce decrypted content read from literal data ignoring signatures"),
|
||||
|
||||
PACKAGED("Produce decrypted content packaged as an OpenPGP message for additional processing");
|
||||
|
||||
private final String description;
|
||||
|
||||
DecryptionStrategy(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.attributes;
|
||||
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
|
||||
/**
|
||||
* PGP Hash Algorithm Definitions supported for Signing
|
||||
*/
|
||||
public enum HashAlgorithm {
|
||||
SHA256(HashAlgorithmTags.SHA256),
|
||||
|
||||
SHA384(HashAlgorithmTags.SHA384),
|
||||
|
||||
SHA512(HashAlgorithmTags.SHA512);
|
||||
|
||||
private int id;
|
||||
|
||||
HashAlgorithm(final int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.attributes;
|
||||
|
||||
/**
|
||||
* Pretty Good Privacy File Signing Strategy
|
||||
*/
|
||||
public enum SigningStrategy {
|
||||
SIGNED("Produce signed content packaged as an OpenPGP message"),
|
||||
|
||||
DETACHED("Produce detached signature based on associated content packaged according to OpenPGP encoding");
|
||||
|
||||
private final String description;
|
||||
|
||||
SigningStrategy(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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.io;
|
||||
|
||||
import org.apache.nifi.processor.io.StreamCallback;
|
||||
import org.apache.nifi.processors.pgp.attributes.CompressionAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.attributes.FileEncoding;
|
||||
import org.apache.nifi.processors.pgp.exception.PGPProcessException;
|
||||
import org.apache.nifi.stream.io.StreamUtils;
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPLiteralData;
|
||||
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Encoding Stream Callback handles writing PGP messages using configured properties
|
||||
*/
|
||||
public class EncodingStreamCallback implements StreamCallback {
|
||||
private static final int OUTPUT_BUFFER_SIZE = 8192;
|
||||
|
||||
private final FileEncoding fileEncoding;
|
||||
|
||||
private final CompressionAlgorithm compressionAlgorithm;
|
||||
|
||||
private final String filename;
|
||||
|
||||
public EncodingStreamCallback(final FileEncoding fileEncoding, final CompressionAlgorithm compressionAlgorithm, final String filename) {
|
||||
this.fileEncoding = Objects.requireNonNull(fileEncoding, "File Encoding required");
|
||||
this.compressionAlgorithm = Objects.requireNonNull(compressionAlgorithm, "Compression Algorithm required");
|
||||
this.filename = Objects.requireNonNull(filename, "Filename required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Input Stream and write encoded contents to Output Stream
|
||||
*
|
||||
* @param inputStream Input Stream
|
||||
* @param outputStream Output Stream for encrypted contents
|
||||
* @throws IOException Thrown when unable to read or write streams
|
||||
*/
|
||||
@Override
|
||||
public void process(final InputStream inputStream, final OutputStream outputStream) throws IOException {
|
||||
try (final OutputStream encodingOutputStream = getEncodingOutputStream(outputStream)) {
|
||||
processEncoding(inputStream, encodingOutputStream);
|
||||
} catch (final PGPException e) {
|
||||
throw new PGPProcessException("PGP Stream Processing Failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Output Buffer byte array with size of 8192
|
||||
*
|
||||
* @return New empty array of 8192 bytes
|
||||
*/
|
||||
protected byte[] createOutputBuffer() {
|
||||
return new byte[OUTPUT_BUFFER_SIZE];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Encoding passing Input Stream through Compression Output Stream
|
||||
*
|
||||
* @param inputStream Input Stream
|
||||
* @param encodingOutputStream Output Stream configured according to File Encoding
|
||||
* @throws IOException Thrown when unable to read or write streams
|
||||
* @throws PGPException Thrown when unable to process compression
|
||||
*/
|
||||
protected void processEncoding(final InputStream inputStream, final OutputStream encodingOutputStream) throws IOException, PGPException {
|
||||
final PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(compressionAlgorithm.getId());
|
||||
try (final OutputStream compressedOutputStream = compressedDataGenerator.open(encodingOutputStream, createOutputBuffer())) {
|
||||
processCompression(inputStream, compressedOutputStream);
|
||||
}
|
||||
compressedDataGenerator.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Compression passing Input Stream through Literal Data Output Stream
|
||||
*
|
||||
* @param inputStream Input Stream
|
||||
* @param compressedOutputStream Output Stream configured according to Compression Algorithm
|
||||
* @throws IOException Thrown when unable to read or write streams
|
||||
* @throws PGPException Thrown when unable to process streams using PGP operations
|
||||
*/
|
||||
protected void processCompression(final InputStream inputStream, final OutputStream compressedOutputStream) throws IOException, PGPException {
|
||||
final PGPLiteralDataGenerator generator = new PGPLiteralDataGenerator();
|
||||
try (final OutputStream literalOutputStream = openLiteralOutputStream(generator, compressedOutputStream)) {
|
||||
StreamUtils.copy(inputStream, literalOutputStream);
|
||||
}
|
||||
generator.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Literal Data Output Stream using binary indicator with configured filename and current date indicating modification
|
||||
*
|
||||
* @param generator PGP Literal Data Generator
|
||||
* @param compressedOutputStream Output Stream configured according to Compression Algorithm
|
||||
* @return Literal Data Output Stream
|
||||
* @throws IOException Thrown when unable to open Literal Data Output Stream
|
||||
*/
|
||||
protected OutputStream openLiteralOutputStream(final PGPLiteralDataGenerator generator, final OutputStream compressedOutputStream) throws IOException {
|
||||
return generator.open(compressedOutputStream, PGPLiteralData.BINARY, filename, new Date(), createOutputBuffer());
|
||||
}
|
||||
|
||||
private OutputStream getEncodingOutputStream(final OutputStream outputStream) {
|
||||
return FileEncoding.ASCII.equals(fileEncoding) ? new ArmoredOutputStream(outputStream) : outputStream;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.io;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Key Identifier Converter from number to hexadecimal string
|
||||
*/
|
||||
public class KeyIdentifierConverter {
|
||||
private static final int HEXADECIMAL_RADIX = 16;
|
||||
|
||||
/**
|
||||
* Format numeric key identifier as uppercase hexadecimal string
|
||||
*
|
||||
* @param keyId Key Identifier
|
||||
* @return Uppercase hexadecimal string
|
||||
*/
|
||||
public static String format(final long keyId) {
|
||||
return Long.toHexString(keyId).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse hexadecimal key identifier to numeric key identifier
|
||||
*
|
||||
* @param keyId Hexadecimal string
|
||||
* @return Key Identifier
|
||||
*/
|
||||
public static long parse(final String keyId) {
|
||||
final BigInteger parsed = new BigInteger(keyId, HEXADECIMAL_RADIX);
|
||||
return parsed.longValue();
|
||||
}
|
||||
}
|
|
@ -13,4 +13,6 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
org.apache.nifi.processors.pgp.EncryptContentPGP
|
||||
org.apache.nifi.processors.pgp.DecryptContentPGP
|
||||
org.apache.nifi.processors.pgp.DecryptContentPGP
|
||||
org.apache.nifi.processors.pgp.SignContentPGP
|
||||
org.apache.nifi.processors.pgp.VerifyContentPGP
|
||||
|
|
|
@ -18,9 +18,12 @@ 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.pgp.util.PGPOperationUtils;
|
||||
import org.apache.nifi.processors.pgp.attributes.DecryptionStrategy;
|
||||
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.stream.io.StreamUtils;
|
||||
import org.apache.nifi.util.LogMessage;
|
||||
import org.apache.nifi.util.MockFlowFile;
|
||||
import org.apache.nifi.util.TestRunner;
|
||||
|
@ -30,20 +33,23 @@ import org.bouncycastle.openpgp.PGPCompressedData;
|
|||
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.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
|
||||
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.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
|
||||
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.jupiter.api.BeforeAll;
|
||||
|
@ -55,7 +61,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
@ -66,6 +74,7 @@ import java.util.UUID;
|
|||
|
||||
import static org.hamcrest.CoreMatchers.hasItem;
|
||||
import static org.hamcrest.CoreMatchers.isA;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
@ -95,8 +104,6 @@ public class DecryptContentPGPTest {
|
|||
|
||||
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;
|
||||
|
@ -264,6 +271,23 @@ public class DecryptContentPGPTest {
|
|||
assertSuccess();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessPublicKeyEncryptionRsaPrivateKeyPackaged() throws InitializationException, IOException, PGPException {
|
||||
setPrivateKeyService();
|
||||
final PGPPublicKey publicKey = rsaSecretKey.getPublicKey();
|
||||
when(privateKeyService.findPrivateKey(eq(publicKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey));
|
||||
|
||||
runner.setProperty(DecryptContentPGP.DECRYPTION_STRATEGY, DecryptionStrategy.PACKAGED.toString());
|
||||
|
||||
final byte[] contents = DATA.getBytes(DATA_CHARSET);
|
||||
final byte[] signedData = PGPOperationUtils.getOnePassSignedLiteralData(contents, rsaPrivateKey);
|
||||
final byte[] encryptedData = getPublicKeyEncryptedData(signedData, publicKey);
|
||||
runner.enqueue(encryptedData);
|
||||
runner.run();
|
||||
|
||||
assertSuccess(ENCRYPTION_ALGORITHM, DecryptionStrategy.PACKAGED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessPublicKeyEncryptionElGamalPrivateKey() throws InitializationException, IOException, PGPException {
|
||||
setPrivateKeyService();
|
||||
|
@ -281,8 +305,9 @@ public class DecryptContentPGPTest {
|
|||
final PGPPublicKey publicKey = rsaSecretKey.getPublicKey();
|
||||
when(privateKeyService.findPrivateKey(eq(publicKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey));
|
||||
|
||||
final byte[] encryptedData = getPublicKeyEncryptedData(getLiteralData(), publicKey);
|
||||
final byte[] signedData = getSignedData(encryptedData, publicKey, rsaPrivateKey);
|
||||
final byte[] literalData = getLiteralData();
|
||||
final byte[] encrypted = getPublicKeyEncryptedData(literalData, publicKey);
|
||||
final byte[] signedData = PGPOperationUtils.getOnePassSignedData(encrypted, rsaPrivateKey);
|
||||
runner.enqueue(signedData);
|
||||
runner.run();
|
||||
|
||||
|
@ -325,18 +350,71 @@ public class DecryptContentPGPTest {
|
|||
}
|
||||
|
||||
private void assertSuccess() {
|
||||
assertSuccess(ENCRYPTION_ALGORITHM);
|
||||
assertSuccess(ENCRYPTION_ALGORITHM, DecryptionStrategy.DECRYPTED);
|
||||
}
|
||||
|
||||
private void assertSuccess(final int encryptionAlgorithm) {
|
||||
assertSuccess(encryptionAlgorithm, DecryptionStrategy.DECRYPTED);
|
||||
}
|
||||
|
||||
private void assertSuccess(final int encryptionAlgorithm, final DecryptionStrategy decryptionStrategy) {
|
||||
runner.assertAllFlowFilesTransferred(DecryptContentPGP.SUCCESS);
|
||||
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(DecryptContentPGP.SUCCESS);
|
||||
final MockFlowFile flowFile = flowFiles.iterator().next();
|
||||
flowFile.assertContentEquals(DATA, DATA_CHARSET);
|
||||
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.LITERAL_DATA_FILENAME, FILE_NAME);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.LITERAL_DATA_MODIFIED, Long.toString(MODIFIED_MILLISECONDS));
|
||||
if (DecryptionStrategy.PACKAGED == decryptionStrategy) {
|
||||
assertSuccessPackaged(flowFile.getContentStream());
|
||||
} else {
|
||||
flowFile.assertContentEquals(DATA, DATA_CHARSET);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.LITERAL_DATA_FILENAME, FILE_NAME);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.LITERAL_DATA_MODIFIED, Long.toString(MODIFIED_MILLISECONDS));
|
||||
}
|
||||
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, Integer.toString(encryptionAlgorithm));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_BLOCK_CIPHER, PGPUtil.getSymmetricCipherName(encryptionAlgorithm));
|
||||
}
|
||||
|
||||
private void assertSuccessPackaged(final InputStream inputStream) {
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(inputStream);
|
||||
try {
|
||||
final Object firstObject = objectFactory.nextObject();
|
||||
assertOnePassSignatureEquals(firstObject);
|
||||
|
||||
final Object secondObject = objectFactory.nextObject();
|
||||
assertLiteralDataEquals(secondObject);
|
||||
|
||||
final Object thirdObject = objectFactory.nextObject();
|
||||
assertSignatureEquals(thirdObject);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertOnePassSignatureEquals(final Object object) {
|
||||
assertTrue(object instanceof PGPOnePassSignatureList);
|
||||
final PGPOnePassSignatureList onePassSignatureList = (PGPOnePassSignatureList) object;
|
||||
final PGPOnePassSignature onePassSignature = onePassSignatureList.iterator().next();
|
||||
assertEquals(onePassSignature.getKeyID(), rsaPrivateKey.getKeyID());
|
||||
}
|
||||
|
||||
private void assertLiteralDataEquals(final Object object) throws IOException {
|
||||
assertTrue(object instanceof PGPLiteralData);
|
||||
final PGPLiteralData literalData = (PGPLiteralData) object;
|
||||
assertEquals(FILE_NAME, literalData.getFileName());
|
||||
assertEquals(MODIFIED, literalData.getModificationTime());
|
||||
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
StreamUtils.copy(literalData.getDataStream(), outputStream);
|
||||
final byte[] literalBinary = outputStream.toByteArray();
|
||||
final String literal = new String(literalBinary, DATA_CHARSET);
|
||||
assertEquals(DATA, literal);
|
||||
}
|
||||
|
||||
private void assertSignatureEquals(final Object object) {
|
||||
assertTrue(object instanceof PGPSignatureList);
|
||||
final PGPSignatureList signatureList = (PGPSignatureList) object;
|
||||
final PGPSignature signature = signatureList.iterator().next();
|
||||
assertEquals(rsaPrivateKey.getKeyID(), signature.getKeyID());
|
||||
}
|
||||
|
||||
private void assertFailureExceptionLogged(final Class<? extends Exception> exceptionClass) {
|
||||
|
@ -347,19 +425,6 @@ public class DecryptContentPGPTest {
|
|||
assertThat(Arrays.asList(logMessage.getArgs()), hasItem(isA(exceptionClass)));
|
||||
}
|
||||
|
||||
private byte[] getSignedData(final byte[] contents, final PGPPublicKey publicKey, final PGPPrivateKey privateKey) throws PGPException, IOException {
|
||||
final PGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(publicKey.getAlgorithm(), PGPUtil.SHA1);
|
||||
final PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
|
||||
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
|
||||
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
signatureGenerator.generateOnePassVersion(NESTED_SIGNATURE_DISABLED).encode(outputStream);
|
||||
outputStream.write(contents);
|
||||
signatureGenerator.update(contents);
|
||||
signatureGenerator.generate().encode(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] getPublicKeyEncryptedData(final byte[] contents, final PGPPublicKey publicKey) throws IOException, PGPException {
|
||||
final PGPDataEncryptorBuilder builder = new BcPGPDataEncryptorBuilder(ENCRYPTION_ALGORITHM).setWithIntegrityPacket(INTEGRITY_ENABLED);
|
||||
final PGPEncryptedDataGenerator generator = new PGPEncryptedDataGenerator(builder);
|
||||
|
|
|
@ -17,9 +17,12 @@
|
|||
package org.apache.nifi.processors.pgp;
|
||||
|
||||
import org.apache.nifi.pgp.service.api.PGPPublicKeyService;
|
||||
import org.apache.nifi.pgp.util.PGPOperationUtils;
|
||||
import org.apache.nifi.processors.pgp.attributes.CompressionAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.attributes.DecryptionStrategy;
|
||||
import org.apache.nifi.processors.pgp.attributes.FileEncoding;
|
||||
import org.apache.nifi.processors.pgp.attributes.SymmetricKeyAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
|
||||
import org.apache.nifi.reporting.InitializationException;
|
||||
import org.apache.nifi.stream.io.StreamUtils;
|
||||
import org.apache.nifi.util.MockFlowFile;
|
||||
|
@ -59,10 +62,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
@ -74,6 +79,8 @@ public class EncryptContentPGPTest {
|
|||
|
||||
private static final String DATA = String.class.getName();
|
||||
|
||||
private static final byte[] DATA_BINARY = DATA.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final SymmetricKeyAlgorithm DEFAULT_SYMMETRIC_KEY_ALGORITHM = SymmetricKeyAlgorithm.valueOf(EncryptContentPGP.SYMMETRIC_KEY_ALGORITHM.getDefaultValue());
|
||||
|
||||
private static final String SERVICE_ID = PGPPublicKeyService.class.getName();
|
||||
|
@ -180,19 +187,34 @@ public class EncryptContentPGPTest {
|
|||
public void testSuccessPublicKeyEncryptionRsaPublicKey() throws IOException, InitializationException, PGPException {
|
||||
final PGPPublicKey publicKey = rsaSecretKey.getPublicKey();
|
||||
setPublicKeyService(publicKey);
|
||||
final String publicKeyIdSearch = Long.toHexString(publicKey.getKeyID()).toUpperCase();
|
||||
final String publicKeyIdSearch = KeyIdentifierConverter.format(publicKey.getKeyID());
|
||||
when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.of(publicKey));
|
||||
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
assertSuccess(rsaPrivateKey);
|
||||
assertSuccess(rsaPrivateKey, DecryptionStrategy.DECRYPTED, DATA_BINARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessPublicKeyEncryptionRsaPublicKeySignedDataPackaged() throws IOException, InitializationException, PGPException {
|
||||
final PGPPublicKey publicKey = rsaSecretKey.getPublicKey();
|
||||
setPublicKeyService(publicKey);
|
||||
final String publicKeyIdSearch = KeyIdentifierConverter.format(publicKey.getKeyID());
|
||||
when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.of(publicKey));
|
||||
|
||||
final byte[] contents = DATA.getBytes(StandardCharsets.UTF_8);
|
||||
final byte[] signedData = PGPOperationUtils.getOnePassSignedLiteralData(contents, rsaPrivateKey);
|
||||
|
||||
runner.enqueue(signedData);
|
||||
runner.run();
|
||||
assertSuccess(rsaPrivateKey, DecryptionStrategy.PACKAGED, signedData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessPasswordBasedAndPublicKeyEncryptionRsaPublicKey() throws IOException, InitializationException, PGPException {
|
||||
final PGPPublicKey publicKey = rsaSecretKey.getPublicKey();
|
||||
setPublicKeyService(publicKey);
|
||||
final String publicKeyIdSearch = Long.toHexString(publicKey.getKeyID()).toUpperCase();
|
||||
final String publicKeyIdSearch = KeyIdentifierConverter.format(publicKey.getKeyID());
|
||||
when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.of(publicKey));
|
||||
|
||||
runner.setProperty(EncryptContentPGP.PASSPHRASE, PASSPHRASE);
|
||||
|
@ -200,19 +222,19 @@ public class EncryptContentPGPTest {
|
|||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
|
||||
assertSuccess(rsaPrivateKey);
|
||||
assertSuccess(rsaPrivateKey, DecryptionStrategy.DECRYPTED, DATA_BINARY);
|
||||
assertSuccess(DEFAULT_SYMMETRIC_KEY_ALGORITHM, PASSPHRASE.toCharArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessPublicKeyEncryptionElGamalPublicKey() throws IOException, InitializationException, PGPException {
|
||||
setPublicKeyService(elGamalPublicKey);
|
||||
final String publicKeyIdSearch = Long.toHexString(elGamalPublicKey.getKeyID()).toUpperCase();
|
||||
final String publicKeyIdSearch = KeyIdentifierConverter.format(elGamalPublicKey.getKeyID());
|
||||
when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.of(elGamalPublicKey));
|
||||
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
assertSuccess(elGamalPrivateKey);
|
||||
assertSuccess(elGamalPrivateKey, DecryptionStrategy.DECRYPTED, DATA_BINARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -220,7 +242,7 @@ public class EncryptContentPGPTest {
|
|||
final PGPPublicKey publicKey = rsaSecretKey.getPublicKey();
|
||||
setPublicKeyService(publicKey);
|
||||
|
||||
final String publicKeyIdNotFound = Long.toHexString(Long.MAX_VALUE).toUpperCase();
|
||||
final String publicKeyIdNotFound = KeyIdentifierConverter.format(Long.MAX_VALUE);
|
||||
runner.setProperty(EncryptContentPGP.PUBLIC_KEY_SEARCH, publicKeyIdNotFound);
|
||||
|
||||
runner.enqueue(DATA);
|
||||
|
@ -234,12 +256,11 @@ public class EncryptContentPGPTest {
|
|||
runner.enableControllerService(publicKeyService);
|
||||
|
||||
runner.setProperty(EncryptContentPGP.PUBLIC_KEY_SERVICE, SERVICE_ID);
|
||||
final long publicKeyId = publicKey.getKeyID();
|
||||
final String publicKeyIdLong = Long.toHexString(publicKeyId).toUpperCase();
|
||||
runner.setProperty(EncryptContentPGP.PUBLIC_KEY_SEARCH, publicKeyIdLong);
|
||||
final String publicKeyId = KeyIdentifierConverter.format(publicKey.getKeyID());
|
||||
runner.setProperty(EncryptContentPGP.PUBLIC_KEY_SEARCH, publicKeyId);
|
||||
}
|
||||
|
||||
private void assertSuccess(final PGPPrivateKey privateKey) throws IOException, PGPException {
|
||||
private void assertSuccess(final PGPPrivateKey privateKey, final DecryptionStrategy decryptionStrategy, final byte[] expected) throws IOException, PGPException {
|
||||
runner.assertAllFlowFilesTransferred(EncryptContentPGP.SUCCESS);
|
||||
final MockFlowFile flowFile = runner.getFlowFilesForRelationship(EncryptContentPGP.SUCCESS).iterator().next();
|
||||
assertAttributesFound(DEFAULT_SYMMETRIC_KEY_ALGORITHM, flowFile);
|
||||
|
@ -251,8 +272,8 @@ public class EncryptContentPGPTest {
|
|||
assertTrue(encryptedData.isPresent(), "Public Key Encrypted Data not found");
|
||||
|
||||
final PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData.get();
|
||||
final String decryptedData = getDecryptedData(publicKeyEncryptedData, privateKey);
|
||||
assertEquals(DATA, decryptedData);
|
||||
final byte[] decryptedData = getDecryptedData(publicKeyEncryptedData, privateKey, decryptionStrategy);
|
||||
assertArrayEquals(expected, decryptedData);
|
||||
}
|
||||
|
||||
private void assertSuccess(final SymmetricKeyAlgorithm symmetricKeyAlgorithm, final char[] passphrase) throws IOException, PGPException {
|
||||
|
@ -267,8 +288,8 @@ public class EncryptContentPGPTest {
|
|||
assertTrue(encryptedData.isPresent(), "Password Based Encrypted Data not found");
|
||||
|
||||
final PGPPBEEncryptedData passwordBasedEncryptedData = (PGPPBEEncryptedData) encryptedData.get();
|
||||
final String decryptedData = getDecryptedData(passwordBasedEncryptedData, passphrase);
|
||||
assertEquals(DATA, decryptedData);
|
||||
final byte[] decryptedData = getDecryptedData(passwordBasedEncryptedData, passphrase);
|
||||
assertArrayEquals(DATA_BINARY, decryptedData);
|
||||
}
|
||||
|
||||
private void assertAttributesFound(final SymmetricKeyAlgorithm symmetricKeyAlgorithm, final MockFlowFile flowFile) {
|
||||
|
@ -295,24 +316,30 @@ public class EncryptContentPGPTest {
|
|||
return (PGPEncryptedDataList) firstObject;
|
||||
}
|
||||
|
||||
private String getDecryptedData(final PGPPBEEncryptedData passwordBasedEncryptedData, final char[] passphrase) throws PGPException, IOException {
|
||||
private byte[] getDecryptedData(final PGPPBEEncryptedData passwordBasedEncryptedData, final char[] passphrase) throws PGPException, IOException {
|
||||
final PBEDataDecryptorFactory decryptorFactory = new BcPBEDataDecryptorFactory(passphrase, new BcPGPDigestCalculatorProvider());
|
||||
final InputStream decryptedDataStream = passwordBasedEncryptedData.getDataStream(decryptorFactory);
|
||||
return getDecryptedData(decryptedDataStream);
|
||||
return getDecryptedData(decryptedDataStream, DecryptionStrategy.DECRYPTED);
|
||||
}
|
||||
|
||||
private String getDecryptedData(final PGPPublicKeyEncryptedData publicKeyEncryptedData, final PGPPrivateKey privateKey) throws PGPException, IOException {
|
||||
private byte[] getDecryptedData(final PGPPublicKeyEncryptedData publicKeyEncryptedData,
|
||||
final PGPPrivateKey privateKey,
|
||||
final DecryptionStrategy decryptionStrategy) throws PGPException, IOException {
|
||||
final PublicKeyDataDecryptorFactory decryptorFactory = new BcPublicKeyDataDecryptorFactory(privateKey);
|
||||
final InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory);
|
||||
return getDecryptedData(decryptedDataStream);
|
||||
return getDecryptedData(decryptedDataStream, decryptionStrategy);
|
||||
}
|
||||
|
||||
private String getDecryptedData(final InputStream decryptedDataStream) throws PGPException, IOException {
|
||||
private byte[] getDecryptedData(final InputStream decryptedDataStream, final DecryptionStrategy decryptionStrategy) throws PGPException, IOException {
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(decryptedDataStream);
|
||||
final PGPLiteralData literalData = getLiteralData(objectFactory);
|
||||
StreamUtils.copy(literalData.getDataStream(), outputStream);
|
||||
return outputStream.toString();
|
||||
if (DecryptionStrategy.PACKAGED == decryptionStrategy) {
|
||||
StreamUtils.copy(decryptedDataStream, outputStream);
|
||||
} else {
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(decryptedDataStream);
|
||||
final PGPLiteralData literalData = getLiteralData(objectFactory);
|
||||
StreamUtils.copy(literalData.getDataStream(), outputStream);
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private PGPLiteralData getLiteralData(final PGPObjectFactory objectFactory) throws PGPException {
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* 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.attributes.CompressionAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.attributes.FileEncoding;
|
||||
import org.apache.nifi.processors.pgp.attributes.HashAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.attributes.SigningStrategy;
|
||||
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
|
||||
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.openpgp.PGPCompressedData;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPLiteralData;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class SignContentPGPTest {
|
||||
private static final String PASSPHRASE = UUID.randomUUID().toString();
|
||||
|
||||
private static final String DATA = String.class.getName();
|
||||
|
||||
private static final byte[] DATA_BINARY = DATA.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final String SERVICE_ID = PGPPrivateKeyService.class.getName();
|
||||
|
||||
private static final int SIGNATURE_VERSION = 4;
|
||||
|
||||
private static PGPPrivateKey rsaPrivateKey;
|
||||
|
||||
private static PGPPublicKey rsaPublicKey;
|
||||
|
||||
@Mock
|
||||
private PGPPrivateKeyService privateKeyService;
|
||||
|
||||
private TestRunner runner;
|
||||
|
||||
@BeforeAll
|
||||
public static void setKeys() throws Exception {
|
||||
final PGPSecretKey rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray());
|
||||
rsaPublicKey = rsaSecretKey.getPublicKey();
|
||||
|
||||
final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray());
|
||||
rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setRunner() throws InitializationException {
|
||||
runner = TestRunners.newTestRunner(new SignContentPGP());
|
||||
|
||||
when(privateKeyService.getIdentifier()).thenReturn(SERVICE_ID);
|
||||
runner.addControllerService(SERVICE_ID, privateKeyService);
|
||||
runner.enableControllerService(privateKeyService);
|
||||
runner.setProperty(SignContentPGP.PRIVATE_KEY_SERVICE, SERVICE_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailurePrivateKeyIdParsingException() {
|
||||
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, String.class.getSimpleName());
|
||||
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
|
||||
assertFailureErrorLogged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailureServiceException() {
|
||||
final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
|
||||
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
|
||||
when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenThrow(new RuntimeException());
|
||||
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
|
||||
assertFailureErrorLogged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailurePrivateKeyIdNotFound() {
|
||||
final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
|
||||
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
|
||||
when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenReturn(Optional.empty());
|
||||
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
|
||||
assertFailureErrorLogged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessFileEncodingAsciiHashAlgorithmSha512() throws PGPException, IOException {
|
||||
assertSuccess(FileEncoding.ASCII, HashAlgorithm.SHA512, SigningStrategy.SIGNED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessFileEncodingBinaryHashAlgorithmSha512() throws PGPException, IOException {
|
||||
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA512, SigningStrategy.SIGNED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessFileEncodingBinaryUncompressedHashAlgorithmSha256() throws PGPException, IOException {
|
||||
runner.setProperty(SignContentPGP.COMPRESSION_ALGORITHM, CompressionAlgorithm.UNCOMPRESSED.toString());
|
||||
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.SIGNED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessFileEncodingBinaryHashAlgorithmSha256() throws PGPException, IOException {
|
||||
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.SIGNED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessDetachedFileEncodingBinaryHashAlgorithmSha256() throws PGPException, IOException {
|
||||
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.DETACHED);
|
||||
}
|
||||
|
||||
private void setPrivateKey() {
|
||||
final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
|
||||
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
|
||||
when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey));
|
||||
}
|
||||
|
||||
private void assertSuccess(final FileEncoding fileEncoding, final HashAlgorithm hashAlgorithm, final SigningStrategy signingStrategy) throws PGPException, IOException {
|
||||
setPrivateKey();
|
||||
|
||||
runner.setProperty(SignContentPGP.FILE_ENCODING, fileEncoding.toString());
|
||||
runner.setProperty(SignContentPGP.HASH_ALGORITHM, hashAlgorithm.toString());
|
||||
runner.setProperty(SignContentPGP.SIGNING_STRATEGY, signingStrategy.toString());
|
||||
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
|
||||
runner.assertTransferCount(SignContentPGP.SUCCESS, 1);
|
||||
final MockFlowFile flowFile = runner.getFlowFilesForRelationship(SignContentPGP.SUCCESS).iterator().next();
|
||||
assertFlowFileAttributesFound(flowFile, fileEncoding, hashAlgorithm);
|
||||
|
||||
if (SigningStrategy.DETACHED == signingStrategy) {
|
||||
assertDetachedSignatureVerified(flowFile);
|
||||
} else {
|
||||
assertSignatureVerified(flowFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFailureErrorLogged() {
|
||||
runner.assertAllFlowFilesTransferred(SignContentPGP.FAILURE);
|
||||
final Optional<LogMessage> optionalLogMessage = runner.getLogger().getErrorMessages().stream().findFirst();
|
||||
assertTrue(optionalLogMessage.isPresent());
|
||||
}
|
||||
|
||||
private void assertFlowFileAttributesFound(final MockFlowFile flowFile, final FileEncoding fileEncoding, final HashAlgorithm hashAlgorithm) throws PGPException {
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.FILE_ENCODING, fileEncoding.toString());
|
||||
flowFile.assertAttributeExists(PGPAttributeKey.COMPRESS_ALGORITHM);
|
||||
flowFile.assertAttributeExists(PGPAttributeKey.COMPRESS_ALGORITHM_ID);
|
||||
|
||||
final String signatureAlgorithm = PGPUtil.getSignatureName(rsaPrivateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getId());
|
||||
final String keyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
|
||||
|
||||
flowFile.assertAttributeExists(PGPAttributeKey.SIGNATURE_CREATED);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_ALGORITHM, signatureAlgorithm);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, Integer.toString(hashAlgorithm.getId()));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, Integer.toString(rsaPrivateKey.getPublicKeyPacket().getAlgorithm()));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ID, keyId);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(PGPSignature.BINARY_DOCUMENT));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_VERSION, Integer.toString(SIGNATURE_VERSION));
|
||||
}
|
||||
|
||||
private void assertDetachedSignatureVerified(final MockFlowFile signatureFlowFile) throws IOException, PGPException {
|
||||
final InputStream signatureContentStream = PGPUtil.getDecoderStream(signatureFlowFile.getContentStream());
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(signatureContentStream);
|
||||
|
||||
final PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject();
|
||||
final PGPSignature signature = signatureList.iterator().next();
|
||||
|
||||
signature.init(new JcaPGPContentVerifierBuilderProvider(), rsaPublicKey);
|
||||
|
||||
signature.update(DATA_BINARY);
|
||||
|
||||
final boolean verified = signature.verify();
|
||||
assertTrue(verified);
|
||||
}
|
||||
|
||||
private void assertSignatureVerified(final MockFlowFile flowFile) throws IOException, PGPException {
|
||||
final InputStream flowFileContentStream = PGPUtil.getDecoderStream(flowFile.getContentStream());
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(flowFileContentStream);
|
||||
|
||||
final PGPCompressedData compressedData = (PGPCompressedData) objectFactory.nextObject();
|
||||
final InputStream dataInputStream = compressedData.getDataStream();
|
||||
final PGPObjectFactory dataObjectFactory = new JcaPGPObjectFactory(dataInputStream);
|
||||
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
PGPOnePassSignature onePassSignature = null;
|
||||
boolean verified = false;
|
||||
for (final Object object : dataObjectFactory) {
|
||||
if (object instanceof PGPOnePassSignatureList) {
|
||||
final PGPOnePassSignatureList onePassSignatureList = (PGPOnePassSignatureList) object;
|
||||
onePassSignature = onePassSignatureList.iterator().next();
|
||||
onePassSignature.init(new JcaPGPContentVerifierBuilderProvider(), rsaPublicKey);
|
||||
} else if (object instanceof PGPLiteralData) {
|
||||
if (onePassSignature == null) {
|
||||
throw new IllegalStateException("One-Pass Signature not found before Literal Data");
|
||||
}
|
||||
|
||||
final PGPLiteralData literalData = (PGPLiteralData) object;
|
||||
final InputStream literalInputStream = literalData.getDataStream();
|
||||
int read;
|
||||
while ((read = literalInputStream.read()) >= 0) {
|
||||
onePassSignature.update((byte) read);
|
||||
outputStream.write(read);
|
||||
}
|
||||
} else if (object instanceof PGPSignatureList) {
|
||||
if (onePassSignature == null) {
|
||||
throw new IllegalStateException("One-Pass Signature not found before Signature");
|
||||
}
|
||||
|
||||
final PGPSignatureList signatureList = (PGPSignatureList) object;
|
||||
final PGPSignature signature = signatureList.iterator().next();
|
||||
verified = onePassSignature.verify(signature);
|
||||
} else {
|
||||
throw new IllegalStateException(String.format("Unexpected PGP Object Found [%s]", object.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(verified);
|
||||
|
||||
final String literal = new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
|
||||
assertEquals(DATA, literal);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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.pgp.util.PGPFileUtils;
|
||||
import org.apache.nifi.pgp.util.PGPSecretKeyGenerator;
|
||||
import org.apache.nifi.pgp.util.PGPOperationUtils;
|
||||
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
|
||||
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.HashAlgorithmTags;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class VerifyContentPGPTest {
|
||||
private static final String PASSPHRASE = UUID.randomUUID().toString();
|
||||
|
||||
private static final String SERVICE_ID = PGPPublicKeyService.class.getName();
|
||||
|
||||
private static final int SIGNATURE_VERSION = 4;
|
||||
|
||||
private static final int HASH_ALGORITHM_ID = HashAlgorithmTags.SHA512;
|
||||
|
||||
private static final String DATA = VerifyContentPGPTest.class.getName();
|
||||
|
||||
private static final byte[] DATA_BINARY = DATA.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static PGPPrivateKey rsaPrivateKey;
|
||||
|
||||
private static PGPPublicKey rsaPublicKey;
|
||||
|
||||
private TestRunner runner;
|
||||
|
||||
@Mock
|
||||
private PGPPublicKeyService publicKeyService;
|
||||
|
||||
@BeforeAll
|
||||
public static void setKeys() throws Exception {
|
||||
final PGPSecretKey rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray());
|
||||
final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray());
|
||||
rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor);
|
||||
rsaPublicKey = rsaSecretKey.getPublicKey();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setRunner() throws InitializationException {
|
||||
runner = TestRunners.newTestRunner(new VerifyContentPGP());
|
||||
|
||||
when(publicKeyService.getIdentifier()).thenReturn(SERVICE_ID);
|
||||
runner.addControllerService(SERVICE_ID, publicKeyService);
|
||||
runner.enableControllerService(publicKeyService);
|
||||
runner.setProperty(EncryptContentPGP.PUBLIC_KEY_SERVICE, SERVICE_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailureDataNotFound() {
|
||||
runner.enqueue(new byte[]{});
|
||||
runner.run();
|
||||
|
||||
assertFailureErrorLogged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailureFlowFileUnchanged() {
|
||||
runner.enqueue(DATA);
|
||||
runner.run();
|
||||
|
||||
assertFailureErrorLogged();
|
||||
final MockFlowFile flowFile = runner.getFlowFilesForRelationship(VerifyContentPGP.FAILURE).iterator().next();
|
||||
flowFile.assertContentEquals(DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailurePublicKeyNotFoundDataUnchanged() throws PGPException, IOException {
|
||||
final byte[] signed = PGPOperationUtils.getOnePassSignedLiteralData(DATA_BINARY, rsaPrivateKey);
|
||||
|
||||
final String publicKeyIdSearch = KeyIdentifierConverter.format((rsaPublicKey.getKeyID()));
|
||||
when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.empty());
|
||||
|
||||
runner.enqueue(signed);
|
||||
runner.run();
|
||||
|
||||
assertFailureErrorLogged();
|
||||
final MockFlowFile flowFile = runner.getFlowFilesForRelationship(VerifyContentPGP.FAILURE).iterator().next();
|
||||
flowFile.assertContentEquals(signed);
|
||||
assertFlowFileAttributesFound(flowFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessAsciiDataUnpacked() throws PGPException, IOException {
|
||||
final byte[] signed = PGPOperationUtils.getOnePassSignedLiteralData(DATA_BINARY, rsaPrivateKey);
|
||||
final String armored = PGPFileUtils.getArmored(signed);
|
||||
|
||||
setPublicKeyId();
|
||||
runner.enqueue(armored);
|
||||
runner.run();
|
||||
|
||||
final MockFlowFile flowFile = assertSuccess();
|
||||
flowFile.assertContentEquals(DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessBinaryDataUnpacked() throws PGPException, IOException {
|
||||
final byte[] signed = PGPOperationUtils.getOnePassSignedLiteralData(DATA_BINARY, rsaPrivateKey);
|
||||
|
||||
setPublicKeyId();
|
||||
runner.enqueue(signed);
|
||||
runner.run();
|
||||
|
||||
final MockFlowFile flowFile = assertSuccess();
|
||||
flowFile.assertContentEquals(DATA);
|
||||
}
|
||||
|
||||
private void setPublicKeyId() {
|
||||
final String publicKeyIdSearch = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
|
||||
when(publicKeyService.findPublicKey(eq(publicKeyIdSearch))).thenReturn(Optional.of(rsaPublicKey));
|
||||
}
|
||||
|
||||
private MockFlowFile assertSuccess() throws PGPException {
|
||||
runner.assertAllFlowFilesTransferred(VerifyContentPGP.SUCCESS);
|
||||
final MockFlowFile flowFile = runner.getFlowFilesForRelationship(VerifyContentPGP.SUCCESS).iterator().next();
|
||||
assertFlowFileAttributesFound(flowFile);
|
||||
return flowFile;
|
||||
}
|
||||
|
||||
private void assertFailureErrorLogged() {
|
||||
runner.assertAllFlowFilesTransferred(VerifyContentPGP.FAILURE);
|
||||
final Optional<LogMessage> optionalLogMessage = runner.getLogger().getErrorMessages().stream().findFirst();
|
||||
assertTrue(optionalLogMessage.isPresent());
|
||||
}
|
||||
|
||||
private void assertFlowFileAttributesFound(final MockFlowFile flowFile) throws PGPException {
|
||||
flowFile.assertAttributeExists(PGPAttributeKey.LITERAL_DATA_FILENAME);
|
||||
flowFile.assertAttributeExists(PGPAttributeKey.LITERAL_DATA_MODIFIED);
|
||||
|
||||
final String signatureAlgorithm = PGPUtil.getSignatureName(rsaPublicKey.getAlgorithm(), HASH_ALGORITHM_ID);
|
||||
final String keyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
|
||||
|
||||
flowFile.assertAttributeExists(PGPAttributeKey.SIGNATURE_CREATED);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_ALGORITHM, signatureAlgorithm);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, Integer.toString(HASH_ALGORITHM_ID));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, Integer.toString(rsaPublicKey.getAlgorithm()));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ID, keyId);
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(PGPSignature.BINARY_DOCUMENT));
|
||||
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_VERSION, Integer.toString(SIGNATURE_VERSION));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.io;
|
||||
|
||||
import org.apache.nifi.processors.pgp.attributes.CompressionAlgorithm;
|
||||
import org.apache.nifi.processors.pgp.attributes.FileEncoding;
|
||||
import org.apache.nifi.stream.io.StreamUtils;
|
||||
import org.bouncycastle.openpgp.PGPCompressedData;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPLiteralData;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class EncodingStreamCallbackTest {
|
||||
private static final String FILENAME = String.class.getName();
|
||||
|
||||
private static final byte[] DATA = String.class.getSimpleName().getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@Test
|
||||
public void testProcessBinaryCompressionZip() throws IOException, PGPException {
|
||||
final CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.ZIP;
|
||||
final EncodingStreamCallback callback = new EncodingStreamCallback(FileEncoding.BINARY, compressionAlgorithm, FILENAME);
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(DATA);
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
callback.process(inputStream, outputStream);
|
||||
|
||||
final InputStream processed = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
final InputStream compressedInputStream = assertCompressDataEquals(processed, compressionAlgorithm);
|
||||
assertLiteralDataEquals(compressedInputStream);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAsciiCompressionBzip2() throws IOException, PGPException {
|
||||
final CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.BZIP2;
|
||||
final EncodingStreamCallback callback = new EncodingStreamCallback(FileEncoding.ASCII, compressionAlgorithm, FILENAME);
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(DATA);
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
callback.process(inputStream, outputStream);
|
||||
|
||||
final InputStream processed = PGPUtil.getDecoderStream(new ByteArrayInputStream(outputStream.toByteArray()));
|
||||
final InputStream compressedInputStream = assertCompressDataEquals(processed, compressionAlgorithm);
|
||||
assertLiteralDataEquals(compressedInputStream);
|
||||
}
|
||||
|
||||
private InputStream assertCompressDataEquals(final InputStream processed, final CompressionAlgorithm compressionAlgorithm) throws IOException, PGPException {
|
||||
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(processed);
|
||||
final Object firstObject = objectFactory.nextObject();
|
||||
assertNotNull(firstObject);
|
||||
assertEquals(PGPCompressedData.class, firstObject.getClass());
|
||||
|
||||
final PGPCompressedData compressedData = (PGPCompressedData) firstObject;
|
||||
assertEquals(compressionAlgorithm.getId(), compressedData.getAlgorithm());
|
||||
return compressedData.getDataStream();
|
||||
}
|
||||
|
||||
private void assertLiteralDataEquals(final InputStream inputStream) throws IOException {
|
||||
final PGPObjectFactory compressedObjectFactory = new JcaPGPObjectFactory(inputStream);
|
||||
final Object firstCompressedObject = compressedObjectFactory.nextObject();
|
||||
assertNotNull(firstCompressedObject);
|
||||
assertEquals(PGPLiteralData.class, firstCompressedObject.getClass());
|
||||
|
||||
final PGPLiteralData literalData = (PGPLiteralData) firstCompressedObject;
|
||||
assertEquals(FILENAME, literalData.getFileName());
|
||||
assertEquals(PGPLiteralData.BINARY, literalData.getFormat());
|
||||
|
||||
final ByteArrayOutputStream literalOutputStream = new ByteArrayOutputStream();
|
||||
StreamUtils.copy(literalData.getDataStream(), literalOutputStream);
|
||||
assertArrayEquals(DATA, literalOutputStream.toByteArray());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.io;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class KeyIdentifierConverterTest {
|
||||
private static final long KEY_ID = Long.MAX_VALUE;
|
||||
|
||||
private static final String KEY_ID_FORMATTED = "7FFFFFFFFFFFFFFF";
|
||||
|
||||
private static final String INVALID = Long.class.getSimpleName();
|
||||
|
||||
@Test
|
||||
public void testFormat() {
|
||||
final String formatted = KeyIdentifierConverter.format(KEY_ID);
|
||||
assertEquals(KEY_ID_FORMATTED, formatted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse() {
|
||||
final long parsed = KeyIdentifierConverter.parse(KEY_ID_FORMATTED);
|
||||
assertEquals(KEY_ID, parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNumberFormatException() {
|
||||
assertThrows(NumberFormatException.class, () -> KeyIdentifierConverter.parse(INVALID));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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.util;
|
||||
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
||||
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Pretty Good Privacy Operation Utilities
|
||||
*/
|
||||
public class PGPOperationUtils {
|
||||
private static final boolean NESTED_SIGNATURE_DISABLED = false;
|
||||
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
|
||||
private static final long MODIFIED_MILLISECONDS = 86400000;
|
||||
|
||||
private static final Date MODIFIED = new Date(MODIFIED_MILLISECONDS);
|
||||
|
||||
private static final String FILE_NAME = String.class.getSimpleName();
|
||||
|
||||
private static final char FILE_TYPE = PGPLiteralDataGenerator.BINARY;
|
||||
|
||||
/**
|
||||
* Get data signed using one-pass signature generator
|
||||
*
|
||||
* @param contents Byte array contents to be signed
|
||||
* @param privateKey Private Key used for signing
|
||||
* @return Signed byte array
|
||||
* @throws PGPException Thrown when signature initialization failed
|
||||
* @throws IOException Thrown when signature generation failed
|
||||
*/
|
||||
public static byte[] getOnePassSignedData(final byte[] contents, final PGPPrivateKey privateKey) throws IOException, PGPException {
|
||||
final PGPSignatureGenerator signatureGenerator = getSignatureGenerator(privateKey);
|
||||
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
final PGPOnePassSignature onePassSignature = signatureGenerator.generateOnePassVersion(NESTED_SIGNATURE_DISABLED);
|
||||
onePassSignature.encode(outputStream);
|
||||
|
||||
outputStream.write(contents);
|
||||
signatureGenerator.update(contents);
|
||||
|
||||
final PGPSignature signature = signatureGenerator.generate();
|
||||
signature.encode(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data signed using one-pass signature generator wrapping literal data
|
||||
*
|
||||
* @param contents Byte array contents to be signed
|
||||
* @param privateKey Private Key used for signing
|
||||
* @return Signed byte array
|
||||
* @throws PGPException Thrown when signature initialization failed
|
||||
* @throws IOException Thrown when signature generation failed
|
||||
*/
|
||||
public static byte[] getOnePassSignedLiteralData(final byte[] contents, final PGPPrivateKey privateKey) throws IOException, PGPException {
|
||||
final PGPSignatureGenerator signatureGenerator = getSignatureGenerator(privateKey);
|
||||
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
final PGPOnePassSignature onePassSignature = signatureGenerator.generateOnePassVersion(NESTED_SIGNATURE_DISABLED);
|
||||
onePassSignature.encode(outputStream);
|
||||
|
||||
final PGPLiteralDataGenerator generator = new PGPLiteralDataGenerator();
|
||||
final byte[] buffer = new byte[BUFFER_SIZE];
|
||||
try (final OutputStream literalStream = generator.open(outputStream, FILE_TYPE, FILE_NAME, MODIFIED, buffer)) {
|
||||
literalStream.write(contents);
|
||||
signatureGenerator.update(contents);
|
||||
}
|
||||
|
||||
final PGPSignature signature = signatureGenerator.generate();
|
||||
signature.encode(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private static PGPSignatureGenerator getSignatureGenerator(final PGPPrivateKey privateKey) throws PGPException {
|
||||
final PGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(privateKey.getPublicKeyPacket().getAlgorithm(), HashAlgorithmTags.SHA512);
|
||||
final PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
|
||||
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
|
||||
return signatureGenerator;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue