/*
* 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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.stream.io.ByteArrayOutputStream;
import org.apache.nifi.stream.io.StreamUtils;
public class CipherUtility {
public static final int BUFFER_SIZE = 65536;
private static final Pattern KEY_LENGTH_PATTERN = Pattern.compile("([\\d]+)BIT");
private static final Map<String, Integer> MAX_PASSWORD_LENGTH_BY_ALGORITHM;
static {
Map<String, Integer> aMap = new HashMap<>();
/**
* These values were determined empirically by running {@link NiFiLegacyCipherProviderGroovyTest#testShouldDetermineDependenceOnUnlimitedStrengthCrypto()}
*, which evaluates each algorithm in a try/catch harness with increasing password size until it throws an exception.
* This was performed on a JVM without the Unlimited Strength Jurisdiction cryptographic policy files installed.
*/
aMap.put("PBEWITHMD5AND128BITAES-CBC-OPENSSL", 16);
aMap.put("PBEWITHMD5AND192BITAES-CBC-OPENSSL", 16);
aMap.put("PBEWITHMD5AND256BITAES-CBC-OPENSSL", 16);
aMap.put("PBEWITHMD5ANDDES", 16);
aMap.put("PBEWITHMD5ANDRC2", 16);
aMap.put("PBEWITHSHA1ANDRC2", 16);
aMap.put("PBEWITHSHA1ANDDES", 16);
aMap.put("PBEWITHSHAAND128BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND192BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND256BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND40BITRC2-CBC", 7);
aMap.put("PBEWITHSHAAND128BITRC2-CBC", 7);
aMap.put("PBEWITHSHAAND40BITRC4", 7);
aMap.put("PBEWITHSHAAND128BITRC4", 7);
aMap.put("PBEWITHSHA256AND128BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHA256AND192BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHA256AND256BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", 7);
aMap.put("PBEWITHSHAAND3-KEYTRIPLEDES-CBC", 7);
aMap.put("PBEWITHSHAANDTWOFISH-CBC", 7);
MAX_PASSWORD_LENGTH_BY_ALGORITHM = Collections.unmodifiableMap(aMap);
}
/**
* Returns the cipher algorithm from the full algorithm name. Useful for getting key lengths, etc.
* <p/>
* Ex: PBEWITHMD5AND128BITAES-CBC-OPENSSL -> AES
*
* @param algorithm the full algorithm name
* @return the generic cipher name or the full algorithm if one cannot be extracted
*/
public static String parseCipherFromAlgorithm(final String algorithm) {
if (StringUtils.isEmpty(algorithm)) {
return algorithm;
}
String formattedAlgorithm = algorithm.toUpperCase();
// This is not optimal but the algorithms do not have a standard format
final String AES = "AES";
final String TDES = "TRIPLEDES";
final String TDES_ALTERNATE = "DESEDE";
final String DES = "DES";
final String RC4 = "RC4";
final String RC2 = "RC2";
final String TWOFISH = "TWOFISH";
final List<String> SYMMETRIC_CIPHERS = Arrays.asList(AES, TDES, TDES_ALTERNATE, DES, RC4, RC2, TWOFISH);
// The algorithms contain "TRIPLEDES" but the cipher name is "DESede"
final String ACTUAL_TDES_CIPHER = "DESede";
for (String cipher : SYMMETRIC_CIPHERS) {
if (formattedAlgorithm.contains(cipher)) {
if (cipher.equals(TDES) || cipher.equals(TDES_ALTERNATE)) {
return ACTUAL_TDES_CIPHER;
} else {
return cipher;
}
}
}
return algorithm;
}
/**
* Returns the cipher key length from the full algorithm name. Useful for getting key lengths, etc.
* <p/>
* Ex: PBEWITHMD5AND128BITAES-CBC-OPENSSL -> 128
*
* @param algorithm the full algorithm name
* @return the key length or -1 if one cannot be extracted
*/
public static int parseKeyLengthFromAlgorithm(final String algorithm) {
int keyLength = parseActualKeyLengthFromAlgorithm(algorithm);
if (keyLength != -1) {
return keyLength;
} else {
// Key length not explicitly named in algorithm
String cipher = parseCipherFromAlgorithm(algorithm);
return getDefaultKeyLengthForCipher(cipher);
}
}
private static int parseActualKeyLengthFromAlgorithm(final String algorithm) {
Matcher matcher = KEY_LENGTH_PATTERN.matcher(algorithm);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
} else {
return -1;
}
}
/**
* Returns true if the provided key length is a valid key length for the provided cipher family. Does not reflect if the Unlimited Strength Cryptography Jurisdiction Policies are installed.
* Does not reflect if the key length is correct for a specific combination of cipher and PBE-derived key length.
* <p/>
* Ex:
* <p/>
* 256 is valid for {@code AES/CBC/PKCS7Padding} but not {@code PBEWITHMD5AND128BITAES-CBC-OPENSSL}. However, this method will return {@code true} for both because it only gets the cipher
* family, {@code AES}.
* <p/>
* 64, AES -> false
* [128, 192, 256], AES -> true
*
* @param keyLength the key length in bits
* @param cipher the cipher family
* @return true if this key length is valid
*/
public static boolean isValidKeyLength(int keyLength, final String cipher) {
if (StringUtils.isEmpty(cipher)) {
return false;
}
return getValidKeyLengthsForAlgorithm(cipher).contains(keyLength);
}
/**
* Returns true if the provided key length is a valid key length for the provided algorithm. Does not reflect if the Unlimited Strength Cryptography Jurisdiction Policies are installed.
* <p/>
* Ex:
* <p/>
* 256 is valid for {@code AES/CBC/PKCS7Padding} but not {@code PBEWITHMD5AND128BITAES-CBC-OPENSSL}.
* <p/>
* 64, AES/CBC/PKCS7Padding -> false
* [128, 192, 256], AES/CBC/PKCS7Padding -> true
* <p/>
* 128, PBEWITHMD5AND128BITAES-CBC-OPENSSL -> true
* [192, 256], PBEWITHMD5AND128BITAES-CBC-OPENSSL -> false
*
* @param keyLength the key length in bits
* @param algorithm the specific algorithm
* @return true if this key length is valid
*/
public static boolean isValidKeyLengthForAlgorithm(int keyLength, final String algorithm) {
if (StringUtils.isEmpty(algorithm)) {
return false;
}
return getValidKeyLengthsForAlgorithm(algorithm).contains(keyLength);
}
public static List<Integer> getValidKeyLengthsForAlgorithm(String algorithm) {
List<Integer> validKeyLengths = new ArrayList<>();
if (StringUtils.isEmpty(algorithm)) {
return validKeyLengths;
}
// Some algorithms specify a single key size
int keyLength = parseActualKeyLengthFromAlgorithm(algorithm);
if (keyLength != -1) {
validKeyLengths.add(keyLength);
return validKeyLengths;
}
// The algorithm does not specify a key size
String cipher = parseCipherFromAlgorithm(algorithm);
switch (cipher.toUpperCase()) {
case "DESEDE":
// 3DES keys have the cryptographic strength of 7/8 because of parity bits, but are often represented with n*8 bytes
return Arrays.asList(56, 64, 112, 128, 168, 192);
case "DES":
return Arrays.asList(56, 64);
case "RC2":
case "RC4":
case "RC5":
/** These ciphers can have arbitrary length keys but that's a really bad idea, {@see http://crypto.stackexchange.com/a/9963/12569}.
* Also, RC* is deprecated and should be considered insecure */
for (int i = 40; i <= 2048; i++) {
validKeyLengths.add(i);
}
return validKeyLengths;
case "AES":
case "TWOFISH":
return Arrays.asList(128, 192, 256);
default:
return validKeyLengths;
}
}
private static int getDefaultKeyLengthForCipher(String cipher) {
if (StringUtils.isEmpty(cipher)) {
return -1;
}
cipher = cipher.toUpperCase();
switch (cipher) {
case "DESEDE":
return 112;
case "DES":
return 64;
case "RC2":
case "RC4":
case "RC5":
default:
return 128;
}
}
public static void processStreams(Cipher cipher, InputStream in, OutputStream out) {
try {
final byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = in.read(buffer)) > 0) {
final byte[] decryptedBytes = cipher.update(buffer, 0, len);
if (decryptedBytes != null) {
out.write(decryptedBytes);
}
}
out.write(cipher.doFinal());
} catch (Exception e) {
throw new ProcessException(e);
}
}
public static byte[] readBytesFromInputStream(InputStream in, String label, int limit, byte[] delimiter) throws IOException, ProcessException {
if (in == null) {
throw new IllegalArgumentException("Cannot read " + label + " from null InputStream");
}
// If the value is not detected within the first n bytes, throw an exception
in.mark(limit);
// The first n bytes of the input stream contain the value up to the custom delimiter
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
byte[] stoppedBy = StreamUtils.copyExclusive(in, bytesOut, limit + delimiter.length, delimiter);
if (stoppedBy != null) {
byte[] bytes = bytesOut.toByteArray();
return bytes;
}
// If no delimiter was found, reset the cursor
in.reset();
return null;
}
public static void writeBytesToOutputStream(OutputStream out, byte[] value, String label, byte[] delimiter) throws IOException {
if (out == null) {
throw new IllegalArgumentException("Cannot write " + label + " to null OutputStream");
}
out.write(value);
out.write(delimiter);
}
public static String encodeBase64NoPadding(final byte[] bytes) {
String base64UrlNoPadding = Base64.encodeBase64URLSafeString(bytes);
base64UrlNoPadding = base64UrlNoPadding.replaceAll("-", "+");
base64UrlNoPadding = base64UrlNoPadding.replaceAll("_", "/");
return base64UrlNoPadding;
}
public static boolean passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(final int passwordLength, EncryptionMethod encryptionMethod) {
if (encryptionMethod == null) {
throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
}
return passwordLength <= getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod);
}
public static int getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(EncryptionMethod encryptionMethod) {
if (encryptionMethod == null) {
throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
}
if (MAX_PASSWORD_LENGTH_BY_ALGORITHM.containsKey(encryptionMethod.getAlgorithm())) {
return MAX_PASSWORD_LENGTH_BY_ALGORITHM.get(encryptionMethod.getAlgorithm());
} else {
return -1;
}
}
public static byte[] concatBytes(byte[]... arrays) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (byte[] bytes : arrays) {
outputStream.write(bytes);
}
return outputStream.toByteArray();
}
}