blob: 37909e9e02c0f13c82859f2bee7866853cb9be6e [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.kms;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.security.repository.config.RepositoryEncryptionConfiguration;
import org.apache.nifi.util.NiFiBootstrapUtils;
import org.bouncycastle.util.encoders.DecoderException;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class CryptoUtils {
private static final Logger logger = LoggerFactory.getLogger(CryptoUtils.class);
public static final String STATIC_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.security.kms.StaticKeyProvider";
public static final String FILE_BASED_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.security.kms.FileBasedKeyProvider";
public static final String KEY_STORE_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.security.kms.KeyStoreKeyProvider";
// TODO: Move to RepositoryEncryptionUtils in NIFI-6617
public static final String LEGACY_SKP_FQCN = "org.apache.nifi.provenance.StaticKeyProvider";
public static final String LEGACY_FBKP_FQCN = "org.apache.nifi.provenance.FileBasedKeyProvider";
// TODO: Enforce even length
private static final Pattern HEX_PATTERN = Pattern.compile("(?i)^[0-9a-f]+$");
private static final List<Integer> UNLIMITED_KEY_LENGTHS = Arrays.asList(32, 48, 64);
public static final String ENCRYPTED_FSR_CLASS_NAME = "org.apache.nifi.controller.repository.crypto.EncryptedFileSystemRepository";
public static final String EWAFFR_CLASS_NAME = "org.apache.nifi.controller.repository.crypto.EncryptedWriteAheadFlowFileRepository";
public static boolean isUnlimitedStrengthCryptoAvailable() {
try {
return Cipher.getMaxAllowedKeyLength("AES") > 128;
} catch (NoSuchAlgorithmException e) {
logger.warn("Tried to determine if unlimited strength crypto is available but the AES algorithm is not available");
return false;
}
}
/**
* Utility method which returns true if the string is null, empty, or entirely whitespace.
*
* @param src the string to evaluate
* @return true if empty
*/
public static boolean isEmpty(String src) {
return src == null || src.trim().isEmpty();
}
/**
* Concatenates multiple byte[] into a single byte[].
*
* @param arrays the component byte[] in order
* @return a concatenated byte[]
*/
public static byte[] concatByteArrays(byte[]... arrays) {
int totalByteLength = 0;
for (byte[] bytes : arrays) {
totalByteLength += bytes.length;
}
byte[] totalBytes = new byte[totalByteLength];
int currentLength = 0;
for (byte[] bytes : arrays) {
System.arraycopy(bytes, 0, totalBytes, currentLength, bytes.length);
currentLength += bytes.length;
}
return totalBytes;
}
/**
* Returns true if the provided configuration values are valid (shallow evaluation only; does not validate the keys
* contained in a {@link FileBasedKeyProvider}).
*
* @param rec the configuration to validate
* @return true if the config is valid
*/
public static boolean isValidRepositoryEncryptionConfiguration(RepositoryEncryptionConfiguration rec) {
return isValidKeyProvider(rec.getKeyProviderImplementation(), rec.getKeyProviderLocation(), rec.getEncryptionKeyId(), rec.getEncryptionKeys());
}
/**
* Returns true if the provided configuration values successfully define the specified {@link KeyProvider}.
*
* @param keyProviderImplementation the FQ class name of the {@link KeyProvider} implementation
* @param keyProviderLocation the location of the definition (for {@link FileBasedKeyProvider}, etc.)
* @param keyId the active key ID
* @param encryptionKeys a map of key IDs to key material in hex format
* @return true if the provided configuration is valid
*/
public static boolean isValidKeyProvider(String keyProviderImplementation, final String keyProviderLocation, final String keyId, final Map<String, String> encryptionKeys) {
try {
keyProviderImplementation = handleLegacyPackages(keyProviderImplementation);
} catch (final KeyManagementException e) {
logger.warn("Key Provider [{}] Validation Failed: {}", keyProviderImplementation, e.getMessage());
return false;
}
switch (keyProviderImplementation) {
case STATIC_KEY_PROVIDER_CLASS_NAME:
if (encryptionKeys == null) {
return false;
} else {
boolean everyKeyValid = encryptionKeys.values().stream().allMatch(CryptoUtils::keyIsValid);
return everyKeyValid && StringUtils.isNotEmpty(keyId);
}
case FILE_BASED_KEY_PROVIDER_CLASS_NAME:
case KEY_STORE_KEY_PROVIDER_CLASS_NAME:
final Path keyProviderPath = Paths.get(keyProviderLocation);
return Files.isReadable(keyProviderPath) && StringUtils.isNotEmpty(keyId);
default:
logger.warn("Validation Failed: Key Provider [{}] Location [{}] Key ID [{}]", keyProviderImplementation, keyProviderLocation, keyId);
return false;
}
}
static String handleLegacyPackages(String implementationClassName) throws KeyManagementException {
if (org.apache.nifi.util.StringUtils.isBlank(implementationClassName)) {
throw new KeyManagementException("Invalid key provider implementation provided: " + implementationClassName);
}
if (implementationClassName.equalsIgnoreCase(LEGACY_SKP_FQCN)) {
return StaticKeyProvider.class.getName();
} else if (implementationClassName.equalsIgnoreCase(LEGACY_FBKP_FQCN)) {
return FileBasedKeyProvider.class.getName();
} else {
return implementationClassName;
}
}
/**
* Returns true if the provided key is valid hex and is the correct length for the current system's JCE policies.
*
* @param encryptionKeyHex the key in hexadecimal
* @return true if this key is valid
*/
public static boolean keyIsValid(String encryptionKeyHex) {
return isHexString(encryptionKeyHex)
&& (isUnlimitedStrengthCryptoAvailable()
? UNLIMITED_KEY_LENGTHS.contains(encryptionKeyHex.length())
: encryptionKeyHex.length() == 32);
}
/**
* Returns true if the input is valid hexadecimal (does not enforce length and is case-insensitive).
*
* @param hexString the string to evaluate
* @return true if the string is valid hex
*/
public static boolean isHexString(String hexString) {
return StringUtils.isNotEmpty(hexString) && HEX_PATTERN.matcher(hexString).matches();
}
/**
* Returns the root key from the {@code bootstrap.conf} file used to encrypt various sensitive properties and data encryption keys.
*
* @return the root key
* @throws KeyManagementException if the key cannot be read
*/
public static SecretKey getRootKey() throws KeyManagementException {
try {
// Get the root encryption key from bootstrap.conf
String rootKeyHex = NiFiBootstrapUtils.extractKeyFromBootstrapFile();
return new SecretKeySpec(Hex.decode(rootKeyHex), "AES");
} catch (IOException | DecoderException e) {
logger.error("Encountered an error: ", e);
throw new KeyManagementException(e);
}
}
/**
* Returns true if the two parameters are equal. This method is null-safe and evaluates the
* equality in constant-time rather than "short-circuiting" on the first inequality. This
* prevents timing attacks (side channel attacks) when comparing passwords or hash values.
*
* @param a a String to compare
* @param b a String to compare
* @return true if the values are equal
*/
public static boolean constantTimeEquals(String a, String b) {
if (a == null) {
return b == null;
} else {
// This returns true IFF b != null and the byte[] are equal; if b == null, a is not, and they are not equal
return b != null && constantTimeEquals(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
}
/**
* Returns true if the two parameters are equal. This method is null-safe and evaluates the
* equality in constant-time rather than "short-circuiting" on the first inequality. This
* prevents timing attacks (side channel attacks) when comparing passwords or hash values.
* Does not convert the character arrays to {@code String}s when converting to {@code byte[]}
* to avoid putting sensitive data in the String pool.
*
* @param a a char[] to compare
* @param b a char[] to compare
* @return true if the values are equal
*/
public static boolean constantTimeEquals(char[] a, char[] b) {
return constantTimeEquals(convertCharsToBytes(a), convertCharsToBytes(b));
}
/**
* Returns true if the two parameters are equal. This method is null-safe and evaluates the
* equality in constant-time rather than "short-circuiting" on the first inequality. This
* prevents timing attacks (side channel attacks) when comparing passwords or hash values.
*
* @param a a byte[] to compare
* @param b a byte[] to compare
* @return true if the values are equal
*/
public static boolean constantTimeEquals(byte[] a, byte[] b) {
return MessageDigest.isEqual(a, b);
}
/**
* Returns a {@code byte[]} containing the value of the provided {@code char[]} without using {@code new String(chars).getBytes()} which would put sensitive data (the password) in the String pool.
*
* @param chars the characters to convert
* @return the byte[]
*/
private static byte[] convertCharsToBytes(char[] chars) {
CharBuffer charBuffer = CharBuffer.wrap(chars);
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
return Arrays.copyOfRange(byteBuffer.array(),
byteBuffer.position(), byteBuffer.limit());
}
}