| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.nifi.processors.pgp; |
| |
| import org.apache.nifi.pgp.service.api.PGPPrivateKeyService; |
| import org.apache.nifi.pgp.util.PGPSecretKeyGenerator; |
| import org.apache.nifi.processors.pgp.exception.PGPDecryptionException; |
| import org.apache.nifi.processors.pgp.exception.PGPProcessException; |
| import org.apache.nifi.reporting.InitializationException; |
| import org.apache.nifi.util.LogMessage; |
| import org.apache.nifi.util.MockFlowFile; |
| import org.apache.nifi.util.TestRunner; |
| import org.apache.nifi.util.TestRunners; |
| import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; |
| import org.bouncycastle.openpgp.PGPCompressedData; |
| import org.bouncycastle.openpgp.PGPCompressedDataGenerator; |
| import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; |
| import org.bouncycastle.openpgp.PGPException; |
| import org.bouncycastle.openpgp.PGPLiteralDataGenerator; |
| import org.bouncycastle.openpgp.PGPPrivateKey; |
| import org.bouncycastle.openpgp.PGPPublicKey; |
| import org.bouncycastle.openpgp.PGPSecretKey; |
| import org.bouncycastle.openpgp.PGPSecretKeyRing; |
| import org.bouncycastle.openpgp.PGPSignature; |
| import org.bouncycastle.openpgp.PGPSignatureGenerator; |
| import org.bouncycastle.openpgp.PGPUtil; |
| import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; |
| import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; |
| import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; |
| import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; |
| import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; |
| import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; |
| import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; |
| import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; |
| import org.junit.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.OutputStream; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.UUID; |
| |
| import static org.hamcrest.CoreMatchers.hasItem; |
| import static org.hamcrest.CoreMatchers.isA; |
| import static org.junit.jupiter.api.Assertions.assertTrue; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.when; |
| |
| @ExtendWith(MockitoExtension.class) |
| public class DecryptContentPGPTest { |
| private static final int ENCRYPTION_ALGORITHM = SymmetricKeyAlgorithmTags.AES_256; |
| |
| private static final boolean INTEGRITY_ENABLED = true; |
| |
| private static final boolean INTEGRITY_DISABLED = false; |
| |
| private static final String PASSPHRASE = UUID.randomUUID().toString(); |
| |
| private static final String FILE_NAME = String.class.getSimpleName(); |
| |
| private static final char FILE_TYPE = PGPLiteralDataGenerator.TEXT; |
| |
| private static final long MODIFIED_MILLISECONDS = 86400000; |
| |
| private static final Date MODIFIED = new Date(MODIFIED_MILLISECONDS); |
| |
| private static final String DATA = String.class.getName(); |
| |
| private static final Charset DATA_CHARSET = StandardCharsets.UTF_8; |
| |
| private static final int BUFFER_SIZE = 128; |
| |
| private static final boolean NESTED_SIGNATURE_DISABLED = false; |
| |
| private static final String SERVICE_ID = PGPPrivateKeyService.class.getSimpleName(); |
| |
| private static PGPSecretKey rsaSecretKey; |
| |
| private static PGPPrivateKey rsaPrivateKey; |
| |
| private static PGPPublicKey elGamalPublicKey; |
| |
| private static PGPPrivateKey elGamalPrivateKey; |
| |
| private TestRunner runner; |
| |
| @Mock |
| private PGPPrivateKeyService privateKeyService; |
| |
| @BeforeAll |
| public static void setKeys() throws Exception { |
| rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray()); |
| |
| final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray()); |
| rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor); |
| final PGPSecretKeyRing dsaElGamalSecretKeyRing = PGPSecretKeyGenerator.generateDsaElGamalSecretKeyRing(PASSPHRASE.toCharArray()); |
| for (final PGPSecretKey secretKey : dsaElGamalSecretKeyRing) { |
| final PGPPublicKey publicKey = secretKey.getPublicKey(); |
| if (PGPPublicKey.ELGAMAL_ENCRYPT == publicKey.getAlgorithm()) { |
| elGamalPrivateKey = secretKey.extractPrivateKey(decryptor); |
| elGamalPublicKey = publicKey; |
| } |
| } |
| } |
| |
| @BeforeEach |
| public void setRunner() { |
| runner = TestRunners.newTestRunner(new DecryptContentPGP()); |
| } |
| |
| @Test |
| public void testMissingProperties() { |
| runner.assertNotValid(); |
| } |
| |
| @Test |
| public void testFailureEncryptedDataNotFound() { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| runner.enqueue(new byte[]{}); |
| runner.run(); |
| |
| assertFailureExceptionLogged(PGPProcessException.class); |
| } |
| |
| @Test |
| public void testFailurePasswordBasedEncryptionPassphraseNotConfigured() throws IOException, PGPException, InitializationException { |
| setPrivateKeyService(); |
| |
| final byte[] encryptedData = getPasswordBasedEncryptedData(getLiteralData(), INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertFailureExceptionLogged(PGPProcessException.class); |
| } |
| |
| @Test |
| public void testFailurePasswordBasedEncryptionPassphraseNotMatched() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, String.class.getSimpleName()); |
| |
| final byte[] encryptedData = getPasswordBasedEncryptedData(getLiteralData(), INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertFailureExceptionLogged(PGPDecryptionException.class); |
| } |
| |
| @Test |
| public void testFailureLiteralDataNotFound() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final byte[] encryptedData = getPasswordBasedEncryptedData(new byte[]{}, INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertFailureExceptionLogged(PGPProcessException.class); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedCompressedZipIntegrityEnabled() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final byte[] literalData = getLiteralData(); |
| final byte[] compressedData = getCompressedData(literalData); |
| final byte[] encryptedData = getPasswordBasedEncryptedData(compressedData, INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedIntegrityEnabled() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final byte[] encryptedData = getPasswordBasedEncryptedData(getLiteralData(), INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedIntegrityDisabled() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final byte[] encryptedData = getPasswordBasedEncryptedData(getLiteralData(), INTEGRITY_DISABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedIntegrityEnabledCamellia128() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final int encryptionAlgorithm = SymmetricKeyAlgorithmTags.CAMELLIA_128; |
| final byte[] encryptedData = getPasswordBasedEncryptedData(encryptionAlgorithm, getLiteralData(), INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(encryptionAlgorithm); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedIntegrityEnabledTripleDes() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final int encryptionAlgorithm = SymmetricKeyAlgorithmTags.TRIPLE_DES; |
| final byte[] encryptedData = getPasswordBasedEncryptedData(encryptionAlgorithm, getLiteralData(), INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(encryptionAlgorithm); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedIntegrityEnabledCast5() throws IOException, PGPException { |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final int encryptionAlgorithm = SymmetricKeyAlgorithmTags.CAST5; |
| final byte[] encryptedData = getPasswordBasedEncryptedData(encryptionAlgorithm, getLiteralData(), INTEGRITY_ENABLED); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(encryptionAlgorithm); |
| } |
| |
| @Test |
| public void testSuccessPublicKeyEncryptionRsaPrivateKey() throws InitializationException, IOException, PGPException { |
| setPrivateKeyService(); |
| final PGPPublicKey publicKey = rsaSecretKey.getPublicKey(); |
| when(privateKeyService.findPrivateKey(eq(publicKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey)); |
| |
| final byte[] encryptedData = getPublicKeyEncryptedData(getLiteralData(), publicKey); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPublicKeyEncryptionElGamalPrivateKey() throws InitializationException, IOException, PGPException { |
| setPrivateKeyService(); |
| when(privateKeyService.findPrivateKey(eq(elGamalPrivateKey.getKeyID()))).thenReturn(Optional.of(elGamalPrivateKey)); |
| final byte[] encryptedData = getPublicKeyEncryptedData(getLiteralData(), elGamalPublicKey); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPublicKeyEncryptionRsaPrivateKeySigned() throws InitializationException, IOException, PGPException { |
| setPrivateKeyService(); |
| 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); |
| runner.enqueue(signedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedAndPublicKeyEncryptionRsaPrivateKey() throws InitializationException, IOException, PGPException { |
| setPrivateKeyService(); |
| final PGPPublicKey publicKey = rsaSecretKey.getPublicKey(); |
| when(privateKeyService.findPrivateKey(eq(publicKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey)); |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final byte[] encryptedData = getPasswordBasedAndPublicKeyEncryptedData(getLiteralData(), publicKey); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| @Test |
| public void testSuccessPasswordBasedAndPublicKeyEncryptionRsaPrivateKeyNotFound() throws InitializationException, IOException, PGPException { |
| setPrivateKeyService(); |
| final PGPPublicKey publicKey = rsaSecretKey.getPublicKey(); |
| when(privateKeyService.findPrivateKey(eq(publicKey.getKeyID()))).thenReturn(Optional.empty()); |
| runner.setProperty(DecryptContentPGP.PASSPHRASE, PASSPHRASE); |
| |
| final byte[] encryptedData = getPasswordBasedAndPublicKeyEncryptedData(getLiteralData(), publicKey); |
| runner.enqueue(encryptedData); |
| runner.run(); |
| |
| assertSuccess(); |
| } |
| |
| private void setPrivateKeyService() throws InitializationException { |
| when(privateKeyService.getIdentifier()).thenReturn(SERVICE_ID); |
| runner.addControllerService(SERVICE_ID, privateKeyService); |
| runner.enableControllerService(privateKeyService); |
| runner.setProperty(DecryptContentPGP.PRIVATE_KEY_SERVICE, SERVICE_ID); |
| } |
| |
| private void assertSuccess() { |
| assertSuccess(ENCRYPTION_ALGORITHM); |
| } |
| |
| private void assertSuccess(final int encryptionAlgorithm) { |
| 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)); |
| flowFile.assertAttributeEquals(PGPAttributeKey.SYMMETRIC_KEY_ALGORITHM_ID, Integer.toString(encryptionAlgorithm)); |
| } |
| |
| private void assertFailureExceptionLogged(final Class<? extends Exception> exceptionClass) { |
| runner.assertAllFlowFilesTransferred(DecryptContentPGP.FAILURE); |
| final Optional<LogMessage> optionalLogMessage = runner.getLogger().getErrorMessages().stream().findFirst(); |
| assertTrue(optionalLogMessage.isPresent()); |
| final LogMessage logMessage = optionalLogMessage.get(); |
| 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); |
| generator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(publicKey)); |
| return getEncryptedData(generator, contents); |
| } |
| |
| private byte[] getPasswordBasedEncryptedData(final byte[] contents, final boolean integrityEnabled) throws IOException, PGPException { |
| return getPasswordBasedEncryptedData(ENCRYPTION_ALGORITHM, contents, integrityEnabled); |
| } |
| |
| private byte[] getPasswordBasedEncryptedData(final int encryptionAlgorithm, final byte[] contents, final boolean integrityEnabled) throws IOException, PGPException { |
| final PGPDataEncryptorBuilder builder = new BcPGPDataEncryptorBuilder(encryptionAlgorithm).setWithIntegrityPacket(integrityEnabled); |
| final PGPEncryptedDataGenerator generator = new PGPEncryptedDataGenerator(builder); |
| generator.addMethod(new JcePBEKeyEncryptionMethodGenerator(PASSPHRASE.toCharArray())); |
| return getEncryptedData(generator, contents); |
| } |
| |
| private byte[] getPasswordBasedAndPublicKeyEncryptedData(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); |
| generator.addMethod(new JcePBEKeyEncryptionMethodGenerator(PASSPHRASE.toCharArray())); |
| generator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(publicKey)); |
| return getEncryptedData(generator, contents); |
| } |
| |
| private byte[] getCompressedData(final byte[] contents) throws IOException { |
| final PGPCompressedDataGenerator generator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP); |
| final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| try (final OutputStream compressedOutputStream = generator.open(outputStream)) { |
| compressedOutputStream.write(contents); |
| } |
| return outputStream.toByteArray(); |
| } |
| |
| private byte[] getLiteralData() throws IOException { |
| final PGPLiteralDataGenerator generator = new PGPLiteralDataGenerator(); |
| final byte[] buffer = new byte[BUFFER_SIZE]; |
| final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| try (final OutputStream literalStream = generator.open(outputStream, FILE_TYPE, FILE_NAME, MODIFIED, buffer)) { |
| literalStream.write(DATA.getBytes(DATA_CHARSET)); |
| } |
| return outputStream.toByteArray(); |
| } |
| |
| private byte[] getEncryptedData(final PGPEncryptedDataGenerator generator, final byte[] contents) throws IOException, PGPException { |
| final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| final byte[] buffer = new byte[BUFFER_SIZE]; |
| try (final OutputStream encryptedStream = generator.open(outputStream, buffer)) { |
| encryptedStream.write(contents); |
| } |
| return outputStream.toByteArray(); |
| } |
| } |