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.processor.io.StreamCallback
import org.apache.nifi.security.util.EncryptionMethod import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.KeyDerivationFunction import org.apache.nifi.security.util.KeyDerivationFunction
import org.apache.nifi.stream.io.exception.BytePatternNotFoundException
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import org.junit.Assert
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -37,8 +37,6 @@ class KeyedEncryptorGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class) private static final Logger logger = LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class)
private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/" 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 String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final SecretKey KEY = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES") 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 @Test
void testShouldEncryptAndDecrypt() throws Exception { void testShouldEncryptAndDecrypt() throws Exception {
// Arrange // Arrange
@ -191,4 +181,79 @@ class KeyedEncryptorGroovyTest {
[l.first(), Integer.valueOf(l.last())] [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.security.util.KeyDerivationFunction
import org.apache.nifi.stream.io.ByteCountingInputStream import org.apache.nifi.stream.io.ByteCountingInputStream
import org.apache.nifi.stream.io.ByteCountingOutputStream import org.apache.nifi.stream.io.ByteCountingOutputStream
import org.apache.nifi.stream.io.exception.BytePatternNotFoundException
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assume import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import org.junit.Assert
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -56,14 +56,6 @@ class PasswordBasedEncryptorGroovyTest {
} }
} }
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
@Test @Test
void testShouldEncryptAndDecrypt() throws Exception { void testShouldEncryptAndDecrypt() throws Exception {
// Arrange // Arrange
@ -516,4 +508,205 @@ class PasswordBasedEncryptorGroovyTest {
assert encryptor.flowfileAttributes.get("encryptcontent.kdf_salt") == EXPECTED_KDF_SALT assert encryptor.flowfileAttributes.get("encryptcontent.kdf_salt") == EXPECTED_KDF_SALT
assert (29..54)*.toString().contains(encryptor.flowfileAttributes.get("encryptcontent.kdf_salt_length")) 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()
}
}
} }