blob: 47d6fa228726002a71d3e7dc85b25c2b64195f59 [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.junit.jupiter.api.condition.EnabledIfSystemProperty
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import java.security.Security
import static org.junit.Assert.fail
import static org.junit.jupiter.api.Assumptions.assumeTrue
class NiFiLegacyCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProviderGroovyTest.class)
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>()
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>()
private static final String PROVIDER_NAME = "BC"
private static final int ITERATION_COUNT = 1000
private static final byte[] SALT_16_BYTES = Hex.decodeHex("aabbccddeeff00112233445566778899".toCharArray())
@BeforeAll
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
}
private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
try {
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray())
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME)
SecretKey tempKey = factory.generateSecret(pbeKeySpec)
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT)
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME)
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec)
return cipher
} catch (Exception e) {
logger.error("Error generating legacy cipher", e)
fail(e.getMessage())
}
return null
}
@Test
void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "shortPassword"
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod encryptionMethod : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), encryptionMethod)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
byte[] salt = cipherProvider.generateSalt(encryptionMethod)
logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "shortPassword"
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod encryptionMethod : pbeEncryptionMethods) {
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm())
byte[] salt = cipherProvider.generateSalt(encryptionMethod)
logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
void testGetCipherShouldSupportLegacyCode() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "short"
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod encryptionMethod : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", encryptionMethod.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), encryptionMethod)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
byte[] salt = cipherProvider.generateSalt(encryptionMethod)
logger.info("Generated salt ${Hex.encodeHexString(salt)} (${salt.length})")
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, salt, encryptionMethod.getAlgorithm())
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt, false)
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "short"
final byte[] SALT = new byte[0]
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm())
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm())
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false)
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
void testGetCipherShouldIgnoreKeyLength() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
final String PASSWORD = "shortPassword"
final byte[] SALT = SALT_16_BYTES
final String plaintext = "This is a plaintext message."
final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
// Initialize a cipher for encryption
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true)
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length)
// Act
KEY_LENGTHS.each { int keyLength ->
logger.info("Decrypting with 'requested' key length: ${keyLength}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
// Assert
assert plaintext.equals(recovered)
}
}
/**
* This test determines for each PBE encryption algorithm if it actually requires the JCE unlimited strength jurisdiction policies to be installed.
* Even some algorithms that use 128-bit keys (which should be allowed on all systems) throw exceptions because BouncyCastle derives the key
* from the password using a long digest result at the time of key length checking.
* @throws IOException
*/
@EnabledIfSystemProperty(named = "legacyCipherTest", matches = "true", disabledReason = "Only needed once to determine max supported password lengths")
@Test
void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() throws IOException {
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
boolean unlimitedCryptoSupported = CipherUtility.isUnlimitedStrengthCryptoSupported()
logger.info("This JVM supports unlimited strength crypto: ${unlimitedCryptoSupported}")
def longestSupportedPasswordByEM = [:]
encryptionMethods.each { EncryptionMethod encryptionMethod ->
logger.info("Attempting ${encryptionMethod.name()} (${encryptionMethod.algorithm}) which claims unlimited strength required: ${encryptionMethod.unlimitedStrength}")
(1..20).find { int length ->
String password = "x" * length
try {
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider()
Cipher cipher = cipherProvider.getCipher(encryptionMethod, password, true)
return false
} catch (Exception e) {
logger.error("Unable to create the cipher with ${encryptionMethod.algorithm} and password ${password} (${password.length()}) due to ${e.getMessage()}")
if (!longestSupportedPasswordByEM.containsKey(encryptionMethod)) {
longestSupportedPasswordByEM.put(encryptionMethod, password.length() - 1)
}
return true
}
}
logger.info("\n")
}
logger.info("Longest supported password by encryption method:")
longestSupportedPasswordByEM.each { EncryptionMethod encryptionMethod, int length ->
logger.info("\t${encryptionMethod.algorithm}\t${length}")
}
}
}