NIFI-1468 Added tests to handle invalid cipher streams missing Salt/IV

- Updated PasswordBasedEncryptorGroovyTest and KeyedEncryptorGroovyTest

This closes #5877

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Emilio Setiadarma 2022-03-16 10:48:57 -07:00 committed by exceptionfactory
parent 5928d2048e
commit 772adbc709
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
2 changed files with 280 additions and 22 deletions

View File

@ -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
}
}

View File

@ -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()
}
}
}