This is an automated email from the ASF dual-hosted git repository.
thenatog pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new cf21bc4 NIFI-9844 Refactored Encryptor tests using JUnit 5
cf21bc4 is described below
commit cf21bc47cd63e316eaa4a899f8b3373a6ca1b1fc
Author: exceptionfactory <[email protected]>
AuthorDate: Mon Mar 28 23:05:09 2022 -0500
NIFI-9844 Refactored Encryptor tests using JUnit 5
- Refactored Keyed and Password Based Encryptor tests from Groovy to Java
Signed-off-by: Nathan Gough <[email protected]>
This closes #5913.
---
.../util/crypto/KeyedEncryptorGroovyTest.groovy | 254 --------
.../crypto/PasswordBasedEncryptorGroovyTest.groovy | 699 ---------------------
.../security/util/crypto/KeyedEncryptorTest.java | 117 ++++
.../util/crypto/OpenPGPKeyBasedEncryptorTest.java | 130 +---
.../crypto/OpenPGPPasswordBasedEncryptorTest.java | 125 +---
.../util/crypto/PasswordBasedEncryptorTest.java | 240 +++++++
6 files changed, 402 insertions(+), 1163 deletions(-)
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 ab2d0f7..0000000
---
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<String, Integer> 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 d79474e..0000000
---
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<EncryptionMethod> 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)..<ivDelimiterStart] as
byte[])
-
-
TestEncryptContentGroovy.printFlowFileAttributes(encryptor.flowfileAttributes)
-
- // Assert the timestamp attribute was written and is accurate
- def diff =
TestEncryptContentGroovy.calculateTimestampDifference(new Date(),
encryptor.flowfileAttributes.get("encryptcontent.timestamp"))
- assert diff.toMilliseconds() < 1_000
- assert
encryptor.flowfileAttributes.get("encryptcontent.algorithm") ==
encryptionMethod.name()
- assert encryptor.flowfileAttributes.get("encryptcontent.kdf") ==
kdf.name()
- assert encryptor.flowfileAttributes.get("encryptcontent.action")
== "encrypted"
- assert encryptor.flowfileAttributes.get("encryptcontent.salt") ==
EXPECTED_SALT_HEX
- assert
encryptor.flowfileAttributes.get("encryptcontent.salt_length") == "16"
- assert encryptor.flowfileAttributes.get("encryptcontent.iv") ==
EXPECTED_IV_HEX
- assert
encryptor.flowfileAttributes.get("encryptcontent.iv_length") == "16"
- assert
encryptor.flowfileAttributes.get("encryptcontent.plaintext_length") ==
PLAINTEXT.size() as String
- assert
encryptor.flowfileAttributes.get("encryptcontent.cipher_text_length") ==
cipherBytes.size() as String
-
- // PBKDF2 doesn't have a KDF salt, just the raw byte[16]
- if (kdf != KeyDerivationFunction.PBKDF2) {
- assert
encryptor.flowfileAttributes.get("encryptcontent.kdf_salt") == EXPECTED_KDF_SALT
- assert
(29..54)*.toString().contains(encryptor.flowfileAttributes.get("encryptcontent.kdf_salt_length"))
- }
- }
- }
-
- @Test
- void testPBKDF2ShouldWriteIterationsAsAttribute() throws Exception {
- // Arrange
- final String PLAINTEXT = "This is a plaintext message. "
- logger.info("Plaintext: ${PLAINTEXT}")
-
- final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
- KeyDerivationFunction kdf = KeyDerivationFunction.PBKDF2
- PBKDF2CipherProvider pbkdf2CipherProvider = new PBKDF2CipherProvider()
- final String EXPECTED_ITERATIONS =
pbkdf2CipherProvider.getIterationCount() as String
-
- // 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 cipherTextHex = Hex.encodeHexString(cipherBytes)
- logger.info("Cipher text (${cipherBytes.size()}): ${cipherTextHex}")
-
-
TestEncryptContentGroovy.printFlowFileAttributes(encryptor.flowfileAttributes)
-
- assert encryptor.flowfileAttributes.get("encryptcontent.algorithm") ==
encryptionMethod.name()
- assert encryptor.flowfileAttributes.get("encryptcontent.kdf") ==
kdf.name()
- assert encryptor.flowfileAttributes.get("encryptcontent.action") ==
"encrypted"
- assert
encryptor.flowfileAttributes.get("encryptcontent.pbkdf2_iterations") ==
EXPECTED_ITERATIONS
- }
-
- @Test
- void testBcryptDecryptShouldSupportLegacyKeyDerivationProcess() throws
Exception {
- // Arrange
- final String PLAINTEXT = "This is a plaintext message. "
- logger.info("Plaintext: ${PLAINTEXT}")
-
- final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
- KeyDerivationFunction kdf = KeyDerivationFunction.BCRYPT
- BcryptCipherProvider bcryptCipherProvider = new BcryptCipherProvider()
-
- // Replicate PBE encryptor with manual legacy key derivation to encrypt
- final String PASSWORD = "shortPassword"
- final byte[] SALT = bcryptCipherProvider.generateSalt()
- String saltString = new String(SALT, StandardCharsets.UTF_8)
- logger.test("Using fixed Bcrypt salt: ${saltString}")
-
- // Determine the expected key bytes using the legacy key derivation
process
- BcryptSecureHasher bcryptSecureHasher = new
BcryptSecureHasher(bcryptCipherProvider.getWorkFactor(),
bcryptCipherProvider.getDefaultSaltLength())
- byte[] rawSaltBytes = BcryptCipherProvider.extractRawSalt(saltString)
- byte[] hashOutputBytes =
bcryptSecureHasher.hashRaw(PASSWORD.getBytes(StandardCharsets.UTF_8),
rawSaltBytes)
- logger.test("Raw hash output (${hashOutputBytes.length}):
${Hex.encodeHexString(hashOutputBytes)}")
-
- MessageDigest sha512 = MessageDigest.getInstance("SHA-512", "BC")
- byte[] keyDigestBytes = sha512.digest(hashOutputBytes)
- logger.test("Key digest (${keyDigestBytes.length}):
${Hex.encodeHexString(keyDigestBytes)}")
-
- int keyLength =
CipherUtility.parseKeyLengthFromAlgorithm(encryptionMethod.algorithm)
- byte[] derivedKeyBytes = Arrays.copyOf(keyDigestBytes, keyLength / 8
as int)
- logger.test("Derived key (${derivedKeyBytes.length}):
${Hex.encodeHexString(derivedKeyBytes)}")
-
- StreamCallback customEncryptCallback = { InputStream is, OutputStream
os ->
- 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)..<ivDelimiterStart] as
byte[])
-
-
TestEncryptContentGroovy.printFlowFileAttributes(encryptor.flowfileAttributes)
-
- // Assert the timestamp attribute was written and is accurate
- def diff = TestEncryptContentGroovy.calculateTimestampDifference(new
Date(), encryptor.flowfileAttributes.get("encryptcontent.timestamp"))
- assert diff.toMilliseconds() < 1_000
- assert encryptor.flowfileAttributes.get("encryptcontent.algorithm") ==
encryptionMethod.name()
- assert encryptor.flowfileAttributes.get("encryptcontent.kdf") ==
kdf.name()
- assert encryptor.flowfileAttributes.get("encryptcontent.action") ==
"encrypted"
- assert encryptor.flowfileAttributes.get("encryptcontent.salt") ==
EXPECTED_SALT_HEX
- assert encryptor.flowfileAttributes.get("encryptcontent.salt_length")
== "16"
- assert encryptor.flowfileAttributes.get("encryptcontent.iv") ==
EXPECTED_IV_HEX
- assert encryptor.flowfileAttributes.get("encryptcontent.iv_length") ==
"16"
- assert
encryptor.flowfileAttributes.get("encryptcontent.plaintext_length") ==
PLAINTEXT.size() as String
- assert
encryptor.flowfileAttributes.get("encryptcontent.cipher_text_length") ==
cipherBytes.size() as String
-
- assert encryptor.flowfileAttributes.get("encryptcontent.kdf_salt") ==
EXPECTED_KDF_SALT
- assert
(29..54)*.toString().contains(encryptor.flowfileAttributes.get("encryptcontent.kdf_salt_length"))
- }
-
- @Test
- void testDecryptShouldHandleCipherStreamMissingSalt() throws Exception {
- // Arrange
- final int OPENSSL_EVP_HEADER_SIZE = 8
-
- final String PLAINTEXT = "This is a plaintext message."
- InputStream plainStream = new
ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"))
-
- def encryptionMethodsAndKdfs = [
- (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY):
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 ->
- 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 0000000..3cb792e
--- /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 3f7c82c..c43ae8a 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;
-public class OpenPGPKeyBasedEncryptorTest {
- private static final Logger logger =
LoggerFactory.getLogger(OpenPGPKeyBasedEncryptorTest.class);
+import org.junit.jupiter.api.Test;
+
+import static org.junit.Assert.assertArrayEquals;
- 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");
+public class OpenPGPKeyBasedEncryptorTest {
+ 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) <[email protected]>";
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());
- }
-
- @Before
- public void setUp() throws Exception {
-
- }
-
- @After
- public void tearDown() throws Exception {
-
- }
-
- @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();
-
- // No file, just streams
- String filename = "tempFile.txt";
-
-
- // 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();
-
- OpenPGPKeyBasedEncryptor decryptor = new
OpenPGPKeyBasedEncryptor(
- EncryptionMethod.PGP.getAlgorithm(), cipher,
EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID,
PASSWORD.toCharArray(), filename);
- StreamCallback decryptionCallback =
decryptor.getDecryptionCallback();
+ private static final int CIPHER = PGPEncryptedData.AES_128;
- // 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);
- }
- }
- }
+ private static final byte[] PLAINTEXT = new byte[]{0, 1, 2, 3, 4, 5, 6, 7,
8};
@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();
+ 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();
- OpenPGPKeyBasedEncryptor encryptor = new
OpenPGPKeyBasedEncryptor(
- EncryptionMethod.PGP.getAlgorithm(), cipher,
EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID,
PASSWORD.toCharArray(), filename);
+ OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor(
+ EncryptionMethod.PGP.getAlgorithm(), CIPHER,
EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID,
PASSWORD.toCharArray(), FILENAME);
+ StreamCallback decryptionCallback = decryptor.getDecryptionCallback();
- StreamCallback decryptionCallback =
encryptor.getDecryptionCallback();
+ final ByteArrayOutputStream encryptedStream = new
ByteArrayOutputStream();
+ encryptionCallback.process(plainStream, encryptedStream);
- // Act
- decryptionCallback.process(cipherStream, recoveredStream);
+ final InputStream encryptedInputStream = new
ByteArrayInputStream(encryptedStream.toByteArray());
+ final ByteArrayOutputStream decryptedStream = new
ByteArrayOutputStream();
+ decryptionCallback.process(encryptedInputStream, decryptedStream);
- // 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 fdd330b..d335778 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;
-
-public class OpenPGPPasswordBasedEncryptorTest {
- private static final Logger logger =
LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptorTest.class);
-
- private final File plainFile = new
File("src/test/resources/TestEncryptContent/text.txt");
- private final File encryptedFile = new
File("src/test/resources/TestEncryptContent/text.txt.asc");
-
- private static final String PASSWORD = "thisIsABadPassword";
- private static final String LEGACY_PASSWORD = "Hello, World!";
-
- @BeforeClass
- public static void setUpOnce() throws Exception {
- 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 {
+import org.junit.jupiter.api.Test;
- }
-
- @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
java.io.ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"));
- OutputStream cipherStream = new ByteArrayOutputStream();
- OutputStream recoveredStream = new ByteArrayOutputStream();
-
- // No file, just streams
- String filename = "tempFile.txt";
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
- OpenPGPPasswordBasedEncryptor encryptor = new
OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(),
- cipher, EncryptionMethod.PGP.getProvider(),
PASSWORD.toCharArray(), filename);
-
- StreamCallback encryptionCallback =
encryptor.getEncryptionCallback();
- StreamCallback decryptionCallback =
encryptor.getDecryptionCallback();
+public class OpenPGPPasswordBasedEncryptorTest {
+ private static final String FILENAME =
OpenPGPPasswordBasedEncryptorTest.class.getSimpleName();
- // Act
- encryptionCallback.process(plainStream, cipherStream);
+ private static final int CIPHER = PGPEncryptedData.AES_128;
- final byte[] cipherBytes = ((ByteArrayOutputStream)
cipherStream).toByteArray();
- logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
- InputStream cipherInputStream = new
ByteArrayInputStream(cipherBytes);
+ private static final byte[] PLAINTEXT = new byte[]{0, 1, 2, 3, 4, 5, 6, 7,
8};
- 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);
- }
- }
- }
+ private static final String PASSWORD =
OpenPGPPasswordBasedEncryptorTest.class.getName();
@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();
+ public void testEncryptDecrypt() throws Exception {
+ final ByteArrayInputStream plainStream = new
ByteArrayInputStream(PLAINTEXT);
- // No file, just streams
- String filename = encryptedFile.getName();
+ final OpenPGPPasswordBasedEncryptor encryptor = new
OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(),
+ CIPHER, EncryptionMethod.PGP.getProvider(),
PASSWORD.toCharArray(), FILENAME);
- OpenPGPPasswordBasedEncryptor encryptor = new
OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), cipher,
- EncryptionMethod.PGP.getProvider(),
LEGACY_PASSWORD.toCharArray(), filename);
+ final StreamCallback encryptionCallback =
encryptor.getEncryptionCallback();
+ final StreamCallback decryptionCallback =
encryptor.getDecryptionCallback();
- StreamCallback decryptionCallback =
encryptor.getDecryptionCallback();
+ final ByteArrayOutputStream encryptedStream = new
ByteArrayOutputStream();
+ encryptionCallback.process(plainStream, encryptedStream);
- // Act
- decryptionCallback.process(cipherStream, recoveredStream);
+ final InputStream encryptedInputStream = new
ByteArrayInputStream(encryptedStream.toByteArray());
+ final ByteArrayOutputStream decryptedStream = new
ByteArrayOutputStream();
+ decryptionCallback.process(encryptedInputStream, decryptedStream);
- // 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 0000000..08cda8b
--- /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<String, String>
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);
+ }
+}