From cf21bc47cd63e316eaa4a899f8b3373a6ca1b1fc Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Mon, 28 Mar 2022 23:05:09 -0500 Subject: [PATCH] NIFI-9844 Refactored Encryptor tests using JUnit 5 - Refactored Keyed and Password Based Encryptor tests from Groovy to Java Signed-off-by: Nathan Gough This closes #5913. --- .../crypto/KeyedEncryptorGroovyTest.groovy | 254 ------- .../PasswordBasedEncryptorGroovyTest.groovy | 699 ------------------ .../util/crypto/KeyedEncryptorTest.java | 117 +++ .../crypto/OpenPGPKeyBasedEncryptorTest.java | 130 +--- .../OpenPGPPasswordBasedEncryptorTest.java | 125 +--- .../crypto/PasswordBasedEncryptorTest.java | 240 ++++++ 6 files changed, 402 insertions(+), 1163 deletions(-) delete mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy delete mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/KeyedEncryptorTest.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorTest.java diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy deleted file mode 100644 index ab2d0f7759..0000000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy +++ /dev/null @@ -1,254 +0,0 @@ -/* - * 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.security.util.crypto - -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.processor.exception.ProcessException -import org.apache.nifi.processor.io.StreamCallback -import org.apache.nifi.security.util.EncryptionMethod -import org.apache.nifi.security.util.KeyDerivationFunction -import org.apache.nifi.stream.io.exception.BytePatternNotFoundException -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.BeforeClass -import org.junit.Test -import org.junit.Assert -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec -import java.nio.charset.StandardCharsets -import java.security.Security - -class KeyedEncryptorGroovyTest { - private static final Logger logger = LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class) - - private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/" - - private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" - private static final SecretKey KEY = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES") - - @BeforeClass - static void setUpOnce() throws Exception { - Security.addProvider(new BouncyCastleProvider()) - - logger.metaClass.methodMissing = { String name, args -> - logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") - } - } - - @Test - void testShouldEncryptAndDecrypt() throws Exception { - // Arrange - final String PLAINTEXT = "This is a plaintext message." - logger.info("Plaintext: {}", PLAINTEXT) - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - logger.info("Using ${encryptionMethod.name()}") - - // Act - KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes)) - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) - decryptionCallback.process(cipherInputStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}\n\n", recovered) - assert PLAINTEXT.equals(recovered) - } - - @Test - void testShouldDecryptOpenSSLUnsaltedCipherTextWithKnownIV() throws Exception { - // Arrange - final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text - logger.info("Plaintext: {}", PLAINTEXT) - byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes - - final String keyHex = "711E85689CE7AFF6F410AEA43ABC5446" - final String ivHex = "842F685B84879B2E00F977C22B9E9A7D" - - InputStream cipherStream = new ByteArrayInputStream(cipherBytes) - OutputStream recoveredStream = new ByteArrayOutputStream() - - final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, new SecretKeySpec(Hex.decodeHex(keyHex as char[]), "AES"), Hex.decodeHex(ivHex as char[])) - - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") - - // Act - decryptionCallback.process(cipherStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}", recovered) - assert PLAINTEXT.equals(recovered) - } - - /** - * This test demonstrates that if incoming cipher text was generated by a cipher using PBE with - * KDF, the salt can be skipped and the cipher bytes can still be decrypted using keyed encryption. - * @throws Exception - */ - @Test - void testShouldSkipSaltOnDecrypt() throws Exception { - final String PASSWORD = "thisIsABadPassword" - - final String PLAINTEXT = "This is a plaintext message." - logger.info("Plaintext: {}", PLAINTEXT) - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(encryptionMethod.algorithm) - logger.info("Using ${encryptionMethod.name()} with key length ${keyLength} bits") - - // The PBE encryptor encrypts the data and prepends the salt and IV - PasswordBasedEncryptor passwordBasedEncryptor = new PasswordBasedEncryptor(EncryptionMethod.AES_CBC, PASSWORD.toCharArray(), KeyDerivationFunction.ARGON2) - StreamCallback encryptionCallback = passwordBasedEncryptor.getEncryptionCallback() - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes)) - - // Derive the decryption key manually from the provided salt & password - String kdfSalt = passwordBasedEncryptor.flowfileAttributes.get("encryptcontent.kdf_salt") - def costs = parseArgon2CostParamsFromSalt(kdfSalt) - SecureHasher secureHasher = new Argon2SecureHasher(keyLength / 8 as int, costs.m, costs.p, costs.t) - byte[] argon2DerivedKeyBytes = secureHasher.hashRaw(PASSWORD.getBytes(StandardCharsets.UTF_8), Argon2CipherProvider.extractRawSaltFromArgon2Salt(kdfSalt)) - logger.sanity("Derived key bytes: ${Hex.encodeHexString(argon2DerivedKeyBytes)}") - - // The keyed encryptor will attempt to decrypt the content, skipping the salt - KeyedEncryptor keyedEncryptor = new KeyedEncryptor(encryptionMethod, argon2DerivedKeyBytes) - StreamCallback decryptionCallback = keyedEncryptor.getDecryptionCallback() - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) - - // Act - decryptionCallback.process(cipherInputStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}\n\n", recovered) - assert PLAINTEXT.equals(recovered) - } - - @Test - void testShouldParseCostParams() { - // Arrange - String argon2Salt = "\$argon2id\$v=19\$m=4096,t=3,p=1\$i8CIuSjrwdSuR42pb15AoQ" - - // Act - def cost = parseArgon2CostParamsFromSalt(argon2Salt) - logger.info("Parsed cost: ${cost}") - - // Assert - assert cost == [m: 4096, t: 3, p: 1] - } - - static Map parseArgon2CostParamsFromSalt(String kdfSalt) { - kdfSalt.tokenize("\$")[2].split(",").collectEntries { - def l = it.split("=") - [l.first(), Integer.valueOf(l.last())] - } - } - - @Test - void testDecryptShouldHandleCipherStreamMissingIV() { - // Arrange - KeyedCipherProvider cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE) - final String IV_DELIMITER = new String(cipherProvider.IV_DELIMITER, StandardCharsets.UTF_8) - - final String PLAINTEXT = "This is a plaintext message." - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Act - KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - - // Remove IV - final String cipherString = new String(cipherBytes, StandardCharsets.UTF_8) - final byte[] removedIVCipherBytes = cipherString.split(IV_DELIMITER)[1].getBytes(StandardCharsets.UTF_8) - - InputStream cipherInputStream = new ByteArrayInputStream(removedIVCipherBytes) - Exception exception = Assert.assertThrows(ProcessException.class, () -> { - decryptionCallback.process(cipherInputStream, recoveredStream) - }) - } - - @Test - void testDecryptShouldHandleCipherStreamMissingIVDelimiter() { - // Arrange - KeyedCipherProvider cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE) - final String IV_DELIMITER = new String(cipherProvider.IV_DELIMITER, StandardCharsets.UTF_8) - - final String PLAINTEXT = "This is a plaintext message." - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - - // Act - KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - - // Remove IV Delimiter - final String cipherString = new String(cipherBytes, StandardCharsets.UTF_8) - final byte[] removedIVDelimiterCipherBytes = cipherString.split(IV_DELIMITER)[1].getBytes(StandardCharsets.UTF_8) - - InputStream cipherInputStream = new ByteArrayInputStream(removedIVDelimiterCipherBytes) - - Exception exception = Assert.assertThrows(ProcessException.class, () -> { - decryptionCallback.process(cipherInputStream, recoveredStream) - }) - } -} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy deleted file mode 100644 index d79474e444..0000000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy +++ /dev/null @@ -1,699 +0,0 @@ -/* - * 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.security.util.crypto - -import org.apache.commons.codec.binary.Hex -import org.apache.nifi.processor.exception.ProcessException -import org.apache.nifi.processor.io.StreamCallback -import org.apache.nifi.processors.standard.TestEncryptContentGroovy -import org.apache.nifi.security.util.EncryptionMethod -import org.apache.nifi.security.util.KeyDerivationFunction -import org.apache.nifi.stream.io.ByteCountingInputStream -import org.apache.nifi.stream.io.ByteCountingOutputStream -import org.apache.nifi.stream.io.exception.BytePatternNotFoundException -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.Assume -import org.junit.BeforeClass -import org.junit.Test -import org.junit.Assert -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.crypto.Cipher -import java.nio.charset.StandardCharsets -import java.security.MessageDigest -import java.security.Security - -class PasswordBasedEncryptorGroovyTest { - private static final Logger logger = LoggerFactory.getLogger(PasswordBasedEncryptorGroovyTest.class) - - private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/" - private static final File plainFile = new File("${TEST_RESOURCES_PREFIX}/plain.txt") - private static final File encryptedFile = new File("${TEST_RESOURCES_PREFIX}/salted_128_raw.asc") - - private static final String PASSWORD = "thisIsABadPassword" - private static final String LEGACY_PASSWORD = "Hello, World!" - - @BeforeClass - static void setUpOnce() throws Exception { - Security.addProvider(new BouncyCastleProvider()) - - logger.metaClass.methodMissing = { String name, args -> - logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") - } - } - - @Test - void testShouldEncryptAndDecrypt() throws Exception { - // Arrange - final String PLAINTEXT = "This is a plaintext message." - logger.info("Plaintext: {}", PLAINTEXT) - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - String shortPassword = "short" - - def encryptionMethodsAndKdfs = [ - (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): EncryptionMethod.MD5_128AES, - (KeyDerivationFunction.NIFI_LEGACY) : EncryptionMethod.MD5_128AES, - (KeyDerivationFunction.BCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.SCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.PBKDF2) : EncryptionMethod.AES_CBC - ] - - // Act - encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod -> - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - logger.info("Using ${kdf.kdfName} and ${encryptionMethod.name()}") - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, shortPassword.toCharArray(), kdf) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes)) - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) - decryptionCallback.process(cipherInputStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}\n\n", recovered) - assert PLAINTEXT.equals(recovered) - - // This is necessary to run multiple iterations - plainStream.reset() - } - } - - /** - * This test was added after observing an encryption which appended a single {@code 0x10} byte after the cipher text was written. All other bytes in the flowfile content were correct. The corresponding {@code DecryptContent} processor could not decrypt the content and manual decryption required truncating the final byte. - * @throws Exception - */ - @Test - void testBcryptKDFShouldNotAddOutputBytes() throws Exception { - // Arrange - final String PLAINTEXT = "This is a plaintext message." * 4 - logger.info("Plaintext: {}", PLAINTEXT) - - int saltLength = 29 - int saltDelimiterLength = 8 - int ivLength = 16 - int ivDelimiterLength = 6 - int plaintextBlockCount = (int) Math.ceil(PLAINTEXT.length() / 16.0) - int cipherByteLength = (PLAINTEXT.length() % 16 == 0 ? plaintextBlockCount + 1 : plaintextBlockCount) * 16 - int EXPECTED_CIPHER_BYTE_COUNT = saltLength + saltDelimiterLength + ivLength + ivDelimiterLength + cipherByteLength - logger.info("Expected total cipher byte count: ${EXPECTED_CIPHER_BYTE_COUNT}") - - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - String shortPassword = "short" - - EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - KeyDerivationFunction kdf = KeyDerivationFunction.BCRYPT - - // Act - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - logger.info("Using ${kdf.kdfName} and ${encryptionMethod.name()}") - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, shortPassword.toCharArray(), kdf) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - logger.info("Encrypted (${cipherBytes.length}): ${Hex.encodeHexString(cipherBytes)}") - assert cipherBytes.length == EXPECTED_CIPHER_BYTE_COUNT - - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) - decryptionCallback.process(cipherInputStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - logger.info("Recovered (${recoveredBytes.length}): ${Hex.encodeHexString(recoveredBytes)}") - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}\n\n", recovered) - assert PLAINTEXT.equals(recovered) - } - - @Test - void testShouldDecryptLegacyOpenSSLSaltedCipherText() throws Exception { - // Arrange - Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", CipherUtility.isUnlimitedStrengthCryptoSupported()) - - final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text - logger.info("Plaintext: {}", PLAINTEXT) - byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/salted_128_raw.enc").bytes - InputStream cipherStream = new ByteArrayInputStream(cipherBytes) - OutputStream recoveredStream = new ByteArrayOutputStream() - - final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES - final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") - - // Act - decryptionCallback.process(cipherStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}", recovered) - assert PLAINTEXT.equals(recovered) - } - - @Test - void testShouldDecryptLegacyOpenSSLUnsaltedCipherText() throws Exception { - // Arrange - Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", CipherUtility.isUnlimitedStrengthCryptoSupported()) - - final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text - logger.info("Plaintext: {}", PLAINTEXT) - byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes - InputStream cipherStream = new ByteArrayInputStream(cipherBytes) - OutputStream recoveredStream = new ByteArrayOutputStream() - - final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES - final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") - - // Act - decryptionCallback.process(cipherStream, recoveredStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray() - String recovered = new String(recoveredBytes, "UTF-8") - logger.info("Recovered: {}", recovered) - assert PLAINTEXT.equals(recovered) - } - - @Test - void testShouldDecryptNiFiLegacySaltedCipherTextWithVariableSaltLength() throws Exception { - // Arrange - final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text - logger.info("Plaintext: {}", PLAINTEXT) - - final String PASSWORD = "short" - logger.info("Password: ${PASSWORD}") - - /* The old NiFi legacy KDF code checked the algorithm block size and used it for the salt length. - If the block size was not available, it defaulted to 8 bytes based on the default salt size. */ - - def pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") } - def encryptionMethodsByBlockSize = pbeEncryptionMethods.groupBy { - Cipher cipher = Cipher.getInstance(it.algorithm, it.provider) - cipher.getBlockSize() - } - - logger.info("Grouped algorithms by block size: ${encryptionMethodsByBlockSize.collectEntries { k, v -> [k, v*.algorithm] }}") - - encryptionMethodsByBlockSize.each { int blockSize, List encryptionMethods -> - encryptionMethods.each { EncryptionMethod encryptionMethod -> - final int EXPECTED_SALT_SIZE = (blockSize > 0) ? blockSize : 8 - logger.info("Testing ${encryptionMethod.algorithm} with expected salt size ${EXPECTED_SALT_SIZE}") - - def legacySaltHex = "aa" * EXPECTED_SALT_SIZE - byte[] legacySalt = Hex.decodeHex(legacySaltHex as char[]) - logger.info("Generated legacy salt ${legacySaltHex} (${legacySalt.length})") - - // Act - - // Encrypt using the raw legacy code - NiFiLegacyCipherProvider legacyCipherProvider = new NiFiLegacyCipherProvider() - Cipher legacyCipher = legacyCipherProvider.getCipher(encryptionMethod, PASSWORD, legacySalt, true) - byte[] cipherBytes = legacyCipher.doFinal(PLAINTEXT.bytes) - logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") - - byte[] completeCipherStreamBytes = org.bouncycastle.util.Arrays.concatenate(legacySalt, cipherBytes) - logger.info("Complete cipher stream: ${Hex.encodeHexString(completeCipherStreamBytes)}") - - InputStream cipherStream = new ByteArrayInputStream(completeCipherStreamBytes) - OutputStream resultStream = new ByteArrayOutputStream() - - // Now parse and decrypt using PBE encryptor - PasswordBasedEncryptor decryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD as char[], KeyDerivationFunction.NIFI_LEGACY) - - StreamCallback decryptCallback = decryptor.decryptionCallback - decryptCallback.process(cipherStream, resultStream) - - logger.info("Decrypted: ${Hex.encodeHexString(resultStream.toByteArray())}") - String recovered = new String(resultStream.toByteArray()) - logger.info("Recovered: ${recovered}") - - // Assert - assert recovered == PLAINTEXT - } - } - } - - @Test - void testShouldWriteEncryptionMetadataAttributesForKDFs() throws Exception { - // Arrange - final String PLAINTEXT = "This is a plaintext message. " - logger.info("Plaintext: ${PLAINTEXT}") - - final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - def kdfs = KeyDerivationFunction.values().findAll { it.isStrongKDF() } - - // Act - kdfs.each { KeyDerivationFunction kdf -> - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - StreamCallback encryptCallback = encryptor.getEncryptionCallback() - - // Reset the streams - InputStream inputStream = new ByteArrayInputStream(PLAINTEXT.bytes) - OutputStream cipherStream = new ByteArrayOutputStream() - - encryptCallback.process(inputStream, cipherStream) - - // Assert - byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - String cipherText = new String(cipherBytes, StandardCharsets.UTF_8) - String cipherTextHex = Hex.encodeHexString(cipherBytes) - logger.info("Cipher text (${cipherBytes.size()}): ${cipherTextHex}") - - int ivDelimiterStart = CipherUtility.findSequence(cipherBytes, RandomIVPBECipherProvider.IV_DELIMITER) - logger.info("IV delimiter starts at ${ivDelimiterStart}") - - final byte[] EXPECTED_KDF_SALT_BYTES = TestEncryptContentGroovy.extractFullSaltFromCipherBytes(cipherBytes) - final String EXPECTED_KDF_SALT = new String(EXPECTED_KDF_SALT_BYTES) - final String EXPECTED_SALT_HEX = TestEncryptContentGroovy.extractRawSaltHexFromFullSalt(EXPECTED_KDF_SALT_BYTES, kdf) - logger.info("Extracted expected raw salt (hex): ${EXPECTED_SALT_HEX}") - - final String EXPECTED_IV_HEX = Hex.encodeHexString(cipherBytes[(ivDelimiterStart - 16).. - byte[] saltBytes = bcryptCipherProvider.generateSalt() - ByteCountingInputStream bcis = new ByteCountingInputStream(is) - ByteCountingOutputStream bcos = new ByteCountingOutputStream(os) - bcryptCipherProvider.writeSalt(saltBytes, bcos) - - Cipher cipher = bcryptCipherProvider.getInitializedCipher(encryptionMethod, PASSWORD, saltBytes, new byte[16], keyLength, true, true) - - bcryptCipherProvider.writeIV(cipher.getIV(), bcos) - CipherUtility.processStreams(cipher, bcis, bcos) - } as StreamCallback - - // Reset the streams - InputStream inputStream = new ByteArrayInputStream(PLAINTEXT.bytes) - OutputStream cipherStream = new ByteArrayOutputStream() - - customEncryptCallback.process(inputStream, cipherStream) - - byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - String cipherTextHex = Hex.encodeHexString(cipherBytes) - logger.info("Cipher text (${cipherBytes.size()}): ${cipherTextHex}") - - // Act - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - StreamCallback pbeDecryptCallback = encryptor.getDecryptionCallback() - - // Reset the streams - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) - OutputStream recoveredOutputStream = new ByteArrayOutputStream() - - // Use PBE w/ Bcrypt to decrypt (and handle legacy key derivation process) - pbeDecryptCallback.process(cipherInputStream, recoveredOutputStream) - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredOutputStream).toByteArray() - String recovered = new String(recoveredBytes, StandardCharsets.UTF_8) - logger.info("Plaintext (${recoveredBytes.size()}): ${recovered}") - - // handle reader logic error (PKCS7 padding false positive) by explicitly testing legacy key derivation - if (PLAINTEXT != recovered) { - logger.warn("Explicit test of legacy key derivation logic.") - InputStream inputStreamLegacy = new ByteArrayInputStream(cipherBytes) - OutputStream outputStreamLegacy = new ByteArrayOutputStream() - byte[] salt = bcryptCipherProvider.readSalt(inputStreamLegacy) - byte[] iv = bcryptCipherProvider.readIV(inputStreamLegacy) - Cipher cipherLegacy = bcryptCipherProvider.getLegacyDecryptCipher(encryptionMethod, PASSWORD, salt, iv, keyLength) - CipherUtility.processStreams(cipherLegacy, inputStreamLegacy, outputStreamLegacy) - String recoveredLegacy = new String(outputStreamLegacy.toByteArray(), StandardCharsets.UTF_8) - assert recoveredLegacy == PLAINTEXT - } - } - - /** - * This test was added to detect a non-deterministic problem with Scrypt expected salts being - * 32 bytes. This was ultimately determined to be a problem with the Scrypt salt regex failing - * to match salts containing a '+' in the first 12 characters. See - * {@code ScryptCipherProviderGroovyTest#testShouldAcceptFormattedSaltWithPlus( )}. - * - * @throws Exception - */ - @Test - void testScryptSaltShouldBe16Bytes() throws Exception { - // Arrange - final String PLAINTEXT = "This is a plaintext message. " - logger.info("Plaintext: ${PLAINTEXT}") - - final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC - KeyDerivationFunction kdf = KeyDerivationFunction.SCRYPT - - // Act - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - StreamCallback encryptCallback = encryptor.getEncryptionCallback() - - // Reset the streams - InputStream inputStream = new ByteArrayInputStream(PLAINTEXT.bytes) - OutputStream cipherStream = new ByteArrayOutputStream() - - encryptCallback.process(inputStream, cipherStream) - - // Assert - byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - String cipherText = new String(cipherBytes, StandardCharsets.UTF_8) - String cipherTextHex = Hex.encodeHexString(cipherBytes) - logger.info("Cipher text (${cipherBytes.size()}): ${cipherTextHex}") - - int ivDelimiterStart = CipherUtility.findSequence(cipherBytes, RandomIVPBECipherProvider.IV_DELIMITER) - logger.info("IV delimiter starts at ${ivDelimiterStart}") - - final byte[] EXPECTED_KDF_SALT_BYTES = TestEncryptContentGroovy.extractFullSaltFromCipherBytes(cipherBytes) - final String EXPECTED_KDF_SALT = new String(EXPECTED_KDF_SALT_BYTES) - final String EXPECTED_SALT_HEX = TestEncryptContentGroovy.extractRawSaltHexFromFullSalt(EXPECTED_KDF_SALT_BYTES, kdf) - logger.info("Extracted expected raw salt (hex): ${EXPECTED_SALT_HEX}") - - final String EXPECTED_IV_HEX = Hex.encodeHexString(cipherBytes[(ivDelimiterStart - 16).. - PBECipherProvider cipherProvider = (PBECipherProvider) CipherProviderFactory.getCipherProvider(kdf) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - - // reads the salt - InputStream saltInputStream = new ByteArrayInputStream(cipherBytes) - final byte[] saltBytes = cipherProvider.readSalt(saltInputStream) - - int skipLength = saltBytes.length - if (cipherProvider instanceof org.apache.nifi.security.util.crypto.OpenSSLPKCS5CipherProvider) { - skipLength += OPENSSL_EVP_HEADER_SIZE - } - - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes) - cipherInputStream.skip(skipLength) - - Exception exception = Assert.assertThrows(ProcessException.class, () -> { - decryptionCallback.process(cipherInputStream, recoveredStream) - }) - - // This is necessary to run multiple iterations - plainStream.reset() - } - } - - @Test - void testDecryptShouldHandleCipherStreamMissingSaltDelimiter() throws Exception { - // Arrange - final String SALT_DELIMITER = "NiFiSALT" - - final String PLAINTEXT = "This is a plaintext message." - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - def encryptionMethodsAndKdfs = [ - (KeyDerivationFunction.BCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.SCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.PBKDF2) : EncryptionMethod.AES_CBC - ] - - // Act - encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod -> - PBECipherProvider cipherProvider = (PBECipherProvider) CipherProviderFactory.getCipherProvider(kdf) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - final String removedDelimiterCipherString = new String(cipherBytes, StandardCharsets.UTF_8).replace(SALT_DELIMITER, "") - - InputStream cipherInputStream = new ByteArrayInputStream(removedDelimiterCipherString.getBytes(StandardCharsets.UTF_8)) - - Exception exception = Assert.assertThrows(ProcessException.class, () -> { - decryptionCallback.process(cipherInputStream, recoveredStream) - }) - - // This is necessary to run multiple iterations - plainStream.reset() - } - } - - @Test - void testDecryptShouldHandleCipherStreamMissingIV() throws Exception { - // Arrange - final String SALT_DELIMITER="NiFiSALT" - final String IV_DELIMITER = "NiFiIV" - - final String PLAINTEXT = "This is a plaintext message." - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - def encryptionMethodsAndKdfs = [ - (KeyDerivationFunction.BCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.SCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.PBKDF2) : EncryptionMethod.AES_CBC - ] - - // Act - encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod -> - PBECipherProvider cipherProvider = (PBECipherProvider) CipherProviderFactory.getCipherProvider(kdf) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - - // remove IV in cipher - final String cipherString = new String(cipherBytes, StandardCharsets.UTF_8) - final StringBuilder sb = new StringBuilder() - sb.append(cipherString.split(SALT_DELIMITER)[0]) - sb.append(SALT_DELIMITER) - sb.append(IV_DELIMITER) - sb.append(cipherString.split(IV_DELIMITER)[1]) - final String removedIVCipherString = sb.toString() - - InputStream cipherInputStream = new ByteArrayInputStream(removedIVCipherString.getBytes(StandardCharsets.UTF_8)) - - Exception exception = Assert.assertThrows(ProcessException.class, () -> { - decryptionCallback.process(cipherInputStream, recoveredStream) - }) - - // This is necessary to run multiple iterations - plainStream.reset() - } - } - - @Test - void testDecryptShouldHandleCipherStreamMissingIVDelimiter() throws Exception { - // Arrange - final String IV_DELIMITER = "NiFiIV" - - final String PLAINTEXT = "This is a plaintext message." - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")) - - def encryptionMethodsAndKdfs = [ - (KeyDerivationFunction.BCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.SCRYPT) : EncryptionMethod.AES_CBC, - (KeyDerivationFunction.PBKDF2) : EncryptionMethod.AES_CBC - ] - - // Act - encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod -> - PBECipherProvider cipherProvider = (PBECipherProvider) CipherProviderFactory.getCipherProvider(kdf) - - OutputStream cipherStream = new ByteArrayOutputStream() - OutputStream recoveredStream = new ByteArrayOutputStream() - - PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf) - - StreamCallback encryptionCallback = encryptor.getEncryptionCallback() - StreamCallback decryptionCallback = encryptor.getDecryptionCallback() - - encryptionCallback.process(plainStream, cipherStream) - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray() - final String removedDelimiterCipherString = new String(cipherBytes, StandardCharsets.UTF_8).replace(IV_DELIMITER, "") - - InputStream cipherInputStream = new ByteArrayInputStream(removedDelimiterCipherString.getBytes(StandardCharsets.UTF_8)) - - Exception exception = Assert.assertThrows(ProcessException.class, () -> { - decryptionCallback.process(cipherInputStream, recoveredStream) - }) - - // This is necessary to run multiple iterations - plainStream.reset() - } - } -} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/KeyedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/KeyedEncryptorTest.java new file mode 100644 index 0000000000..3cb792e1ca --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/KeyedEncryptorTest.java @@ -0,0 +1,117 @@ +/* + * 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.security.util.crypto; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.StreamCallback; +import org.apache.nifi.security.util.EncryptionMethod; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Test; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.Security; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class KeyedEncryptorTest { + private static final byte[] SECRET_KEY_BYTES = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 0}; + + private static final SecretKey SECRET_KEY = new SecretKeySpec(SECRET_KEY_BYTES, "AES"); + + private static final byte[] INITIALIZATION_VECTOR = new byte[]{7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7}; + + private static final byte[] PLAINTEXT = new byte[]{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + + private static final EncryptionMethod ENCRYPTION_METHOD = EncryptionMethod.AES_GCM; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + public void testEncryptDecrypt() throws IOException { + final KeyedEncryptor encryptor = new KeyedEncryptor(ENCRYPTION_METHOD, SECRET_KEY); + + assertEncryptDecryptMatched(encryptor, encryptor); + } + + @Test + public void testEncryptDecryptWithInitializationVector() throws IOException { + final KeyedEncryptor encryptor = new KeyedEncryptor(ENCRYPTION_METHOD, SECRET_KEY, INITIALIZATION_VECTOR); + final KeyedEncryptor decryptor = new KeyedEncryptor(ENCRYPTION_METHOD, SECRET_KEY); + + assertEncryptDecryptMatched(encryptor, decryptor); + } + + @Test + public void testEncryptDecryptInitializationVectorRemoved() throws IOException { + final KeyedEncryptor encryptor = new KeyedEncryptor(ENCRYPTION_METHOD, SECRET_KEY); + + final StreamCallback encryption = encryptor.getEncryptionCallback(); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final ByteArrayOutputStream encryptedOutputStream = new ByteArrayOutputStream(); + encryption.process(new ByteArrayInputStream(PLAINTEXT), encryptedOutputStream); + + final byte[] encryptedBytes = encryptedOutputStream.toByteArray(); + final byte[] encryptedBytesInitializationVectorRemoved = ArrayUtils.subarray(encryptedBytes, INITIALIZATION_VECTOR.length, encryptedBytes.length); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedBytesInitializationVectorRemoved); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + assertThrows(ProcessException.class, () -> decryption.process(encryptedInputStream, decryptedOutputStream)); + } + + @Test + public void testEncryptDecryptInitializationVectorDelimiterRemoved() throws IOException { + final KeyedEncryptor encryptor = new KeyedEncryptor(ENCRYPTION_METHOD, SECRET_KEY); + + final StreamCallback encryption = encryptor.getEncryptionCallback(); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final ByteArrayOutputStream encryptedOutputStream = new ByteArrayOutputStream(); + encryption.process(new ByteArrayInputStream(PLAINTEXT), encryptedOutputStream); + + final byte[] encryptedBytes = encryptedOutputStream.toByteArray(); + final int startIndex = INITIALIZATION_VECTOR.length + KeyedCipherProvider.IV_DELIMITER.length; + final byte[] encryptedBytesInitializationVectorRemoved = ArrayUtils.subarray(encryptedBytes, startIndex, encryptedBytes.length); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedBytesInitializationVectorRemoved); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + assertThrows(ProcessException.class, () -> decryption.process(encryptedInputStream, decryptedOutputStream)); + } + + private void assertEncryptDecryptMatched(final KeyedEncryptor encryptor, final KeyedEncryptor decryptor) throws IOException { + final StreamCallback encryption = encryptor.getEncryptionCallback(); + final StreamCallback decryption = decryptor.getDecryptionCallback(); + + final ByteArrayOutputStream encryptedOutputStream = new ByteArrayOutputStream(); + encryption.process(new ByteArrayInputStream(PLAINTEXT), encryptedOutputStream); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedOutputStream.toByteArray()); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + decryption.process(encryptedInputStream, decryptedOutputStream); + + final byte[] decrypted = decryptedOutputStream.toByteArray(); + assertArrayEquals(PLAINTEXT, decrypted); + } +} diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptorTest.java index 3f7c82c0d1..c43ae8ab7b 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptorTest.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPKeyBasedEncryptorTest.java @@ -18,131 +18,49 @@ package org.apache.nifi.security.util.crypto; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.Security; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.lang3.SystemUtils; import org.apache.nifi.processor.io.StreamCallback; import org.apache.nifi.security.util.EncryptionMethod; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPUtil; -import org.junit.After; -import org.junit.Assert; -import org.junit.Assume; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.assertArrayEquals; public class OpenPGPKeyBasedEncryptorTest { - private static final Logger logger = LoggerFactory.getLogger(OpenPGPKeyBasedEncryptorTest.class); - - private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt"); - private final File unsignedFile = new File("src/test/resources/TestEncryptContent/text.txt.unsigned.gpg"); - private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.gpg"); + private static final String FILENAME = OpenPGPKeyBasedEncryptorTest.class.getSimpleName(); private static final String SECRET_KEYRING_PATH = "src/test/resources/TestEncryptContent/secring.gpg"; + private static final String PUBLIC_KEYRING_PATH = "src/test/resources/TestEncryptContent/pubring.gpg"; + private static final String USER_ID = "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) "; private static final String PASSWORD = "thisIsABadPassword"; - @BeforeClass - public static void setUpOnce() throws Exception { - Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS); - Security.addProvider(new BouncyCastleProvider()); - } + private static final int CIPHER = PGPEncryptedData.AES_128; - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } + private static final byte[] PLAINTEXT = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8}; @Test - public void testShouldEncryptAndDecrypt() throws Exception { - for (int i = 1; i < 14; i++) { - if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped - Integer cipher = i; - logger.info("Testing PGP encryption with " + PGPUtil.getSymmetricCipherName(cipher) + " cipher."); - // Arrange - final String PLAINTEXT = "This is a plaintext message."; - logger.info("Plaintext: {}", PLAINTEXT); - InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")); - OutputStream cipherStream = new ByteArrayOutputStream(); - OutputStream recoveredStream = new ByteArrayOutputStream(); + public void testEncryptDecrypt() throws Exception { + final ByteArrayInputStream plainStream = new ByteArrayInputStream(PLAINTEXT); + final OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor( + EncryptionMethod.PGP.getAlgorithm(), CIPHER, EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], FILENAME); + StreamCallback encryptionCallback = encryptor.getEncryptionCallback(); - // No file, just streams - String filename = "tempFile.txt"; + OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor( + EncryptionMethod.PGP.getAlgorithm(), CIPHER, EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), FILENAME); + StreamCallback decryptionCallback = decryptor.getDecryptionCallback(); + final ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream(); + encryptionCallback.process(plainStream, encryptedStream); - // Encryptor does not require password - OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor( - EncryptionMethod.PGP.getAlgorithm(), cipher, EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], filename); - StreamCallback encryptionCallback = encryptor.getEncryptionCallback(); + final InputStream encryptedInputStream = new ByteArrayInputStream(encryptedStream.toByteArray()); + final ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream(); + decryptionCallback.process(encryptedInputStream, decryptedStream); - OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor( - EncryptionMethod.PGP.getAlgorithm(), cipher, EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename); - StreamCallback decryptionCallback = decryptor.getDecryptionCallback(); - - // Act - encryptionCallback.process(plainStream, cipherStream); - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray(); - logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes)); - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes); - - decryptionCallback.process(cipherInputStream, recoveredStream); - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray(); - String recovered = new String(recoveredBytes, "UTF-8"); - logger.info("Recovered: {}", recovered); - assert PLAINTEXT.equals(recovered); - } - } - } - - @Test - public void testShouldDecryptExternalFile() throws Exception { - for (int i = 1; i<14; i++) { - if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped - Integer cipher = i; - // Arrange - byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath())); - final String PLAINTEXT = new String(plainBytes, "UTF-8"); - - InputStream cipherStream = new FileInputStream(unsignedFile); - OutputStream recoveredStream = new ByteArrayOutputStream(); - - // No file, just streams - String filename = unsignedFile.getName(); - - OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor( - EncryptionMethod.PGP.getAlgorithm(), cipher, EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename); - - StreamCallback decryptionCallback = encryptor.getDecryptionCallback(); - - // Act - decryptionCallback.process(cipherStream, recoveredStream); - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray(); - String recovered = new String(recoveredBytes, "UTF-8"); - logger.info("Recovered: {}", recovered); - Assert.assertEquals("Recovered text", PLAINTEXT, recovered); - } - } + byte[] decryptedBytes = decryptedStream.toByteArray(); + assertArrayEquals(PLAINTEXT, decryptedBytes); } } diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptorTest.java index fdd330b358..d335778341 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptorTest.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/OpenPGPPasswordBasedEncryptorTest.java @@ -18,125 +18,42 @@ package org.apache.nifi.security.util.crypto; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.Security; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.lang3.SystemUtils; + import org.apache.nifi.processor.io.StreamCallback; import org.apache.nifi.security.util.EncryptionMethod; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPUtil; -import org.junit.After; -import org.junit.Assert; -import org.junit.Assume; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; public class OpenPGPPasswordBasedEncryptorTest { - private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptorTest.class); + private static final String FILENAME = OpenPGPPasswordBasedEncryptorTest.class.getSimpleName(); - private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt"); - private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.asc"); + private static final int CIPHER = PGPEncryptedData.AES_128; - private static final String PASSWORD = "thisIsABadPassword"; - private static final String LEGACY_PASSWORD = "Hello, World!"; + private static final byte[] PLAINTEXT = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8}; - @BeforeClass - public static void setUpOnce() throws Exception { - Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS); - Security.addProvider(new BouncyCastleProvider()); - } - - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } + private static final String PASSWORD = OpenPGPPasswordBasedEncryptorTest.class.getName(); @Test - public void testShouldEncryptAndDecrypt() throws Exception { + public void testEncryptDecrypt() throws Exception { + final ByteArrayInputStream plainStream = new ByteArrayInputStream(PLAINTEXT); - for (int i = 1; i<14; i++) { - if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped - Integer cipher = i; - logger.info("Testing PGP encryption with " + PGPUtil.getSymmetricCipherName(cipher) + " cipher."); + final OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), + CIPHER, EncryptionMethod.PGP.getProvider(), PASSWORD.toCharArray(), FILENAME); - // Arrange - final String PLAINTEXT = "This is a plaintext message."; - logger.info("Plaintext: {}", PLAINTEXT); - InputStream plainStream = new java.io.ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8")); - OutputStream cipherStream = new ByteArrayOutputStream(); - OutputStream recoveredStream = new ByteArrayOutputStream(); + final StreamCallback encryptionCallback = encryptor.getEncryptionCallback(); + final StreamCallback decryptionCallback = encryptor.getDecryptionCallback(); - // No file, just streams - String filename = "tempFile.txt"; + final ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream(); + encryptionCallback.process(plainStream, encryptedStream); - OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), - cipher, EncryptionMethod.PGP.getProvider(), PASSWORD.toCharArray(), filename); + final InputStream encryptedInputStream = new ByteArrayInputStream(encryptedStream.toByteArray()); + final ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream(); + decryptionCallback.process(encryptedInputStream, decryptedStream); - StreamCallback encryptionCallback = encryptor.getEncryptionCallback(); - StreamCallback decryptionCallback = encryptor.getDecryptionCallback(); - - // Act - encryptionCallback.process(plainStream, cipherStream); - - final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray(); - logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes)); - InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes); - - decryptionCallback.process(cipherInputStream, recoveredStream); - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray(); - String recovered = new String(recoveredBytes, "UTF-8"); - logger.info("Recovered: {}", recovered); - assert PLAINTEXT.equals(recovered); - } - } - } - - @Test - public void testShouldDecryptExternalFile() throws Exception { - for (int i = 1; i<14; i++) { - if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped - Integer cipher = i; - // Arrange - byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath())); - final String PLAINTEXT = new String(plainBytes, "UTF-8"); - - InputStream cipherStream = new FileInputStream(encryptedFile); - OutputStream recoveredStream = new ByteArrayOutputStream(); - - // No file, just streams - String filename = encryptedFile.getName(); - - OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), cipher, - EncryptionMethod.PGP.getProvider(), LEGACY_PASSWORD.toCharArray(), filename); - - StreamCallback decryptionCallback = encryptor.getDecryptionCallback(); - - // Act - decryptionCallback.process(cipherStream, recoveredStream); - - // Assert - byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray(); - String recovered = new String(recoveredBytes, "UTF-8"); - logger.info("Recovered: {}", recovered); - Assert.assertEquals("Recovered text", PLAINTEXT, recovered); - } - } + byte[] decryptedBytes = decryptedStream.toByteArray(); + assertArrayEquals(PLAINTEXT, decryptedBytes); } } diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorTest.java new file mode 100644 index 0000000000..08cda8b74b --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorTest.java @@ -0,0 +1,240 @@ +/* + * 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.security.util.crypto; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.StreamCallback; +import org.apache.nifi.security.util.EncryptionMethod; +import org.apache.nifi.security.util.KeyDerivationFunction; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Security; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PasswordBasedEncryptorTest { + private static final String TEST_RESOURCES_PATH = "src/test/resources/TestEncryptContent"; + + private static final char[] FILE_PASSWORD = "thisIsABadPassword".toCharArray(); + + private static final Path TEST_SALTED_PATH = Paths.get(String.format("%s/salted_128_raw.enc", TEST_RESOURCES_PATH)); + + private static final Path TEST_UNSALTED_PATH = Paths.get(String.format("%s/unsalted_128_raw.enc", TEST_RESOURCES_PATH)); + + private static final Path TEST_PLAIN_PATH = Paths.get(String.format("%s/plain.txt", TEST_RESOURCES_PATH)); + + private static final byte[] PLAINTEXT = new byte[]{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + + private static final char[] PASSWORD = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g'}; + + private static final int SALT_LENGTH = RandomIVPBECipherProvider.SALT_DELIMITER.length; + + private static final String INITIALIZATION_VECTOR_LENGTH = Integer.toString(RandomIVPBECipherProvider.MAX_IV_LIMIT); + + private static final String IV_ATTRIBUTE = "iv"; + + private static final EncryptionMethod PBE_ENCRYPTION_METHOD = EncryptionMethod.SHA256_256AES; + + private static final EncryptionMethod KDF_ENCRYPTION_METHOD = EncryptionMethod.AES_GCM; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + public void testEncryptDecryptLegacy() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(PBE_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.NIFI_LEGACY); + + assertEncryptDecryptMatched(encryptor); + } + + @Test + public void testEncryptDecryptOpenSsl() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(PBE_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY); + + assertEncryptDecryptMatched(encryptor); + } + + @Test + public void testEncryptDecryptBcrypt() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.BCRYPT); + + assertEncryptDecryptMatched(encryptor); + } + + @Test + public void testEncryptDecryptScrypt() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.SCRYPT); + + assertEncryptDecryptMatched(encryptor); + } + + @Test + public void testEncryptDecryptPbkdf2() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.PBKDF2); + + assertEncryptDecryptMatched(encryptor); + } + + @Test + public void testDecryptOpenSslSalted() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(EncryptionMethod.MD5_128AES, FILE_PASSWORD, KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY); + + final byte[] plainBytes = Files.readAllBytes(TEST_PLAIN_PATH); + final byte[] encryptedBytes = Files.readAllBytes(TEST_SALTED_PATH); + + assertDecryptMatched(encryptor, encryptedBytes, plainBytes); + } + + @Test + public void testDecryptOpenSslUnsalted() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(EncryptionMethod.MD5_128AES, FILE_PASSWORD, KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY); + + final byte[] plainBytes = Files.readAllBytes(TEST_PLAIN_PATH); + final byte[] encryptedBytes = Files.readAllBytes(TEST_UNSALTED_PATH); + + assertDecryptMatched(encryptor, encryptedBytes, plainBytes); + } + + @Test + public void testEncryptDecryptArgon2SkippedSaltMissing() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.ARGON2); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final byte[] encryptedBytes = encryptBytes(encryptor); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedBytes); + final long skipped = encryptedInputStream.skip(SALT_LENGTH); + assertEquals(SALT_LENGTH, skipped); + + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + assertThrows(ProcessException.class, () -> decryption.process(encryptedInputStream, decryptedOutputStream)); + } + + @Test + public void testEncryptDecryptArgon2SaltDelimiterMissing() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.ARGON2); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final byte[] encryptedBytes = encryptBytes(encryptor); + final byte[] delimiterRemoved = ArrayUtils.removeElements(encryptedBytes, RandomIVPBECipherProvider.SALT_DELIMITER); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(delimiterRemoved); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + assertThrows(ProcessException.class, () -> decryption.process(encryptedInputStream, decryptedOutputStream)); + } + + @Test + public void testEncryptDecryptArgon2InitializationVectorMissing() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.ARGON2); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final StreamCallback encryption = encryptor.getEncryptionCallback(); + + final ByteArrayOutputStream encryptedOutputStream = new ByteArrayOutputStream(); + encryption.process(new ByteArrayInputStream(PLAINTEXT), encryptedOutputStream); + final byte[] encryptedBytes = encryptedOutputStream.toByteArray(); + + final String initializationVectorAttribute = encryptor.flowfileAttributes.get(getAttributeName(IV_ATTRIBUTE)); + final byte[] initializationVector = initializationVectorAttribute.getBytes(StandardCharsets.UTF_8); + + final byte[] encryptedBytesUpdated = ArrayUtils.removeElements(encryptedBytes, initializationVector); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedBytesUpdated); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + assertThrows(ProcessException.class, () -> decryption.process(encryptedInputStream, decryptedOutputStream)); + } + + @Test + public void testEncryptDecryptArgon2InitializationVectorDelimiterMissing() throws IOException { + final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(KDF_ENCRYPTION_METHOD, PASSWORD, KeyDerivationFunction.ARGON2); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final byte[] encryptedBytes = encryptBytes(encryptor); + final byte[] encryptedBytesUpdated = ArrayUtils.removeElements(encryptedBytes, RandomIVPBECipherProvider.IV_DELIMITER); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedBytesUpdated); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + assertThrows(ProcessException.class, () -> decryption.process(encryptedInputStream, decryptedOutputStream)); + } + + private byte[] encryptBytes(final PasswordBasedEncryptor encryptor) throws IOException { + final StreamCallback encryption = encryptor.getEncryptionCallback(); + + final ByteArrayOutputStream encryptedOutputStream = new ByteArrayOutputStream(); + encryption.process(new ByteArrayInputStream(PLAINTEXT), encryptedOutputStream); + + return encryptedOutputStream.toByteArray(); + } + + private void assertEncryptDecryptMatched(final PasswordBasedEncryptor encryptor) throws IOException { + final StreamCallback encryption = encryptor.getEncryptionCallback(); + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final ByteArrayOutputStream encryptedOutputStream = new ByteArrayOutputStream(); + encryption.process(new ByteArrayInputStream(PLAINTEXT), encryptedOutputStream); + + final ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedOutputStream.toByteArray()); + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + decryption.process(encryptedInputStream, decryptedOutputStream); + + final byte[] decrypted = decryptedOutputStream.toByteArray(); + assertArrayEquals(PLAINTEXT, decrypted); + + assertFlowFileAttributesFound(encryptor.flowfileAttributes); + } + + private void assertFlowFileAttributesFound(final Map attributes) { + assertTrue(attributes.containsKey(getAttributeName("algorithm"))); + assertTrue(attributes.containsKey(getAttributeName("timestamp"))); + assertTrue(attributes.containsKey(getAttributeName("cipher_text_length"))); + assertEquals("decrypted", attributes.get(getAttributeName("action"))); + assertEquals(Integer.toString(PLAINTEXT.length), attributes.get(getAttributeName("plaintext_length"))); + assertTrue(attributes.containsKey(getAttributeName("salt"))); + assertTrue(attributes.containsKey(getAttributeName("salt_length"))); + assertTrue(attributes.containsKey(getAttributeName(IV_ATTRIBUTE))); + assertEquals(INITIALIZATION_VECTOR_LENGTH, attributes.get(getAttributeName("iv_length"))); + assertTrue(attributes.containsKey(getAttributeName("kdf"))); + } + + private void assertDecryptMatched(final PasswordBasedEncryptor encryptor, final byte[] encrypted, final byte[] expected) throws IOException { + final StreamCallback decryption = encryptor.getDecryptionCallback(); + + final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream(); + decryption.process(new ByteArrayInputStream(encrypted), decryptedOutputStream); + + final byte[] decrypted = decryptedOutputStream.toByteArray(); + assertArrayEquals(expected, decrypted); + } + + private String getAttributeName(final String name) { + return String.format("encryptcontent.%s", name); + } +}