blob: ed071ac9d1e1ace7a7d0b0df5a878dee3aa3a788 [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.registry.properties
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.util.encoders.DecoderException
import org.bouncycastle.util.encoders.Hex
import org.junit.*
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.security.Security
@RunWith(JUnit4.class)
class AESSensitivePropertyProviderTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderTest.class)
private static final String KEY_128_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final String KEY_256_HEX = KEY_128_HEX * 2
private static final int IV_LENGTH = AESSensitivePropertyProvider.getIvLength()
private static final List<Integer> KEY_SIZES = getAvailableKeySizes()
private static final SecureRandom secureRandom = new SecureRandom()
private static final Base64.Encoder encoder = Base64.encoder
private static final Base64.Decoder decoder = Base64.decoder
@BeforeClass
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
private static Cipher getCipher(boolean encrypt = true, int keySize = 256, byte[] iv = [0x00] * IV_LENGTH) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding")
String key = getKeyOfSize(keySize)
cipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, new SecretKeySpec(Hex.decode(key), "AES"), new IvParameterSpec(iv))
logger.setup("Initialized a cipher in ${encrypt ? "encrypt" : "decrypt"} mode with a key of length ${keySize} bits")
cipher
}
private static String getKeyOfSize(int keySize = 256) {
switch (keySize) {
case 128:
return KEY_128_HEX
case 192:
case 256:
if (Cipher.getMaxAllowedKeyLength("AES") < keySize) {
throw new IllegalArgumentException("The JCE unlimited strength cryptographic jurisdiction policies are not installed, so the max key size is 128 bits")
}
return KEY_256_HEX[0..<(keySize / 4)]
default:
throw new IllegalArgumentException("Key size ${keySize} bits is not valid")
}
}
private static List<Integer> getAvailableKeySizes() {
if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
[128, 192, 256]
} else {
[128]
}
}
private static String manipulateString(String input, int start = 0, int end = input?.length()) {
if ((input[start..end] as List).unique().size() == 1) {
throw new IllegalArgumentException("Can't manipulate a String where the entire range is identical [${input[start..end]}]")
}
List shuffled = input[start..end] as List
Collections.shuffle(shuffled)
String reconstituted = input[0..<start] + shuffled.join() + input[end + 1..-1]
return reconstituted != input ? reconstituted : manipulateString(input, start, end)
}
@Test
void testShouldProtectValue() throws Exception {
final String PLAINTEXT = "This is a plaintext value"
// Act
Map<Integer, String> CIPHER_TEXTS = KEY_SIZES.collectEntries { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
[(keySize): spp.protect(PLAINTEXT)]
}
CIPHER_TEXTS.each { ks, ct -> logger.info("Encrypted for ${ks} length key: ${ct}") }
// Assert
// The IV generation is part of #protect, so the expected cipher text values must be generated after #protect has run
Map<Integer, Cipher> decryptionCiphers = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
// The 12 byte IV is the first 16 Base64-encoded characters of the "complete" cipher text
byte[] iv = decoder.decode(cipherText[0..<16])
[(keySize): getCipher(false, keySize, iv)]
}
Map<Integer, String> plaintexts = decryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
String cipherTextWithoutIVAndDelimiter = CIPHER_TEXTS[e.key][18..-1]
String plaintext = new String(e.value.doFinal(decoder.decode(cipherTextWithoutIVAndDelimiter)), StandardCharsets.UTF_8)
[(e.key): plaintext]
}
CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
}
@Test
void testShouldHandleProtectEmptyValue() throws Exception {
final List<String> EMPTY_PLAINTEXTS = ["", " ", null]
// Act
KEY_SIZES.collectEntries { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
EMPTY_PLAINTEXTS.each { String emptyPlaintext ->
def msg = shouldFail(IllegalArgumentException) {
spp.protect(emptyPlaintext)
}
logger.expected("${msg} for keySize ${keySize} and plaintext [${emptyPlaintext}]")
// Assert
assert msg == "Cannot encrypt an empty value"
}
}
}
@Test
void testShouldUnprotectValue() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext value"
Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize ->
byte[] iv = new byte[IV_LENGTH]
secureRandom.nextBytes(iv)
[(keySize): getCipher(true, keySize, iv)]
}
Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
String iv = encoder.encodeToString(e.value.getIV())
String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
[(e.key): "${iv}||${cipherText}"]
}
CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
// Act
Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
[(keySize): spp.unprotect(cipherText)]
}
plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") }
// Assert
assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
}
/**
* Tests inputs where the entire String is empty/blank space/{@code null}.
*
* @throws Exception
*/
@Test
void testShouldHandleUnprotectEmptyValue() throws Exception {
// Arrange
final List<String> EMPTY_CIPHER_TEXTS = ["", " ", null]
// Act
KEY_SIZES.each { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
def msg = shouldFail(IllegalArgumentException) {
spp.unprotect(emptyCipherText)
}
logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]")
// Assert
assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
}
}
}
@Test
void testShouldUnprotectValueWithWhitespace() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext value"
Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize ->
byte[] iv = new byte[IV_LENGTH]
secureRandom.nextBytes(iv)
[(keySize): getCipher(true, keySize, iv)]
}
Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
String iv = encoder.encodeToString(e.value.getIV())
String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
[(e.key): "${iv}||${cipherText}"]
}
CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
// Act
Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
[(keySize): spp.unprotect("\t" + cipherText + "\n")]
}
plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") }
// Assert
assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
}
@Test
void testShouldHandleUnprotectMalformedValue() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext value"
// Act
KEY_SIZES.each { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
String cipherText = spp.protect(PLAINTEXT)
// Swap two characters in the cipher text
final String MALFORMED_CIPHER_TEXT = manipulateString(cipherText, 25, 28)
logger.info("Manipulated ${cipherText} to\n${MALFORMED_CIPHER_TEXT.padLeft(163)}")
def msg = shouldFail(SensitivePropertyProtectionException) {
spp.unprotect(MALFORMED_CIPHER_TEXT)
}
logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_CIPHER_TEXT}]")
// Assert
assert msg == "Error decrypting a protected value"
}
}
@Test
void testShouldHandleUnprotectMissingIV() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext value"
// Act
KEY_SIZES.each { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
String cipherText = spp.protect(PLAINTEXT)
// Remove the IV from the "complete" cipher text
final String MISSING_IV_CIPHER_TEXT = cipherText[18..-1]
logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT.padLeft(172)}")
def msg = shouldFail(IllegalArgumentException) {
spp.unprotect(MISSING_IV_CIPHER_TEXT)
}
logger.expected("${msg} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT}]")
// Remove the IV from the "complete" cipher text but keep the delimiter
final String MISSING_IV_CIPHER_TEXT_WITH_DELIMITER = cipherText[16..-1]
logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER.padLeft(172)}")
def msgWithDelimiter = shouldFail(IllegalArgumentException) {
spp.unprotect(MISSING_IV_CIPHER_TEXT_WITH_DELIMITER)
}
logger.expected("${msgWithDelimiter} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER}]")
// Assert
assert msg == "The cipher text does not contain the delimiter || -- it should be of the form Base64(IV) || Base64(cipherText)"
// Assert
assert msgWithDelimiter == "The IV (0 bytes) must be at least 12 bytes"
}
}
/**
* Tests inputs which have a valid IV and delimiter but no "cipher text".
*
* @throws Exception
*/
@Test
void testShouldHandleUnprotectEmptyCipherText() throws Exception {
// Arrange
final String IV_AND_DELIMITER = "${encoder.encodeToString("Bad IV value".getBytes(StandardCharsets.UTF_8))}||"
logger.info("IV and delimiter: ${IV_AND_DELIMITER}")
final List<String> EMPTY_CIPHER_TEXTS = ["", " ", "\n"].collect { "${IV_AND_DELIMITER}${it}" }
// Act
KEY_SIZES.each { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
def msg = shouldFail(IllegalArgumentException) {
spp.unprotect(emptyCipherText)
}
logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]")
// Assert
assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
}
}
}
@Test
void testShouldHandleUnprotectMalformedIV() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext value"
// Act
KEY_SIZES.each { int keySize ->
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
logger.info("Initialized ${spp.name} with key size ${keySize}")
String cipherText = spp.protect(PLAINTEXT)
// Swap two characters in the IV
final String MALFORMED_IV_CIPHER_TEXT = manipulateString(cipherText, 8, 11)
logger.info("Manipulated ${cipherText} to\n${MALFORMED_IV_CIPHER_TEXT.padLeft(163)}")
def msg = shouldFail(SensitivePropertyProtectionException) {
spp.unprotect(MALFORMED_IV_CIPHER_TEXT)
}
logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_IV_CIPHER_TEXT}]")
// Assert
assert msg == "Error decrypting a protected value"
}
}
@Test
void testShouldGetIdentifierKeyWithDifferentMaxKeyLengths() throws Exception {
// Arrange
def keys = getAvailableKeySizes().collectEntries { int keySize ->
[(keySize): getKeyOfSize(keySize)]
}
logger.info("Keys: ${keys}")
// Act
keys.each { int size, String key ->
String identifierKey = new AESSensitivePropertyProvider(key).getIdentifierKey()
logger.info("Identifier key: ${identifierKey} for size ${size}")
// Assert
assert identifierKey =~ /aes\/gcm\/${size}/
}
}
@Test
void testShouldNotAllowEmptyKey() throws Exception {
// Arrange
final String INVALID_KEY = ""
// Act
def msg = shouldFail(SensitivePropertyProtectionException) {
AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
}
// Assert
assert msg == "The key cannot be empty"
}
@Test
void testShouldNotAllowIncorrectlySizedKey() throws Exception {
// Arrange
final String INVALID_KEY = "Z" * 31
// Act
def msg = shouldFail(SensitivePropertyProtectionException) {
AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
}
// Assert
assert msg == "The key must be a valid hexadecimal key"
}
@Test
void testShouldNotAllowInvalidKey() throws Exception {
// Arrange
final String INVALID_KEY = "Z" * 32
// Act
def msg = shouldFail(SensitivePropertyProtectionException) {
AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
}
// Assert
assert msg == "The key must be a valid hexadecimal key"
}
/**
* This test is to ensure internal consistency and allow for encrypting value for various property files
*/
@Test
void testShouldEncryptArbitraryValues() {
// Arrange
def values = ["thisIsABadPassword", "thisIsABadSensitiveKeyPassword", "thisIsABadKeystorePassword", "thisIsABadKeyPassword", "thisIsABadTruststorePassword", "This is an encrypted banner message", "nififtw!"]
String key = "2C576A9585DB862F5ECBEE5B4FFFCCA1" //getKeyOfSize(128)
// key = "0" * 64
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
// Act
def encryptedValues = values.collect { String v ->
def encryptedValue = spp.protect(v)
logger.info("${v} -> ${encryptedValue}")
def (String iv, String cipherText) = encryptedValue.tokenize("||")
logger.info("Normal Base64 encoding would be ${encoder.encodeToString(decoder.decode(iv))}||${encoder.encodeToString(decoder.decode(cipherText))}")
encryptedValue
}
// Assert
assert values == encryptedValues.collect { spp.unprotect(it) }
}
/**
* This test is to ensure external compatibility in case someone encodes the encrypted value with Base64 and does not remove the padding
*/
@Test
void testShouldDecryptPaddedValueWith256BitKey() {
// Arrange
Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
final String EXPECTED_VALUE = getKeyOfSize(256) // "thisIsABadKeyPassword"
String cipherText = "aYDkDKys1ENr3gp+||sTBPpMlIvHcOLTGZlfWct8r9RY8BuDlDkoaYmGJ/9m9af9tZIVzcnDwvYQAaIKxRGF7vI2yrY7Xd6x9GTDnWGiGiRXlaP458BBMMgfzH2O8"
String unpaddedCipherText = cipherText.replaceAll("=", "")
String key = "AAAABBBBCCCCDDDDEEEEFFFF00001111" * 2 // getKeyOfSize(256)
SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
// Act
String rawValue = spp.unprotect(cipherText)
logger.info("Decrypted ${cipherText} to ${rawValue}")
String rawUnpaddedValue = spp.unprotect(unpaddedCipherText)
logger.info("Decrypted ${unpaddedCipherText} to ${rawUnpaddedValue}")
// Assert
assert rawValue == EXPECTED_VALUE
assert rawUnpaddedValue == EXPECTED_VALUE
}
}