This is an automated email from the ASF dual-hosted git repository. joewitt pushed a commit to branch support/nifi-1.16 in repository https://gitbox.apache.org/repos/asf/nifi.git
commit 0d3ad93120734f87cd319cb5f2882b68bbe62583 Author: Emilio Setiadarma <[email protected]> AuthorDate: Wed Mar 16 10:48:57 2022 -0700 NIFI-1468 Added tests to handle invalid cipher streams missing Salt/IV - Updated PasswordBasedEncryptorGroovyTest and KeyedEncryptorGroovyTest This closes #5877 Signed-off-by: David Handermann <[email protected]> --- .../util/crypto/KeyedEncryptorGroovyTest.groovy | 89 +++++++-- .../crypto/PasswordBasedEncryptorGroovyTest.groovy | 213 ++++++++++++++++++++- 2 files changed, 280 insertions(+), 22 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 index a3931eb5c6..1b6110bcf1 100644 --- 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 @@ -20,11 +20,11 @@ import org.apache.commons.codec.binary.Hex 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.After -import org.junit.Before import org.junit.BeforeClass import org.junit.Test +import org.junit.Assert import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -37,8 +37,6 @@ 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 File plainFile = new File("${TEST_RESOURCES_PREFIX}/plain.txt") - private static final File encryptedFile = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.asc") private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" private static final SecretKey KEY = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES") @@ -52,14 +50,6 @@ class KeyedEncryptorGroovyTest { } } - @Before - void setUp() throws Exception { - } - - @After - void tearDown() throws Exception { - } - @Test void testShouldEncryptAndDecrypt() throws Exception { // Arrange @@ -191,4 +181,79 @@ class KeyedEncryptorGroovyTest { [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(Exception.class, () -> { + decryptionCallback.process(cipherInputStream, recoveredStream) + }) + + // Assert + assert exception.getCause() instanceof BytePatternNotFoundException + } + + @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(Exception.class, () -> { + decryptionCallback.process(cipherInputStream, recoveredStream) + }) + + // Assert + assert exception.getCause() instanceof BytePatternNotFoundException + } } \ 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 index 238fba3917..96c7435221 100644 --- 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 @@ -23,12 +23,12 @@ 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.After import org.junit.Assume -import org.junit.Before import org.junit.BeforeClass import org.junit.Test +import org.junit.Assert import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -56,14 +56,6 @@ class PasswordBasedEncryptorGroovyTest { } } - @Before - void setUp() throws Exception { - } - - @After - void tearDown() throws Exception { - } - @Test void testShouldEncryptAndDecrypt() throws Exception { // Arrange @@ -516,4 +508,205 @@ class PasswordBasedEncryptorGroovyTest { 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(Exception.class, () -> { + decryptionCallback.process(cipherInputStream, recoveredStream) + }) + + // Assert + if (!(cipherProvider instanceof OpenSSLPKCS5CipherProvider)) { + assert exception.getCause() instanceof IllegalArgumentException + } + + // 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(Exception.class, () -> { + decryptionCallback.process(cipherInputStream, recoveredStream) + }) + + // Assert + assert exception.getCause() instanceof BytePatternNotFoundException + + // 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(Exception.class, () -> { + decryptionCallback.process(cipherInputStream, recoveredStream) + }) + + // Assert + assert exception.getCause() instanceof IllegalArgumentException + + // 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(Exception.class, () -> { + decryptionCallback.process(cipherInputStream, recoveredStream) + }) + + // Assert + assert exception.getCause() instanceof BytePatternNotFoundException + + // This is necessary to run multiple iterations + plainStream.reset() + } + } } \ No newline at end of file
