You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by al...@apache.org on 2016/02/05 01:41:38 UTC
[6/7] nifi git commit: NIFI-1257 NIFI-1259
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