http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java deleted file mode 100644 index 0364f1c..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPKeyBasedEncryptor.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * 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.processors.standard.util; - -import org.apache.nifi.processor.exception.ProcessException; -import org.apache.nifi.processor.io.StreamCallback; -import org.apache.nifi.processors.standard.EncryptContent; -import org.apache.nifi.processors.standard.EncryptContent.Encryptor; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPCompressedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.util.Date; -import java.util.Iterator; -import java.util.zip.Deflater; - -import static org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE; -import static org.apache.nifi.processors.standard.util.PGPUtil.BUFFER_SIZE; - -public class OpenPGPKeyBasedEncryptor implements Encryptor { - private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class); - - private String algorithm; - private String provider; - // TODO: This can hold either the secret or public keyring path - private String keyring; - private String userId; - private char[] passphrase; - private String filename; - - public OpenPGPKeyBasedEncryptor(final String algorithm, final String provider, final String keyring, final String userId, final char[] passphrase, final String filename) { - this.algorithm = algorithm; - this.provider = provider; - this.keyring = keyring; - this.userId = userId; - this.passphrase = passphrase; - this.filename = filename; - } - - @Override - public StreamCallback getEncryptionCallback() throws Exception { - return new OpenPGPEncryptCallback(algorithm, provider, keyring, userId, filename); - } - - @Override - public StreamCallback getDecryptionCallback() throws Exception { - return new OpenPGPDecryptCallback(provider, keyring, passphrase); - } - - /** - * Returns true if the passphrase is valid. - * <p> - * This is used in the EncryptContent custom validation to check if the passphrase can extract a private key from the secret key ring. After BC was upgraded from 1.46 to 1.53, the API changed - * so this is performed differently but the functionality is equivalent. - * - * @param provider the provider name - * @param secretKeyringFile the file path to the keyring - * @param passphrase the passphrase - * @return true if the passphrase can successfully extract any private key - * @throws IOException if there is a problem reading the keyring file - * @throws PGPException if there is a problem parsing/extracting the private key - * @throws NoSuchProviderException if the provider is not available - */ - public static boolean validateKeyring(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException, NoSuchProviderException { - try { - getDecryptedPrivateKey(provider, secretKeyringFile, passphrase); - return true; - } catch (Exception e) { - // If this point is reached, no private key could be extracted with the given passphrase - return false; - } - } - - private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException { - // TODO: Verify that key IDs cannot be 0 - return getDecryptedPrivateKey(provider, secretKeyringFile, 0L, passphrase); - } - - private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, long keyId, char[] passphrase) throws IOException, PGPException { - // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated - - // Read in from the secret keyring file - try (FileInputStream keyInputStream = new FileInputStream(secretKeyringFile)) { - - // Form the SecretKeyRing collection (1.53 way with fingerprint calculator) - PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator()); - - // The decryptor is identical for all keys - final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(provider).build(passphrase); - - // Iterate over all secret keyrings - Iterator<PGPSecretKeyRing> keyringIterator = pgpSecretKeyRingCollection.getKeyRings(); - PGPSecretKeyRing keyRing; - PGPSecretKey secretKey; - - while (keyringIterator.hasNext()) { - keyRing = keyringIterator.next(); - - // If keyId exists, get a specific secret key; else, iterate over all - if (keyId != 0) { - secretKey = keyRing.getSecretKey(keyId); - try { - return secretKey.extractPrivateKey(decryptor); - } catch (Exception e) { - throw new PGPException("No private key available using passphrase", e); - } - } else { - Iterator<PGPSecretKey> keyIterator = keyRing.getSecretKeys(); - - while (keyIterator.hasNext()) { - secretKey = keyIterator.next(); - try { - return secretKey.extractPrivateKey(decryptor); - } catch (Exception e) { - // TODO: Log (expected) failures? - } - } - } - } - } - - // If this point is reached, no private key could be extracted with the given passphrase - throw new PGPException("No private key available using passphrase"); - } - - /* - * Get the public key for a specific user id from a keyring. - */ - @SuppressWarnings("rawtypes") - public static PGPPublicKey getPublicKey(String userId, String publicKeyringFile) throws IOException, PGPException { - // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated - - // Read in from the public keyring file - try (FileInputStream keyInputStream = new FileInputStream(publicKeyringFile)) { - - // Form the PublicKeyRing collection (1.53 way with fingerprint calculator) - PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator()); - - // Iterate over all public keyrings - Iterator<PGPPublicKeyRing> iter = pgpPublicKeyRingCollection.getKeyRings(); - PGPPublicKeyRing keyRing; - while (iter.hasNext()) { - keyRing = iter.next(); - - // Iterate over each public key in this keyring - Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys(); - while (keyIter.hasNext()) { - PGPPublicKey publicKey = keyIter.next(); - - // Iterate over each userId attached to the public key - Iterator userIdIterator = publicKey.getUserIDs(); - while (userIdIterator.hasNext()) { - String id = (String) userIdIterator.next(); - if (userId.equalsIgnoreCase(id)) { - return publicKey; - } - } - } - } - } - - // If this point is reached, no public key could be extracted with the given userId - throw new PGPException("Could not find a public key with the given userId"); - } - - private static class OpenPGPDecryptCallback implements StreamCallback { - - private String provider; - private String secretKeyringFile; - private char[] passphrase; - - OpenPGPDecryptCallback(final String provider, final String secretKeyringFile, final char[] passphrase) { - this.provider = provider; - this.secretKeyringFile = secretKeyringFile; - this.passphrase = passphrase; - } - - @Override - public void process(InputStream in, OutputStream out) throws IOException { - try (InputStream pgpin = PGPUtil.getDecoderStream(in)) { - PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin, new BcKeyFingerprintCalculator()); - - Object obj = pgpFactory.nextObject(); - if (!(obj instanceof PGPEncryptedDataList)) { - obj = pgpFactory.nextObject(); - if (!(obj instanceof PGPEncryptedDataList)) { - throw new ProcessException("Invalid OpenPGP data"); - } - } - PGPEncryptedDataList encList = (PGPEncryptedDataList) obj; - - try { - PGPPrivateKey privateKey = null; - PGPPublicKeyEncryptedData encData = null; - - // Find the secret key in the encrypted data - Iterator it = encList.getEncryptedDataObjects(); - while (privateKey == null && it.hasNext()) { - obj = it.next(); - if (!(obj instanceof PGPPublicKeyEncryptedData)) { - throw new ProcessException("Invalid OpenPGP data"); - } - encData = (PGPPublicKeyEncryptedData) obj; - - // Check each encrypted data object to see if it contains the key ID for the secret key -> private key - try { - privateKey = getDecryptedPrivateKey(provider, secretKeyringFile, encData.getKeyID(), passphrase); - } catch (PGPException e) { - // TODO: Log (expected) exception? - } - } - if (privateKey == null) { - throw new ProcessException("Secret keyring does not contain the key required to decrypt"); - } - - // Read in the encrypted data stream and decrypt it - final PublicKeyDataDecryptorFactory dataDecryptor = new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(provider).build(privateKey); - try (InputStream clear = encData.getDataStream(dataDecryptor)) { - // Create a plain object factory - JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear); - - Object message = plainFact.nextObject(); - - // Check the message type and act accordingly - - // If compressed, decompress - if (message instanceof PGPCompressedData) { - PGPCompressedData cData = (PGPCompressedData) message; - JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream()); - - message = pgpFact.nextObject(); - } - - // If the message is literal data, read it and process to the out stream - if (message instanceof PGPLiteralData) { - PGPLiteralData literalData = (PGPLiteralData) message; - - try (InputStream lis = literalData.getInputStream()) { - final byte[] buffer = new byte[BLOCK_SIZE]; - int len; - while ((len = lis.read(buffer)) >= 0) { - out.write(buffer, 0, len); - } - } - } else if (message instanceof PGPOnePassSignatureList) { - // TODO: This is legacy code but should verify signature list here - throw new PGPException("encrypted message contains a signed message - not literal data."); - } else { - throw new PGPException("message is not a simple encrypted file - type unknown."); - } - - if (encData.isIntegrityProtected()) { - if (!encData.verify()) { - throw new PGPException("Failed message integrity check"); - } - } else { - logger.warn("No message integrity check"); - } - } - } catch (Exception e) { - throw new ProcessException(e.getMessage()); - } - } - } - - } - - private static class OpenPGPEncryptCallback implements StreamCallback { - - private String algorithm; - private String provider; - private String publicKeyring; - private String userId; - private String filename; - - OpenPGPEncryptCallback(final String algorithm, final String provider, final String keyring, final String userId, final String filename) { - this.algorithm = algorithm; - this.provider = provider; - this.publicKeyring = keyring; - this.userId = userId; - this.filename = filename; - } - - @Override - public void process(InputStream in, OutputStream out) throws IOException { - PGPPublicKey publicKey; - final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm); - - try { - publicKey = getPublicKey(userId, publicKeyring); - } catch (Exception e) { - throw new ProcessException("Invalid public keyring - " + e.getMessage()); - } - - try { - OutputStream output = out; - if (isArmored) { - output = new ArmoredOutputStream(out); - } - - try { - // TODO: Refactor internal symmetric encryption algorithm to be customizable - PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator( - new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider)); - - encryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider(provider)); - - // TODO: Refactor shared encryption code to utility - try (OutputStream encryptedOut = encryptedDataGenerator.open(output, new byte[BUFFER_SIZE])) { - PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED); - try (OutputStream compressedOut = compressedDataGenerator.open(encryptedOut, new byte[BUFFER_SIZE])) { - PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); - try (OutputStream literalOut = literalDataGenerator.open(compressedOut, PGPLiteralData.BINARY, filename, new Date(), new byte[BUFFER_SIZE])) { - - final byte[] buffer = new byte[BLOCK_SIZE]; - int len; - while ((len = in.read(buffer)) >= 0) { - literalOut.write(buffer, 0, len); - } - } - } - } - } finally { - if (isArmored) { - output.close(); - } - } - } catch (Exception e) { - throw new ProcessException(e.getMessage()); - } - } - } -}
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java deleted file mode 100644 index 1ffe9e4..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/OpenPGPPasswordBasedEncryptor.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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.processors.standard.util; - -import org.apache.nifi.processor.exception.ProcessException; -import org.apache.nifi.processor.io.StreamCallback; -import org.apache.nifi.processors.standard.EncryptContent.Encryptor; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import static org.bouncycastle.openpgp.PGPUtil.getDecoderStream; - -public class OpenPGPPasswordBasedEncryptor implements Encryptor { - private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class); - - private String algorithm; - private String provider; - private char[] password; - private String filename; - - public OpenPGPPasswordBasedEncryptor(final String algorithm, final String provider, final char[] passphrase, final String filename) { - this.algorithm = algorithm; - this.provider = provider; - this.password = passphrase; - this.filename = filename; - } - - @Override - public StreamCallback getEncryptionCallback() throws Exception { - return new OpenPGPEncryptCallback(algorithm, provider, password, filename); - } - - @Override - public StreamCallback getDecryptionCallback() throws Exception { - return new OpenPGPDecryptCallback(provider, password); - } - - private static class OpenPGPDecryptCallback implements StreamCallback { - - private String provider; - private char[] password; - - OpenPGPDecryptCallback(final String provider, final char[] password) { - this.provider = provider; - this.password = password; - } - - @Override - public void process(InputStream in, OutputStream out) throws IOException { - InputStream pgpin = getDecoderStream(in); - JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(pgpin); - - Object obj = pgpFactory.nextObject(); - if (!(obj instanceof PGPEncryptedDataList)) { - obj = pgpFactory.nextObject(); - if (!(obj instanceof PGPEncryptedDataList)) { - throw new ProcessException("Invalid OpenPGP data"); - } - } - PGPEncryptedDataList encList = (PGPEncryptedDataList) obj; - - obj = encList.get(0); - if (!(obj instanceof PGPPBEEncryptedData)) { - throw new ProcessException("Invalid OpenPGP data"); - } - PGPPBEEncryptedData encryptedData = (PGPPBEEncryptedData) obj; - - try { - final PGPDigestCalculatorProvider digestCalculatorProvider = new JcaPGPDigestCalculatorProviderBuilder().setProvider(provider).build(); - final PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(digestCalculatorProvider).setProvider(provider).build(password); - InputStream clear = encryptedData.getDataStream(decryptorFactory); - - JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(clear); - - obj = pgpObjectFactory.nextObject(); - if (obj instanceof PGPCompressedData) { - PGPCompressedData compressedData = (PGPCompressedData) obj; - pgpObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream()); - obj = pgpObjectFactory.nextObject(); - } - - PGPLiteralData literalData = (PGPLiteralData) obj; - InputStream plainIn = literalData.getInputStream(); - final byte[] buffer = new byte[org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE]; - int len; - while ((len = plainIn.read(buffer)) >= 0) { - out.write(buffer, 0, len); - } - - if (encryptedData.isIntegrityProtected()) { - if (!encryptedData.verify()) { - throw new PGPException("Integrity check failed"); - } - } else { - logger.warn("No message integrity check"); - } - } catch (Exception e) { - throw new ProcessException(e.getMessage()); - } - } - } - - private static class OpenPGPEncryptCallback implements StreamCallback { - - private String algorithm; - private String provider; - private char[] password; - private String filename; - - OpenPGPEncryptCallback(final String algorithm, final String provider, final char[] password, final String filename) { - this.algorithm = algorithm; - this.provider = provider; - this.password = password; - this.filename = filename; - } - - @Override - public void process(InputStream in, OutputStream out) throws IOException { - try { - PGPKeyEncryptionMethodGenerator encryptionMethodGenerator = new JcePBEKeyEncryptionMethodGenerator(password).setProvider(provider); - org.apache.nifi.processors.standard.util.PGPUtil.encrypt(in, out, algorithm, provider, PGPEncryptedData.AES_128, filename, encryptionMethodGenerator); - } catch (Exception e) { - throw new ProcessException(e.getMessage()); - } - } - } -} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java deleted file mode 100644 index d3d50b8..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/PasswordBasedEncryptor.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * 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.processors.standard.util; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.List; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.PBEParameterSpec; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.processor.exception.ProcessException; -import org.apache.nifi.processor.io.StreamCallback; -import org.apache.nifi.processors.standard.EncryptContent.Encryptor; -import org.apache.nifi.security.util.KeyDerivationFunction; -import org.apache.nifi.stream.io.StreamUtils; - -public class PasswordBasedEncryptor implements Encryptor { - - private Cipher cipher; - private int saltSize; - private SecretKey secretKey; - private KeyDerivationFunction kdf; - private int iterationsCount = LEGACY_KDF_ITERATIONS; - - @Deprecated - private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; - private static final int DEFAULT_SALT_SIZE = 8; - // TODO: Eventually KDF-specific values should be refactored into injectable interface impls - private static final int LEGACY_KDF_ITERATIONS = 1000; - private static final int OPENSSL_EVP_HEADER_SIZE = 8; - private static final int OPENSSL_EVP_SALT_SIZE = 8; - private static final String OPENSSL_EVP_HEADER_MARKER = "Salted__"; - private static final int OPENSSL_EVP_KDF_ITERATIONS = 0; - private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128; - - private static boolean isUnlimitedStrengthCryptographyEnabled; - - // Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system - static { - try { - isUnlimitedStrengthCryptographyEnabled = (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH); - } catch (NoSuchAlgorithmException e) { - // if there are issues with this, we default back to the value established - isUnlimitedStrengthCryptographyEnabled = false; - } - } - - public PasswordBasedEncryptor(final String algorithm, final String providerName, final char[] password, KeyDerivationFunction kdf) { - super(); - try { - // initialize cipher - this.cipher = Cipher.getInstance(algorithm, providerName); - this.kdf = kdf; - - if (isOpenSSLKDF()) { - this.saltSize = OPENSSL_EVP_SALT_SIZE; - this.iterationsCount = OPENSSL_EVP_KDF_ITERATIONS; - } else { - int algorithmBlockSize = cipher.getBlockSize(); - this.saltSize = (algorithmBlockSize > 0) ? algorithmBlockSize : DEFAULT_SALT_SIZE; - } - - // initialize SecretKey from password - final PBEKeySpec pbeKeySpec = new PBEKeySpec(password); - final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, providerName); - this.secretKey = factory.generateSecret(pbeKeySpec); - } catch (Exception e) { - throw new ProcessException(e); - } - } - - public static int getMaxAllowedKeyLength(final String algorithm) { - if (StringUtils.isEmpty(algorithm)) { - return DEFAULT_MAX_ALLOWED_KEY_LENGTH; - } - String parsedCipher = parseCipherFromAlgorithm(algorithm); - try { - return Cipher.getMaxAllowedKeyLength(parsedCipher); - } catch (NoSuchAlgorithmException e) { - // Default algorithm max key length on unmodified JRE - return DEFAULT_MAX_ALLOWED_KEY_LENGTH; - } - } - - private static String parseCipherFromAlgorithm(final String algorithm) { - // This is not optimal but the algorithms do not have a standard format - final String AES = "AES"; - final String TDES = "TRIPLEDES"; - 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, 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 (algorithm.contains(cipher)) { - if (cipher.equals(TDES)) { - return ACTUAL_TDES_CIPHER; - } else { - return cipher; - } - } - } - - return algorithm; - } - - public static boolean supportsUnlimitedStrength() { - return isUnlimitedStrengthCryptographyEnabled; - } - - @Override - public StreamCallback getEncryptionCallback() throws ProcessException { - try { - byte[] salt = new byte[saltSize]; - SecureRandom secureRandom = new SecureRandom(); - secureRandom.nextBytes(salt); - return new EncryptCallback(salt); - } catch (Exception e) { - throw new ProcessException(e); - } - } - - @Override - public StreamCallback getDecryptionCallback() throws ProcessException { - return new DecryptCallback(); - } - - private int getIterationsCount() { - return iterationsCount; - } - - private boolean isOpenSSLKDF() { - return KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY.equals(kdf); - } - - private class DecryptCallback implements StreamCallback { - - public DecryptCallback() { - } - @Override - public void process(final InputStream in, final OutputStream out) throws IOException { - byte[] salt = new byte[saltSize]; - - try { - // If the KDF is OpenSSL, try to read the salt from the input stream - if (isOpenSSLKDF()) { - // The header and salt format is "Salted__salt x8b" in ASCII - - // Try to read the header and salt from the input - byte[] header = new byte[PasswordBasedEncryptor.OPENSSL_EVP_HEADER_SIZE]; - - // Mark the stream in case there is no salt - in.mark(OPENSSL_EVP_HEADER_SIZE + 1); - StreamUtils.fillBuffer(in, header); - - final byte[] headerMarkerBytes = OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII); - - if (!Arrays.equals(headerMarkerBytes, header)) { - // No salt present - salt = new byte[0]; - // Reset the stream because we skipped 8 bytes of cipher text - in.reset(); - } - } - - StreamUtils.fillBuffer(in, salt); - } catch (final EOFException e) { - throw new ProcessException("Cannot decrypt because file size is smaller than salt size", e); - } - - final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationsCount()); - try { - cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec); - } catch (final Exception e) { - throw new ProcessException(e); - } - - final byte[] buffer = new byte[65536]; - int len; - while ((len = in.read(buffer)) > 0) { - final byte[] decryptedBytes = cipher.update(buffer, 0, len); - if (decryptedBytes != null) { - out.write(decryptedBytes); - } - } - - try { - out.write(cipher.doFinal()); - } catch (final Exception e) { - throw new ProcessException(e); - } - } - } - - private class EncryptCallback implements StreamCallback { - - private final byte[] salt; - - public EncryptCallback(final byte[] salt) { - this.salt = salt; - } - - @Override - public void process(final InputStream in, final OutputStream out) throws IOException { - final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationsCount()); - try { - cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec); - } catch (final Exception e) { - throw new ProcessException(e); - } - - // If this is OpenSSL EVP, the salt must be preceded by the header - if (isOpenSSLKDF()) { - out.write(OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII)); - } - - out.write(salt); - - final byte[] buffer = new byte[65536]; - int len; - while ((len = in.read(buffer)) > 0) { - final byte[] encryptedBytes = cipher.update(buffer, 0, len); - if (encryptedBytes != null) { - out.write(encryptedBytes); - } - } - - try { - out.write(cipher.doFinal()); - } catch (final IllegalBlockSizeException | BadPaddingException e) { - throw new ProcessException(e); - } - } - } -} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProvider.java new file mode 100644 index 0000000..907aed2 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProvider.java @@ -0,0 +1,153 @@ +/* + * 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.processors.standard.util.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.EncryptionMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.List; + +/** + * This is a standard implementation of {@link KeyedCipherProvider} which supports {@code AES} cipher families with arbitrary modes of operation (currently only {@code CBC}, {@code CTR}, and {@code + * GCM} are supported as {@link EncryptionMethod}s. + */ +public class AESKeyedCipherProvider extends KeyedCipherProvider { + private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProvider.class); + private static final int IV_LENGTH = 16; + private static final List<Integer> VALID_KEY_LENGTHS = Arrays.asList(128, 192, 256); + + /** + * Returns an initialized cipher for the specified algorithm. The IV is provided externally to allow for non-deterministic IVs, as IVs + * deterministically derived from the password are a potential vulnerability and compromise semantic security. See + * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param key the key + * @param iv the IV or nonce (cannot be all 0x00) + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws Exception { + try { + return getInitializedCipher(encryptionMethod, key, iv, encryptMode); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new ProcessException("Error initializing the cipher", e); + } + } + + /** + * Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param key the key + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher or if decryption is requested + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, key, new byte[0], encryptMode); + } + + protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, + boolean encryptMode) throws NoSuchAlgorithmException, NoSuchProviderException, + InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, UnsupportedEncodingException { + if (encryptionMethod == null) { + throw new IllegalArgumentException("The encryption method must be specified"); + } + + if (!encryptionMethod.isKeyedCipher()) { + throw new IllegalArgumentException(encryptionMethod.name() + " requires a PBECipherProvider"); + } + + String algorithm = encryptionMethod.getAlgorithm(); + String provider = encryptionMethod.getProvider(); + + if (key == null) { + throw new IllegalArgumentException("The key must be specified"); + } + + if (!isValidKeyLength(key)) { + throw new IllegalArgumentException("The key must be of length [" + StringUtils.join(VALID_KEY_LENGTHS, ", ") + "]"); + } + + Cipher cipher = Cipher.getInstance(algorithm, provider); + final String operation = encryptMode ? "encrypt" : "decrypt"; + + boolean ivIsInvalid = false; + + // If an IV was not provided already, generate a random IV and inject it in the cipher + int ivLength = cipher.getBlockSize(); + if (iv.length != ivLength) { + logger.warn("An IV was provided of length {} bytes for {}ion but should be {} bytes", iv.length, operation, ivLength); + ivIsInvalid = true; + } + + final byte[] emptyIv = new byte[ivLength]; + if (Arrays.equals(iv, emptyIv)) { + logger.warn("An empty IV was provided of length {} for {}ion", iv.length, operation); + ivIsInvalid = true; + } + + if (ivIsInvalid) { + if (encryptMode) { + logger.warn("Generating new IV. The value can be obtained in the calling code by invoking 'cipher.getIV()';"); + iv = generateIV(); + } else { + // Can't decrypt without an IV + throw new IllegalArgumentException("Cannot decrypt without a valid IV"); + } + } + cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + + return cipher; + } + + private boolean isValidKeyLength(SecretKey key) { + return VALID_KEY_LENGTHS.contains(key.getEncoded().length * 8); + } + + /** + * Generates a new random IV of 16 bytes using {@link java.security.SecureRandom}. + * + * @return the IV + */ + public byte[] generateIV() { + byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + return iv; + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProvider.java new file mode 100644 index 0000000..8c5f464 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProvider.java @@ -0,0 +1,198 @@ +/* + * 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.processors.standard.util.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.standard.util.crypto.bcrypt.BCrypt; +import org.apache.nifi.security.util.EncryptionMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BcryptCipherProvider extends RandomIVPBECipherProvider { + private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProvider.class); + + private final int workFactor; + /** + * This can be calculated automatically using the code {@see BcryptCipherProviderGroovyTest#calculateMinimumWorkFactor} or manually updated by a maintainer + */ + private static final int DEFAULT_WORK_FACTOR = 12; + private static final int DEFAULT_SALT_LENGTH = 16; + + private static final Pattern BCRYPT_SALT_FORMAT = Pattern.compile("^\\$\\d\\w\\$\\d{2}\\$[\\w\\/\\.]{22}"); + + /** + * Instantiates a Bcrypt cipher provider with the default work factor 12 (2^12 key expansion rounds). + */ + public BcryptCipherProvider() { + this(DEFAULT_WORK_FACTOR); + } + + /** + * Instantiates a Bcrypt cipher provider with the specified work factor w (2^w key expansion rounds). + * + * @param workFactor the (log) number of key expansion rounds [4..30] + */ + public BcryptCipherProvider(int workFactor) { + this.workFactor = workFactor; + if (workFactor < DEFAULT_WORK_FACTOR) { + logger.warn("The provided work factor {} is below the recommended minimum {}", workFactor, DEFAULT_WORK_FACTOR); + } + } + + /** + * Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. The IV is provided externally to allow for non-deterministic IVs, as IVs + * deterministically derived from the password are a potential vulnerability and compromise semantic security. See + * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)}) + * @param iv the IV + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception { + try { + return getInitializedCipher(encryptionMethod, password, salt, iv, keyLength, encryptMode); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new ProcessException("Error initializing the cipher", e); + } + } + + @Override + Logger getLogger() { + return logger; + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * <p> + * This method is deprecated because while Bcrypt could generate a random salt to use, it would not be returned to the caller of this method and future derivations would fail. Provide a valid + * salt generated by {@link BcryptCipherProvider#generateSalt()}. + * </p> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + * @deprecated Provide a salt parameter using {@link BcryptCipherProvider#getCipher(EncryptionMethod, String, byte[], int, boolean)} + */ + @Deprecated + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, int keyLength, boolean encryptMode) throws Exception { + throw new UnsupportedOperationException("The cipher cannot be initialized without a valid salt. Use BcryptCipherProvider#generateSalt() to generate a valid salt"); + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * + * The IV can be retrieved by the calling method using {@link Cipher#getIV()}. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)}) + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, salt, new byte[0], keyLength, encryptMode); + } + + protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception { + if (encryptionMethod == null) { + throw new IllegalArgumentException("The encryption method must be specified"); + } + if (!encryptionMethod.isCompatibleWithStrongKDFs()) { + throw new IllegalArgumentException(encryptionMethod.name() + " is not compatible with Bcrypt"); + } + + if (StringUtils.isEmpty(password)) { + throw new IllegalArgumentException("Encryption with an empty password is not supported"); + } + + String algorithm = encryptionMethod.getAlgorithm(); + String provider = encryptionMethod.getProvider(); + + final String cipherName = CipherUtility.parseCipherFromAlgorithm(algorithm); + if (!CipherUtility.isValidKeyLength(keyLength, cipherName)) { + throw new IllegalArgumentException(String.valueOf(keyLength) + " is not a valid key length for " + cipherName); + } + + String bcryptSalt = formatSaltForBcrypt(salt); + + String hash = BCrypt.hashpw(password, bcryptSalt); + + /* The SHA-512 hash is required in order to derive a key longer than 184 bits (the resulting size of the Bcrypt hash) and ensuring the avalanche effect causes higher key entropy (if all + derived keys follow a consistent pattern, it weakens the strength of the encryption) */ + MessageDigest digest = MessageDigest.getInstance("SHA-512", provider); + byte[] dk = digest.digest(hash.getBytes(StandardCharsets.UTF_8)); + dk = Arrays.copyOf(dk, keyLength / 8); + SecretKey tempKey = new SecretKeySpec(dk, algorithm); + + KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider(); + return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode); + } + + private String formatSaltForBcrypt(byte[] salt) { + if (salt == null || salt.length == 0) { + throw new IllegalArgumentException("The salt cannot be empty. To generate a salt, use BcryptCipherProvider#generateSalt()"); + } + + String rawSalt = new String(salt, StandardCharsets.UTF_8); + Matcher matcher = BCRYPT_SALT_FORMAT.matcher(rawSalt); + + if (matcher.find()) { + return rawSalt; + } else { + throw new IllegalArgumentException("The salt must be of the format $2a$10$gUVbkVzp79H8YaCOsCVZNu. To generate a salt, use BcryptCipherProvider#generateSalt()"); + } + } + + @Override + public byte[] generateSalt() { + return BCrypt.gensalt(workFactor).getBytes(StandardCharsets.UTF_8); + } + + @Override + public int getDefaultSaltLength() { + return DEFAULT_SALT_LENGTH; + } + + protected int getWorkFactor() { + return workFactor; + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProvider.java new file mode 100644 index 0000000..46e815f --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProvider.java @@ -0,0 +1,23 @@ +/* + * 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.processors.standard.util.crypto; + +/** + * Marker interface for cipher providers. + */ +public interface CipherProvider { +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactory.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactory.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactory.java new file mode 100644 index 0000000..e04a7b4 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactory.java @@ -0,0 +1,57 @@ +/* + * 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.processors.standard.util.crypto; + +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.KeyDerivationFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class CipherProviderFactory { + private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactory.class); + + private static Map<KeyDerivationFunction, Class<? extends CipherProvider>> registeredCipherProviders; + + static { + registeredCipherProviders = new HashMap<>(); + registeredCipherProviders.put(KeyDerivationFunction.NIFI_LEGACY, NiFiLegacyCipherProvider.class); + registeredCipherProviders.put(KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY, OpenSSLPKCS5CipherProvider.class); + registeredCipherProviders.put(KeyDerivationFunction.PBKDF2, PBKDF2CipherProvider.class); + registeredCipherProviders.put(KeyDerivationFunction.BCRYPT, BcryptCipherProvider.class); + registeredCipherProviders.put(KeyDerivationFunction.SCRYPT, ScryptCipherProvider.class); + registeredCipherProviders.put(KeyDerivationFunction.NONE, AESKeyedCipherProvider.class); + } + + public static CipherProvider getCipherProvider(KeyDerivationFunction kdf) { + logger.debug("{} KDFs registered", registeredCipherProviders.size()); + + if (registeredCipherProviders.containsKey(kdf)) { + Class<? extends CipherProvider> clazz = registeredCipherProviders.get(kdf); + try { + return clazz.newInstance(); + } catch (Exception e) { + logger.error("Error instantiating new {} with default parameters for {}", clazz.getName(), kdf.getName()); + throw new ProcessException("Error instantiating cipher provider"); + } + } + + throw new IllegalArgumentException("No cipher provider registered for " + kdf.getName()); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherUtility.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherUtility.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherUtility.java new file mode 100644 index 0000000..fbd5b4e --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/CipherUtility.java @@ -0,0 +1,320 @@ +/* + * 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.processors.standard.util.crypto; + +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; + +import javax.crypto.Cipher; +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; + +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; + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedCipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedCipherProvider.java new file mode 100644 index 0000000..f0fa4fc --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedCipherProvider.java @@ -0,0 +1,73 @@ +/* + * 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.processors.standard.util.crypto; + +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.EncryptionMethod; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public abstract class KeyedCipherProvider implements CipherProvider { + static final byte[] IV_DELIMITER = "NiFiIV".getBytes(StandardCharsets.UTF_8); + // This is 16 bytes for AES but can vary for other ciphers + static final int MAX_IV_LIMIT = 16; + + /** + * Returns an initialized cipher for the specified algorithm. The IV is provided externally to allow for non-deterministic IVs, as IVs + * deterministically derived from the password are a potential vulnerability and compromise semantic security. See + * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param key the key + * @param iv the IV or nonce + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws Exception; + + /** + * Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param key the key + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher or if decryption is requested + */ + abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception; + + /** + * Generates a new random IV of the correct length. + * + * @return the IV + */ + abstract byte[] generateIV(); + + public byte[] readIV(InputStream in) throws IOException, ProcessException { + return CipherUtility.readBytesFromInputStream(in, "IV", MAX_IV_LIMIT, IV_DELIMITER); + } + + public void writeIV(byte[] iv, OutputStream out) throws IOException { + CipherUtility.writeBytesToOutputStream(out, iv, "IV", IV_DELIMITER); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptor.java new file mode 100644 index 0000000..c573d3d --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptor.java @@ -0,0 +1,163 @@ +/* + * 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.processors.standard.util.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.StreamCallback; +import org.apache.nifi.processors.standard.EncryptContent.Encryptor; +import org.apache.nifi.security.util.EncryptionMethod; +import org.apache.nifi.security.util.KeyDerivationFunction; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchAlgorithmException; + +public class KeyedEncryptor implements Encryptor { + + private EncryptionMethod encryptionMethod; + private SecretKey key; + private byte[] iv; + + private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128; + + private static boolean isUnlimitedStrengthCryptographyEnabled; + + // Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system + static { + try { + isUnlimitedStrengthCryptographyEnabled = (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH); + } catch (NoSuchAlgorithmException e) { + // if there are issues with this, we default back to the value established + isUnlimitedStrengthCryptographyEnabled = false; + } + } + + public KeyedEncryptor(final EncryptionMethod encryptionMethod, final SecretKey key) { + this(encryptionMethod, key == null ? new byte[0] : key.getEncoded(), new byte[0]); + } + + public KeyedEncryptor(final EncryptionMethod encryptionMethod, final SecretKey key, final byte[] iv) { + this(encryptionMethod, key == null ? new byte[0] : key.getEncoded(), iv); + } + + public KeyedEncryptor(final EncryptionMethod encryptionMethod, final byte[] keyBytes) { + this(encryptionMethod, keyBytes, new byte[0]); + } + + public KeyedEncryptor(final EncryptionMethod encryptionMethod, final byte[] keyBytes, final byte[] iv) { + super(); + try { + if (encryptionMethod == null) { + throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with null encryption method"); + } + if (!encryptionMethod.isKeyedCipher()) { + throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with encryption method " + encryptionMethod.name()); + } + this.encryptionMethod = encryptionMethod; + if (keyBytes == null || keyBytes.length == 0) { + throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with empty key"); + } + if (!CipherUtility.isValidKeyLengthForAlgorithm(keyBytes.length * 8, encryptionMethod.getAlgorithm())) { + throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with key of length " + keyBytes.length); + } + String cipherName = CipherUtility.parseCipherFromAlgorithm(encryptionMethod.getAlgorithm()); + this.key = new SecretKeySpec(keyBytes, cipherName); + + this.iv = iv; + } catch (Exception e) { + throw new ProcessException(e); + } + } + + public static int getMaxAllowedKeyLength(final String algorithm) { + if (StringUtils.isEmpty(algorithm)) { + return DEFAULT_MAX_ALLOWED_KEY_LENGTH; + } + String parsedCipher = CipherUtility.parseCipherFromAlgorithm(algorithm); + try { + return Cipher.getMaxAllowedKeyLength(parsedCipher); + } catch (NoSuchAlgorithmException e) { + // Default algorithm max key length on unmodified JRE + return DEFAULT_MAX_ALLOWED_KEY_LENGTH; + } + } + + public static boolean supportsUnlimitedStrength() { + return isUnlimitedStrengthCryptographyEnabled; + } + + @Override + public StreamCallback getEncryptionCallback() throws ProcessException { + return new EncryptCallback(); + } + + @Override + public StreamCallback getDecryptionCallback() throws ProcessException { + return new DecryptCallback(); + } + + private class DecryptCallback implements StreamCallback { + + public DecryptCallback() { + } + + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + // Initialize cipher provider + KeyedCipherProvider cipherProvider = (KeyedCipherProvider) CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE); + + // Generate cipher + try { + Cipher cipher; + // The IV could have been set by the constructor, but if not, read from the cipher stream + if (iv.length == 0) { + iv = cipherProvider.readIV(in); + } + cipher = cipherProvider.getCipher(encryptionMethod, key, iv, false); + CipherUtility.processStreams(cipher, in, out); + } catch (Exception e) { + throw new ProcessException(e); + } + } + } + + private class EncryptCallback implements StreamCallback { + + public EncryptCallback() { + } + + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + // Initialize cipher provider + KeyedCipherProvider cipherProvider = (KeyedCipherProvider) CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE); + + // Generate cipher + try { + Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, iv, true); + cipherProvider.writeIV(cipher.getIV(), out); + CipherUtility.processStreams(cipher, in, out); + } catch (Exception e) { + throw new ProcessException(e); + } + } + } +} \ No newline at end of file