mirror of https://github.com/apache/nifi.git
NIFI-9844 Refactored Encryptor tests using JUnit 5
- Refactored Keyed and Password Based Encryptor tests from Groovy to Java Signed-off-by: Nathan Gough <thenatog@gmail.com> This closes #5913.
This commit is contained in:
parent
14e4f1bfc2
commit
cf21bc47cd
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,131 +18,49 @@ package org.apache.nifi.security.util.crypto;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.InputStream;
|
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.processor.io.StreamCallback;
|
||||||
import org.apache.nifi.security.util.EncryptionMethod;
|
import org.apache.nifi.security.util.EncryptionMethod;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import org.bouncycastle.openpgp.PGPEncryptedData;
|
import org.bouncycastle.openpgp.PGPEncryptedData;
|
||||||
import org.bouncycastle.openpgp.PGPUtil;
|
|
||||||
import org.junit.After;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Assume;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class OpenPGPKeyBasedEncryptorTest {
|
public class OpenPGPKeyBasedEncryptorTest {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(OpenPGPKeyBasedEncryptorTest.class);
|
private static final String FILENAME = OpenPGPKeyBasedEncryptorTest.class.getSimpleName();
|
||||||
|
|
||||||
private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
|
|
||||||
private final File unsignedFile = new File("src/test/resources/TestEncryptContent/text.txt.unsigned.gpg");
|
|
||||||
private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.gpg");
|
|
||||||
|
|
||||||
private static final String SECRET_KEYRING_PATH = "src/test/resources/TestEncryptContent/secring.gpg";
|
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 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) <alopresto.apache+test@gmail.com>";
|
private static final String USER_ID = "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) <alopresto.apache+test@gmail.com>";
|
||||||
|
|
||||||
private static final String PASSWORD = "thisIsABadPassword";
|
private static final String PASSWORD = "thisIsABadPassword";
|
||||||
|
|
||||||
@BeforeClass
|
private static final int CIPHER = PGPEncryptedData.AES_128;
|
||||||
public static void setUpOnce() throws Exception {
|
|
||||||
Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS);
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
private static final byte[] PLAINTEXT = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8};
|
||||||
public void setUp() throws Exception {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void tearDown() throws Exception {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldEncryptAndDecrypt() throws Exception {
|
public void testEncryptDecrypt() throws Exception {
|
||||||
for (int i = 1; i < 14; i++) {
|
final ByteArrayInputStream plainStream = new ByteArrayInputStream(PLAINTEXT);
|
||||||
if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped
|
final OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(
|
||||||
Integer cipher = i;
|
EncryptionMethod.PGP.getAlgorithm(), CIPHER, EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], FILENAME);
|
||||||
logger.info("Testing PGP encryption with " + PGPUtil.getSymmetricCipherName(cipher) + " cipher.");
|
StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
|
||||||
// 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
|
OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor(
|
||||||
String filename = "tempFile.txt";
|
EncryptionMethod.PGP.getAlgorithm(), CIPHER, EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), FILENAME);
|
||||||
|
StreamCallback decryptionCallback = decryptor.getDecryptionCallback();
|
||||||
|
|
||||||
|
final ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream();
|
||||||
|
encryptionCallback.process(plainStream, encryptedStream);
|
||||||
|
|
||||||
// Encryptor does not require password
|
final InputStream encryptedInputStream = new ByteArrayInputStream(encryptedStream.toByteArray());
|
||||||
OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(
|
final ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream();
|
||||||
EncryptionMethod.PGP.getAlgorithm(), cipher, EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], filename);
|
decryptionCallback.process(encryptedInputStream, decryptedStream);
|
||||||
StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
|
|
||||||
|
|
||||||
OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor(
|
byte[] decryptedBytes = decryptedStream.toByteArray();
|
||||||
EncryptionMethod.PGP.getAlgorithm(), cipher, EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
|
assertArrayEquals(PLAINTEXT, decryptedBytes);
|
||||||
StreamCallback decryptionCallback = decryptor.getDecryptionCallback();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
encryptionCallback.process(plainStream, cipherStream);
|
|
||||||
|
|
||||||
final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray();
|
|
||||||
logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
|
|
||||||
InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes);
|
|
||||||
|
|
||||||
decryptionCallback.process(cipherInputStream, recoveredStream);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
|
|
||||||
String recovered = new String(recoveredBytes, "UTF-8");
|
|
||||||
logger.info("Recovered: {}", recovered);
|
|
||||||
assert PLAINTEXT.equals(recovered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testShouldDecryptExternalFile() throws Exception {
|
|
||||||
for (int i = 1; i<14; i++) {
|
|
||||||
if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped
|
|
||||||
Integer cipher = i;
|
|
||||||
// Arrange
|
|
||||||
byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
|
|
||||||
final String PLAINTEXT = new String(plainBytes, "UTF-8");
|
|
||||||
|
|
||||||
InputStream cipherStream = new FileInputStream(unsignedFile);
|
|
||||||
OutputStream recoveredStream = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
// No file, just streams
|
|
||||||
String filename = unsignedFile.getName();
|
|
||||||
|
|
||||||
OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(
|
|
||||||
EncryptionMethod.PGP.getAlgorithm(), cipher, EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
|
|
||||||
|
|
||||||
StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
decryptionCallback.process(cipherStream, recoveredStream);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
|
|
||||||
String recovered = new String(recoveredBytes, "UTF-8");
|
|
||||||
logger.info("Recovered: {}", recovered);
|
|
||||||
Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,125 +18,42 @@ package org.apache.nifi.security.util.crypto;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.InputStream;
|
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.processor.io.StreamCallback;
|
||||||
import org.apache.nifi.security.util.EncryptionMethod;
|
import org.apache.nifi.security.util.EncryptionMethod;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import org.bouncycastle.openpgp.PGPEncryptedData;
|
import org.bouncycastle.openpgp.PGPEncryptedData;
|
||||||
import org.bouncycastle.openpgp.PGPUtil;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Assert;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
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 {
|
public class OpenPGPPasswordBasedEncryptorTest {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptorTest.class);
|
private static final String FILENAME = OpenPGPPasswordBasedEncryptorTest.class.getSimpleName();
|
||||||
|
|
||||||
private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
|
private static final int CIPHER = PGPEncryptedData.AES_128;
|
||||||
private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.asc");
|
|
||||||
|
|
||||||
private static final String PASSWORD = "thisIsABadPassword";
|
private static final byte[] PLAINTEXT = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8};
|
||||||
private static final String LEGACY_PASSWORD = "Hello, World!";
|
|
||||||
|
|
||||||
@BeforeClass
|
private static final String PASSWORD = OpenPGPPasswordBasedEncryptorTest.class.getName();
|
||||||
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
|
@Test
|
||||||
public void testShouldEncryptAndDecrypt() throws Exception {
|
public void testEncryptDecrypt() throws Exception {
|
||||||
|
final ByteArrayInputStream plainStream = new ByteArrayInputStream(PLAINTEXT);
|
||||||
|
|
||||||
for (int i = 1; i<14; i++) {
|
final OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(),
|
||||||
if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped
|
CIPHER, EncryptionMethod.PGP.getProvider(), PASSWORD.toCharArray(), FILENAME);
|
||||||
Integer cipher = i;
|
|
||||||
logger.info("Testing PGP encryption with " + PGPUtil.getSymmetricCipherName(cipher) + " cipher.");
|
|
||||||
|
|
||||||
// Arrange
|
final StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
|
||||||
final String PLAINTEXT = "This is a plaintext message.";
|
final StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
|
||||||
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
|
final ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream();
|
||||||
String filename = "tempFile.txt";
|
encryptionCallback.process(plainStream, encryptedStream);
|
||||||
|
|
||||||
OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(),
|
final InputStream encryptedInputStream = new ByteArrayInputStream(encryptedStream.toByteArray());
|
||||||
cipher, EncryptionMethod.PGP.getProvider(), PASSWORD.toCharArray(), filename);
|
final ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream();
|
||||||
|
decryptionCallback.process(encryptedInputStream, decryptedStream);
|
||||||
|
|
||||||
StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
|
byte[] decryptedBytes = decryptedStream.toByteArray();
|
||||||
StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
|
assertArrayEquals(PLAINTEXT, decryptedBytes);
|
||||||
|
|
||||||
// Act
|
|
||||||
encryptionCallback.process(plainStream, cipherStream);
|
|
||||||
|
|
||||||
final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray();
|
|
||||||
logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
|
|
||||||
InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes);
|
|
||||||
|
|
||||||
decryptionCallback.process(cipherInputStream, recoveredStream);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
|
|
||||||
String recovered = new String(recoveredBytes, "UTF-8");
|
|
||||||
logger.info("Recovered: {}", recovered);
|
|
||||||
assert PLAINTEXT.equals(recovered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testShouldDecryptExternalFile() throws Exception {
|
|
||||||
for (int i = 1; i<14; i++) {
|
|
||||||
if (PGPEncryptedData.SAFER != i) { // SAFER cipher is not supported and therefore its test is skipped
|
|
||||||
Integer cipher = i;
|
|
||||||
// Arrange
|
|
||||||
byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
|
|
||||||
final String PLAINTEXT = new String(plainBytes, "UTF-8");
|
|
||||||
|
|
||||||
InputStream cipherStream = new FileInputStream(encryptedFile);
|
|
||||||
OutputStream recoveredStream = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
// No file, just streams
|
|
||||||
String filename = encryptedFile.getName();
|
|
||||||
|
|
||||||
OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), cipher,
|
|
||||||
EncryptionMethod.PGP.getProvider(), LEGACY_PASSWORD.toCharArray(), filename);
|
|
||||||
|
|
||||||
StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
decryptionCallback.process(cipherStream, recoveredStream);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
|
|
||||||
String recovered = new String(recoveredBytes, "UTF-8");
|
|
||||||
logger.info("Recovered: {}", recovered);
|
|
||||||
Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue