blob: 1ebc1f5813864705ef01ae45bf6760a2a4aca7c7 [file] [log] [blame]
/*
* 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.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
import static org.junit.jupiter.api.Assumptions.assumeTrue
class AESKeyedCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProviderGroovyTest.class)
private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final String PLAINTEXT = "ExactBlockSizeRequiredForProcess"
private static final List<EncryptionMethod> keyedEncryptionMethods = EncryptionMethod.values().findAll { it.keyedCipher }
private static final SecretKey key = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
@BeforeAll
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
private static boolean isUnlimitedStrengthCryptoAvailable() {
Cipher.getMaxAllowedKeyLength("AES") > 128
}
@Test
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
// Act
for (EncryptionMethod em : keyedEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, key, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered)
}
}
@Test
void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
logger.info("Using algorithm: ${em.getAlgorithm()}")
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, iv, true)
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, key, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered)
}
}
@Test
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
assumeTrue(isUnlimitedStrengthCryptoAvailable(), "Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.")
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final List<Integer> LONG_KEY_LENGTHS = [192, 256]
SecureRandom secureRandom = new SecureRandom()
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
// Re-use the same IV for the different length keys to ensure the encryption is different
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
LONG_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${em.getAlgorithm()} with key length ${keyLength}")
// Generate a key
byte[] keyBytes = new byte[keyLength / 8]
secureRandom.nextBytes(keyBytes)
SecretKey localKey = new SecretKeySpec(keyBytes, "AES")
logger.info("Key: ${Hex.encodeHexString(keyBytes)} ${keyBytes.length}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, localKey, iv, true)
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, localKey, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered)
}
}
}
@Test
void testShouldRejectEmptyKey() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, null, true)
}
// Assert
assert msg =~ "The key must be specified"
}
@Test
void testShouldRejectIncorrectLengthKey() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
SecretKey localKey = new SecretKeySpec(Hex.decodeHex("0123456789ABCDEF" as char[]), "AES")
assert ![128, 192, 256].contains(localKey.encoded.length)
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, localKey, true)
}
// Assert
assert msg =~ "The key must be of length \\[128, 192, 256\\]"
}
@Test
void testShouldRejectEmptyEncryptionMethod() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(null, key, true)
}
// Assert
assert msg =~ "The encryption method must be specified"
}
@Test
void testShouldRejectUnsupportedEncryptionMethod() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, key, true)
}
// Assert
assert msg =~ " requires a PBECipherProvider"
}
@Test
void testGetCipherShouldSupportExternalCompatibility() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final String plaintext = "This is a plaintext message."
// These values can be generated by running `$ ./openssl_aes.rb` in the terminal
final byte[] IV = Hex.decodeHex("e0bc8cc7fbc0bdfdc184dc22ce2fcb5b" as char[])
final byte[] LOCAL_KEY = Hex.decodeHex("c72943d27c3e5a276169c5998a779117" as char[])
final String CIPHER_TEXT = "a2725ea55c7dd717664d044cab0f0b5f763653e322c27df21954f5be394efb1b"
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
SecretKey localKey = new SecretKeySpec(LOCAL_KEY, "AES")
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, localKey, IV, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered)
}
@Test
void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
logger.info("Using algorithm: ${em.getAlgorithm()}")
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, iv, true)
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, key, false)
}
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
void testGetCipherShouldRejectInvalidIVLengths() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final def INVALID_IVS = (0..15).collect { int length -> new byte[length] }
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
INVALID_IVS.each { byte[] badIV ->
logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}")
// Encrypt should print a warning about the bad IV but overwrite it
Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, true)
// Decrypt should fail
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false)
}
logger.expected(msg)
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
void testGetCipherShouldRejectEmptyIV() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
byte[] badIV = [0x00 as byte] * 16 as byte[]
// Act
logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}")
// Encrypt should print a warning about the bad IV but overwrite it
Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, true)
logger.info("IV after encrypt: ${Hex.encodeHexString(cipher.getIV())}")
// Decrypt should fail
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false)
}
logger.expected(msg)
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}