/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.processors.standard; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.security.Security; import java.util.Collection; import org.apache.commons.codec.binary.Hex; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.security.util.EncryptionMethod; import org.apache.nifi.security.util.KeyDerivationFunction; import org.apache.nifi.security.util.crypto.CipherUtility; import org.apache.nifi.security.util.crypto.PasswordBasedEncryptor; import org.apache.nifi.util.MockFlowFile; import org.apache.nifi.util.MockProcessContext; import org.apache.nifi.util.TestRunner; import org.apache.nifi.util.TestRunners; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TestEncryptContent { private static final Logger logger = LoggerFactory.getLogger(TestEncryptContent.class); @Before public void setUp() { Security.addProvider(new BouncyCastleProvider()); } @Test public void testRoundTrip() throws IOException { final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent()); testRunner.setProperty(EncryptContent.PASSWORD, "short"); testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name()); // Must be allowed or short password will cause validation errors testRunner.setProperty(EncryptContent.ALLOW_WEAK_CRYPTO, "allowed"); for (final EncryptionMethod encryptionMethod : EncryptionMethod.values()) { if (encryptionMethod.isUnlimitedStrength()) { continue; // cannot test unlimited strength in unit tests because it's not enabled by the JVM by default. } // KeyedCiphers tested in TestEncryptContentGroovy.groovy if (encryptionMethod.isKeyedCipher()) { continue; } logger.info("Attempting {}", encryptionMethod.name()); testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name()); testRunner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE); testRunner.enqueue(Paths.get("src/test/resources/hello.txt")); testRunner.clearTransferState(); testRunner.run(); testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1); MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0); testRunner.assertQueueEmpty(); testRunner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); testRunner.enqueue(flowFile); testRunner.clearTransferState(); testRunner.run(); testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1); logger.info("Successfully decrypted {}", encryptionMethod.name()); flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0); flowFile.assertContentEquals(new File("src/test/resources/hello.txt")); } } @Test public void testShouldDetermineMaxKeySizeForAlgorithms() throws IOException { // Arrange final String AES_ALGORITHM = EncryptionMethod.MD5_256AES.getAlgorithm(); final String DES_ALGORITHM = EncryptionMethod.MD5_DES.getAlgorithm(); final int AES_MAX_LENGTH = PasswordBasedEncryptor.supportsUnlimitedStrength() ? Integer.MAX_VALUE : 128; final int DES_MAX_LENGTH = PasswordBasedEncryptor.supportsUnlimitedStrength() ? Integer.MAX_VALUE : 64; // Act int determinedAESMaxLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(AES_ALGORITHM); int determinedTDESMaxLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(DES_ALGORITHM); // Assert assert determinedAESMaxLength == AES_MAX_LENGTH; assert determinedTDESMaxLength == DES_MAX_LENGTH; } @Test public void testShouldDecryptOpenSSLRawSalted() throws IOException { // Arrange Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", PasswordBasedEncryptor.supportsUnlimitedStrength()); final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent()); final String password = "thisIsABadPassword"; final EncryptionMethod method = EncryptionMethod.MD5_256AES; final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY; testRunner.setProperty(EncryptContent.PASSWORD, password); testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, kdf.name()); testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, method.name()); testRunner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); // Act testRunner.enqueue(Paths.get("src/test/resources/TestEncryptContent/salted_raw.enc")); testRunner.clearTransferState(); testRunner.run(); // Assert testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1); testRunner.assertQueueEmpty(); MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0); logger.info("Decrypted contents (hex): {}", Hex.encodeHexString(flowFile.toByteArray())); logger.info("Decrypted contents: {}", new String(flowFile.toByteArray(), "UTF-8")); // Assert flowFile.assertContentEquals(new File("src/test/resources/TestEncryptContent/plain.txt")); } @Test public void testShouldDecryptOpenSSLRawUnsalted() throws IOException { // Arrange Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", PasswordBasedEncryptor.supportsUnlimitedStrength()); final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent()); final String password = "thisIsABadPassword"; final EncryptionMethod method = EncryptionMethod.MD5_256AES; final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY; testRunner.setProperty(EncryptContent.PASSWORD, password); testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, kdf.name()); testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, method.name()); testRunner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); // Act testRunner.enqueue(Paths.get("src/test/resources/TestEncryptContent/unsalted_raw.enc")); testRunner.clearTransferState(); testRunner.run(); // Assert testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1); testRunner.assertQueueEmpty(); MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0); logger.info("Decrypted contents (hex): {}", Hex.encodeHexString(flowFile.toByteArray())); logger.info("Decrypted contents: {}", new String(flowFile.toByteArray(), "UTF-8")); // Assert flowFile.assertContentEquals(new File("src/test/resources/TestEncryptContent/plain.txt")); } @Test public void testDecryptShouldDefaultToBcrypt() throws IOException { // Arrange final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent()); // Assert Assert.assertEquals("Decrypt should default to Legacy KDF", testRunner.getProcessor().getPropertyDescriptor(EncryptContent.KEY_DERIVATION_FUNCTION .getName()).getDefaultValue(), KeyDerivationFunction.BCRYPT.name()); } @Test public void testDecryptSmallerThanSaltSize() { final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); runner.setProperty(EncryptContent.PASSWORD, "Hello, World!"); runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name()); runner.enqueue(new byte[4]); runner.run(); runner.assertAllFlowFilesTransferred(EncryptContent.REL_FAILURE, 1); } @Test public void testPGPDecrypt() throws IOException { final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP_ASCII_ARMOR.name()); runner.setProperty(EncryptContent.PASSWORD, "Hello, World!"); runner.enqueue(Paths.get("src/test/resources/TestEncryptContent/text.txt.asc")); runner.run(); runner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1); final MockFlowFile flowFile = runner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0); flowFile.assertContentEquals(Paths.get("src/test/resources/TestEncryptContent/text.txt")); } @Test public void testShouldValidatePGPPublicKeyringRequiresUserId() { // Arrange final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); Collection<ValidationResult> results; MockProcessContext pc; runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name()); runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); // Act results = pc.validate(); // Assert Assert.assertEquals(1, results.size()); ValidationResult vr = (ValidationResult) results.toArray()[0]; String expectedResult = " encryption without a " + EncryptContent.PASSWORD.getDisplayName() + " requires both " + EncryptContent.PUBLIC_KEYRING.getDisplayName() + " and " + EncryptContent.PUBLIC_KEY_USERID.getDisplayName(); String message = "'" + vr.toString() + "' contains '" + expectedResult + "'"; Assert.assertTrue(message, vr.toString().contains(expectedResult)); } @Test public void testShouldValidatePGPPublicKeyringExists() { // Arrange final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); Collection<ValidationResult> results; MockProcessContext pc; runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name()); runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg.missing"); runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); // Act results = pc.validate(); // Assert Assert.assertEquals(1, results.size()); ValidationResult vr = (ValidationResult) results.toArray()[0]; String expectedResult = "java.io.FileNotFoundException"; String message = "'" + vr.toString() + "' contains '" + expectedResult + "'"; Assert.assertTrue(message, vr.toString().contains(expectedResult)); } @Test public void testShouldValidatePGPPublicKeyringIsProperFormat() { // Arrange final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); Collection<ValidationResult> results; MockProcessContext pc; runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name()); runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/text.txt"); runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); // Act results = pc.validate(); // Assert Assert.assertEquals(1, results.size()); ValidationResult vr = (ValidationResult) results.toArray()[0]; String expectedResult = " java.io.IOException: invalid header encountered"; String message = "'" + vr.toString() + "' contains '" + expectedResult + "'"; Assert.assertTrue(message, vr.toString().contains(expectedResult)); } @Test public void testShouldValidatePGPPublicKeyringContainsUserId() { // Arrange final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); Collection<ValidationResult> results; MockProcessContext pc; runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name()); runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg"); runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); // Act results = pc.validate(); // Assert Assert.assertEquals(1, results.size()); ValidationResult vr = (ValidationResult) results.toArray()[0]; String expectedResult = "PGPException: Could not find a public key with the given userId"; String message = "'" + vr.toString() + "' contains '" + expectedResult + "'"; Assert.assertTrue(message, vr.toString().contains(expectedResult)); } @Test public void testShouldExtractPGPPublicKeyFromKeyring() { // Arrange final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); Collection<ValidationResult> results; MockProcessContext pc; runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name()); runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg"); runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) <alopresto.apache+test@gmail.com>"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); // Act results = pc.validate(); // Assert Assert.assertEquals(0, results.size()); } @Test public void testValidation() { final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); Collection<ValidationResult> results; MockProcessContext pc; runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); results = pc.validate(); Assert.assertEquals(results.toString(), 1, results.size()); for (final ValidationResult vr : results) { Assert.assertTrue(vr.toString() .contains(EncryptContent.PASSWORD.getDisplayName() + " is required when using algorithm")); } runner.enqueue(new byte[0]); final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES; runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name()); runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name()); runner.setProperty(EncryptContent.PASSWORD, "ThisIsAPasswordThatIsLongerThanSixteenCharacters"); pc = (MockProcessContext) runner.getProcessContext(); results = pc.validate(); if (!PasswordBasedEncryptor.supportsUnlimitedStrength()) { logger.info(results.toString()); Assert.assertEquals(1, results.size()); for (final ValidationResult vr : results) { Assert.assertTrue( "Did not successfully catch validation error of a long password in a non-JCE Unlimited Strength environment", vr.toString().contains("Password length greater than " + CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod) + " characters is not supported by this JVM due to lacking JCE Unlimited Strength Jurisdiction Policy files.")); } } else { Assert.assertEquals(results.toString(), 0, results.size()); } runner.removeProperty(EncryptContent.PASSWORD); runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name()); runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/text.txt"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); results = pc.validate(); Assert.assertEquals(1, results.size()); for (final ValidationResult vr : results) { Assert.assertTrue(vr.toString().contains( " encryption without a " + EncryptContent.PASSWORD.getDisplayName() + " requires both " + EncryptContent.PUBLIC_KEYRING.getDisplayName() + " and " + EncryptContent.PUBLIC_KEY_USERID.getDisplayName())); } // Legacy tests moved to individual tests to comply with new library // TODO: Move secring tests out to individual as well runner.removeProperty(EncryptContent.PUBLIC_KEYRING); runner.removeProperty(EncryptContent.PUBLIC_KEY_USERID); runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); runner.setProperty(EncryptContent.PRIVATE_KEYRING, "src/test/resources/TestEncryptContent/secring.gpg"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); results = pc.validate(); Assert.assertEquals(1, results.size()); for (final ValidationResult vr : results) { Assert.assertTrue(vr.toString().contains( " decryption without a " + EncryptContent.PASSWORD.getDisplayName() + " requires both " + EncryptContent.PRIVATE_KEYRING.getDisplayName() + " and " + EncryptContent.PRIVATE_KEYRING_PASSPHRASE.getDisplayName())); } runner.setProperty(EncryptContent.PRIVATE_KEYRING_PASSPHRASE, "PASSWORD"); runner.enqueue(new byte[0]); pc = (MockProcessContext) runner.getProcessContext(); results = pc.validate(); Assert.assertEquals(1, results.size()); for (final ValidationResult vr : results) { Assert.assertTrue(vr.toString().contains( " could not be opened with the provided " + EncryptContent.PRIVATE_KEYRING_PASSPHRASE.getDisplayName())); } } }