diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml
index 9cdb654ac4..9214536eec 100644
--- a/nifi-assembly/pom.xml
+++ b/nifi-assembly/pom.xml
@@ -235,6 +235,12 @@ language governing permissions and limitations under the License. -->
1.20.0-SNAPSHOTnar
+
+ org.apache.nifi
+ nifi-cipher-nar
+ 1.20.0-SNAPSHOT
+ nar
+ org.apache.nifinifi-distributed-cache-services-nar
diff --git a/nifi-commons/nifi-security-crypto-key/pom.xml b/nifi-commons/nifi-security-crypto-key/pom.xml
new file mode 100644
index 0000000000..c569e48fca
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-commons
+ 1.20.0-SNAPSHOT
+
+ nifi-security-crypto-key
+ Cryptographic key derivation function components with minimal dependencies
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+
+
+
+
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKey.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKey.java
new file mode 100644
index 0000000000..bb2a6d1afc
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKey.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+import java.security.Key;
+
+/**
+ * Derived Key extension of standard Key supporting serialized representation
+ */
+public interface DerivedKey extends Key {
+ /**
+ * Get serialized representation of derived key including parameters when applicable to a particular function algorithm
+ *
+ * @return Serialized representation of derived key
+ */
+ String getSerialized();
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyParameterSpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyParameterSpec.java
new file mode 100644
index 0000000000..4a9061fe8d
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyParameterSpec.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+/**
+ * Derived Key Parameter Specification with properties for implementing algorithms
+ */
+public interface DerivedKeyParameterSpec {
+ /**
+ * Get cryptographic salt that algorithms use for key derivation
+ *
+ * @return Cryptographic salt bytes
+ */
+ byte[] getSalt();
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyParameterSpecReader.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyParameterSpecReader.java
new file mode 100644
index 0000000000..c198643c96
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyParameterSpecReader.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+/**
+ * Abstraction for reading Derived Key Parameter Specifications from serialized parameters
+ */
+public interface DerivedKeyParameterSpecReader {
+ /**
+ * Read serialized parameters and return Derived Key Parameter Specification
+ *
+ * @param serializedParameters Serialized parameters
+ * @return Derived Key Parameter Specification
+ */
+ T read(byte[] serializedParameters);
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyProvider.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyProvider.java
new file mode 100644
index 0000000000..705f2b5bfc
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeyProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+/**
+ * Provider abstraction for Key Derivation Function implementations
+ */
+public interface DerivedKeyProvider {
+ /**
+ * Get Derived Key using configured properties
+ *
+ * @param derivedKeySpec Derived Key Specification
+ * @return Derived Key
+ */
+ DerivedKey getDerivedKey(DerivedKeySpec derivedKeySpec);
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeySpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeySpec.java
new file mode 100644
index 0000000000..4567f7a733
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedKeySpec.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+/**
+ * Derived Key Specification with password and parameters for implementing algorithms
+ */
+public interface DerivedKeySpec {
+ /**
+ * Get password characters that algorithms use for key derivation
+ *
+ * @return Password characters
+ */
+ char[] getPassword();
+
+ /**
+ * Get length requested for derived key in bytes
+ *
+ * @return Length of derived key in bytes
+ */
+ int getDerivedKeyLength();
+
+ /**
+ * Get cipher algorithm for which the derived key will be used
+ *
+ * @return Cipher algorithm
+ */
+ String getAlgorithm();
+
+ /**
+ * Get parameter specification
+ *
+ * @return Parameter specification
+ */
+ T getParameterSpec();
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedSecretKey.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedSecretKey.java
new file mode 100644
index 0000000000..e88becc315
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/DerivedSecretKey.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Derived Secret Key containing encoded key bytes and serialized representation of key with parameters
+ */
+public class DerivedSecretKey extends SecretKeySpec implements DerivedKey {
+ private final String serialized;
+
+ /**
+ * Derived Secret Key constructor with encoded key and serialized representation
+ *
+ * @param key Encoded key bytes
+ * @param algorithm Cipher algorithm for which the key will be used
+ * @param serialized Serialized representation of the key with parameters depending on derivation function
+ */
+ public DerivedSecretKey(final byte[] key, final String algorithm, final String serialized) {
+ super(key, algorithm);
+ this.serialized = serialized;
+ }
+
+ /**
+ * Get serialized key with parameters
+ *
+ * @return Serialized key with parameters
+ */
+ @Override
+ public String getSerialized() {
+ return serialized;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/StandardDerivedKeySpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/StandardDerivedKeySpec.java
new file mode 100644
index 0000000000..9e501c0735
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/StandardDerivedKeySpec.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key;
+
+import java.util.Objects;
+
+/**
+ * Standard implementation of Derived Key Specification with required properties
+ */
+public class StandardDerivedKeySpec implements DerivedKeySpec {
+ private final char[] password;
+
+ private final int derivedKeyLength;
+
+ private final String algorithm;
+
+ private final T parameterSpec;
+
+ public StandardDerivedKeySpec(
+ final char[] password,
+ final int derivedKeyLength,
+ final String algorithm,
+ final T parameterSpec
+ ) {
+ this.password = Objects.requireNonNull(password, "Password required");
+ this.derivedKeyLength = derivedKeyLength;
+ this.algorithm = Objects.requireNonNull(algorithm, "Algorithm required");
+ this.parameterSpec = Objects.requireNonNull(parameterSpec, "Parameter Specification required");
+ }
+
+ @Override
+ public char[] getPassword() {
+ return password;
+ }
+
+ @Override
+ public int getDerivedKeyLength() {
+ return derivedKeyLength;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ @Override
+ public T getParameterSpec() {
+ return parameterSpec;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpec.java
new file mode 100644
index 0000000000..0561261f90
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpec.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.argon2;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+
+/**
+ * Argon2 key derivation function parameter specification
+ */
+public class Argon2DerivedKeyParameterSpec implements DerivedKeyParameterSpec {
+ private final int memory;
+
+ private final int iterations;
+
+ private final int parallelism;
+
+ private final byte[] salt;
+
+ /**
+ * Argon2 Parameter Specification constructor with required properties
+ *
+ * @param memory Size of memory in kilobytes for processing
+ * @param iterations Number of iterations to perform
+ * @param parallelism Number of threads for parallel processing
+ * @param salt Array of random salt bytes
+ */
+ public Argon2DerivedKeyParameterSpec(
+ final int memory,
+ final int iterations,
+ final int parallelism,
+ final byte[] salt
+ ) {
+ this.memory = memory;
+ this.iterations = iterations;
+ this.parallelism = parallelism;
+ this.salt = salt;
+ }
+
+ @Override
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getMemory() {
+ return memory;
+ }
+
+ public int getIterations() {
+ return iterations;
+ }
+
+ public int getParallelism() {
+ return parallelism;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpecReader.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpecReader.java
new file mode 100644
index 0000000000..78ee04bdc6
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpecReader.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.argon2;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpecReader;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Argon2 implementation reads a US-ASCII string of bytes using the Password-Hashing-Competition String Format specification
+ */
+public class Argon2DerivedKeyParameterSpecReader implements DerivedKeyParameterSpecReader {
+ /** Argon2id hybrid with version 1.3 and 22 character Base64 encoded salt with optional trailing hash parameter */
+ private static final Pattern PHC_STRING_FORMAT = Pattern.compile("^\\$argon2id\\$v=19\\$m=(\\d+),t=(\\d+),p=(\\d+)\\$([\\w/+]{22})(\\$[\\w/+]+)?$");
+
+ private static final int MEMORY_GROUP = 1;
+
+ private static final int ITERATIONS_GROUP = 2;
+
+ private static final int PARALLELISM_GROUP = 3;
+
+ private static final int SALT_GROUP = 4;
+
+ private static final Charset PARAMETERS_CHARACTER_SET = StandardCharsets.US_ASCII;
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ /**
+ * Read serialized parameters parsed from US-ASCII string of bytes
+ *
+ * @param serializedParameters Serialized parameters
+ * @return Argon2 Parameter Specification
+ */
+ @Override
+ public Argon2DerivedKeyParameterSpec read(final byte[] serializedParameters) {
+ Objects.requireNonNull(serializedParameters, "Parameters required");
+ final String parameters = new String(serializedParameters, PARAMETERS_CHARACTER_SET);
+
+ final Matcher matcher = PHC_STRING_FORMAT.matcher(parameters);
+ if (matcher.matches()) {
+ final String memoryGroup = matcher.group(MEMORY_GROUP);
+ final String iterationsGroup = matcher.group(ITERATIONS_GROUP);
+ final String parallelismGroup = matcher.group(PARALLELISM_GROUP);
+ final String saltGroup = matcher.group(SALT_GROUP);
+
+ final int memory = Integer.parseInt(memoryGroup);
+ final int iterations = Integer.parseInt(iterationsGroup);
+ final int parallelism = Integer.parseInt(parallelismGroup);
+ final byte[] salt = decoder.decode(saltGroup);
+ return new Argon2DerivedKeyParameterSpec(
+ memory,
+ iterations,
+ parallelism,
+ salt
+ );
+ } else {
+ final String message = String.format("Argon2 serialized parameters [%s] format not matched", parameters);
+ throw new IllegalArgumentException(message);
+ }
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyProvider.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyProvider.java
new file mode 100644
index 0000000000..81992e6dfb
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyProvider.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.argon2;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.DerivedSecretKey;
+import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
+import org.bouncycastle.crypto.params.Argon2Parameters;
+
+import java.util.Base64;
+
+/**
+ * Argon2 implementation of Derived Key Provider based on Bouncy Castle Argon2 components described in RFC 9016
+ */
+public class Argon2DerivedKeyProvider implements DerivedKeyProvider {
+ private static final int HYBRID_VERSION = Argon2Parameters.ARGON2_id;
+
+ private static final int CURRENT_VERSION = Argon2Parameters.ARGON2_VERSION_13;
+
+ private static final String SERIALIZED_FORMAT = "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s";
+
+ private static final Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
+
+ /**
+ * Get Derived Key using Argon2id version 1.3 and provided specification
+ *
+ * @param derivedKeySpec Derived Key Specification
+ * @return Derived Secret Key
+ */
+ @Override
+ public DerivedKey getDerivedKey(final DerivedKeySpec derivedKeySpec) {
+ final Argon2DerivedKeyParameterSpec parameterSpec = derivedKeySpec.getParameterSpec();
+ final Argon2Parameters parameters = getParameters(parameterSpec);
+ final Argon2BytesGenerator generator = new Argon2BytesGenerator();
+ generator.init(parameters);
+
+ final int derivedKeyLength = derivedKeySpec.getDerivedKeyLength();
+ final byte[] derivedKeyBytes = new byte[derivedKeyLength];
+ final char[] password = derivedKeySpec.getPassword();
+ generator.generateBytes(password, derivedKeyBytes);
+
+ final String serialized = getSerialized(derivedKeyBytes, parameterSpec);
+ return new DerivedSecretKey(derivedKeyBytes, derivedKeySpec.getAlgorithm(), serialized);
+ }
+
+ private Argon2Parameters getParameters(final Argon2DerivedKeyParameterSpec parameterSpec) {
+ return new Argon2Parameters.Builder(HYBRID_VERSION)
+ .withVersion(CURRENT_VERSION)
+ .withMemoryAsKB(parameterSpec.getMemory())
+ .withIterations(parameterSpec.getIterations())
+ .withParallelism(parameterSpec.getParallelism())
+ .withSalt(parameterSpec.getSalt())
+ .build();
+ }
+
+ private String getSerialized(final byte[] derivedKeyBytes, final Argon2DerivedKeyParameterSpec parameterSpec) {
+ final String derivedKeyEncoded = encoder.encodeToString(derivedKeyBytes);
+ final String saltEncoded = encoder.encodeToString(parameterSpec.getSalt());
+ return String.format(
+ SERIALIZED_FORMAT,
+ CURRENT_VERSION,
+ parameterSpec.getMemory(),
+ parameterSpec.getIterations(),
+ parameterSpec.getParallelism(),
+ saltEncoded,
+ derivedKeyEncoded
+ );
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptBase64Decoder.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptBase64Decoder.java
new file mode 100644
index 0000000000..1c0a439a2f
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptBase64Decoder.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.bcrypt;
+
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Base64 Decoder translates from bcrypt Base64 characters to RFC 4648 characters
+ */
+class BcryptBase64Decoder {
+ /** Alphabet of shared characters following common ordering */
+ private static final String SHARED_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ /** bcrypt alphabet defined according to the OpenBSD bcrypt function beginning with control characters */
+ private static final String BCRYPT_ALPHABET = String.format("./%s", SHARED_ALPHABET);
+
+ /** Standard alphabet defined according to RFC 4648 ending with control characters */
+ private static final String STANDARD_ALPHABET = String.format("%s+/", SHARED_ALPHABET);
+
+ private static final Map BCRYPT_STANDARD_CHARACTERS = new LinkedHashMap<>();
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ static {
+ final char[] bcryptCharacters = BCRYPT_ALPHABET.toCharArray();
+ final char[] standardCharacters = STANDARD_ALPHABET.toCharArray();
+
+ for (int i = 0; i < bcryptCharacters.length; i++) {
+ final char bcryptCharacter = bcryptCharacters[i];
+ final char standardCharacter = standardCharacters[i];
+ BCRYPT_STANDARD_CHARACTERS.put(bcryptCharacter, standardCharacter);
+ }
+ }
+
+ /**
+ * Decode bcrypt Base64 encoded ASCII characters to support reading salt and hash strings
+ *
+ * @param encoded ASCII string of characters encoded using bcrypt Base64 characters
+ * @return Decoded bytes
+ */
+ static byte[] decode(final String encoded) {
+ final int encodedLength = encoded.length();
+ final byte[] converted = new byte[encodedLength];
+ for (int i = 0; i < encodedLength; i++) {
+ final char encodedCharacter = encoded.charAt(i);
+ final char standardCharacter = getStandardCharacter(encodedCharacter);
+ converted[i] = (byte) standardCharacter;
+ }
+ return decoder.decode(converted);
+ }
+
+ private static char getStandardCharacter(final char encodedCharacter) {
+ final Character standardCharacter = BCRYPT_STANDARD_CHARACTERS.get(encodedCharacter);
+ if (standardCharacter == null) {
+ final String message = String.format("Encoded character [%c] not supported for bcrypt Base64 decoding", encodedCharacter);
+ throw new IllegalArgumentException(message);
+ }
+ return standardCharacter;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpec.java
new file mode 100644
index 0000000000..6d11deda2c
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpec.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.bcrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+
+/**
+ * bcrypt key derivation function parameter specification
+ */
+public class BcryptDerivedKeyParameterSpec implements DerivedKeyParameterSpec {
+ private final int cost;
+
+ private final byte[] salt;
+
+ /**
+ * bcrypt Parameter Specification constructor with required properties
+ *
+ * @param cost Cost parameter
+ * @param salt Array of random salt bytes
+ */
+ public BcryptDerivedKeyParameterSpec(
+ final int cost,
+ final byte[] salt
+ ) {
+ this.cost = cost;
+ this.salt = salt;
+ }
+
+ @Override
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getCost() {
+ return cost;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpecReader.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpecReader.java
new file mode 100644
index 0000000000..9ca69b4bef
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpecReader.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.bcrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpecReader;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * bcrypt implementation reads a US-ASCII string of bytes using the Modular Crypt Format specification as added in NiFi 0.5.0
+ */
+public class BcryptDerivedKeyParameterSpecReader implements DerivedKeyParameterSpecReader {
+ /** Modular Crypt Format containing the cost as a zero-padded number with a trailing bcrypt-Base64 encoded 16 byte salt and optional hash */
+ private static final Pattern MODULAR_CRYPT_FORMAT = Pattern.compile("^\\$2[abxy]\\$(\\d{2})\\$([\\w/.]{22})([\\w/.]{31})?$");
+
+ private static final int COST_GROUP = 1;
+
+ private static final int SALT_GROUP = 2;
+
+ private static final Charset PARAMETERS_CHARACTER_SET = StandardCharsets.US_ASCII;
+
+ /**
+ * Read serialized parameters parsed from US-ASCII string of bytes
+ *
+ * @param serializedParameters Serialized parameters
+ * @return bcrypt Parameter Specification
+ */
+ @Override
+ public BcryptDerivedKeyParameterSpec read(final byte[] serializedParameters) {
+ Objects.requireNonNull(serializedParameters, "Parameters required");
+ final String parameters = new String(serializedParameters, PARAMETERS_CHARACTER_SET);
+
+ final Matcher matcher = MODULAR_CRYPT_FORMAT.matcher(parameters);
+ if (matcher.matches()) {
+ final String costGroup = matcher.group(COST_GROUP);
+ final String saltGroup = matcher.group(SALT_GROUP);
+
+ final int cost = Integer.parseInt(costGroup);
+ final byte[] salt = BcryptBase64Decoder.decode(saltGroup);
+
+ return new BcryptDerivedKeyParameterSpec(cost, salt);
+ } else {
+ final String message = String.format("bcrypt serialized parameters [%s] format not matched", parameters);
+ throw new IllegalArgumentException(message);
+ }
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyProvider.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyProvider.java
new file mode 100644
index 0000000000..94ed1cda54
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyProvider.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.bcrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.DerivedSecretKey;
+import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * bcrypt implementation of Derived Key Provider based on Bouncy Castle bcrypt components with SHA-512 digest for derived key
+ */
+public class BcryptDerivedKeyProvider implements DerivedKeyProvider {
+ private static final Charset SERIALIZED_CHARACTER_SET = StandardCharsets.US_ASCII;
+
+ private static final int SERIALIZED_HASH_START_INDEX = 29;
+
+ private static final String DIGEST_ALGORITHM = "SHA-512";
+
+ private static final String BCRYPT_VERSION = "2a";
+
+ /**
+ * Get Derived Key using bcrypt version 2a and provided specification with SHA-512 digest of encoded raw hash bytes
+ *
+ * @param derivedKeySpec Derived Key Specification
+ * @return Derived Secret Key
+ */
+ @Override
+ public DerivedKey getDerivedKey(final DerivedKeySpec derivedKeySpec) {
+ final String serialized = getHashMessage(derivedKeySpec);
+ final byte[] hashMessage = serialized.getBytes(SERIALIZED_CHARACTER_SET);
+
+ final byte[] encodedRawHash = Arrays.copyOfRange(hashMessage, SERIALIZED_HASH_START_INDEX, hashMessage.length);
+ final byte[] derivedKeyBytes = getDerivedKeyBytes(encodedRawHash, derivedKeySpec.getDerivedKeyLength());
+ return new DerivedSecretKey(derivedKeyBytes, derivedKeySpec.getAlgorithm(), serialized);
+ }
+
+ private String getHashMessage(final DerivedKeySpec derivedKeySpec) {
+ final BcryptDerivedKeyParameterSpec parameterSpec = derivedKeySpec.getParameterSpec();
+
+ final int cost = parameterSpec.getCost();
+ final byte[] salt = parameterSpec.getSalt();
+
+ final char[] password = derivedKeySpec.getPassword();
+
+ return OpenBSDBCrypt.generate(BCRYPT_VERSION, password, salt, cost);
+ }
+
+ private byte[] getDerivedKeyBytes(final byte[] hash, final int derivedKeyLength) {
+ final MessageDigest messageDigest = getMessageDigest();
+ final byte[] digested = messageDigest.digest(hash);
+ return Arrays.copyOf(digested, derivedKeyLength);
+ }
+
+ private MessageDigest getMessageDigest() {
+ try {
+ return MessageDigest.getInstance(DIGEST_ALGORITHM);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(DIGEST_ALGORITHM, e);
+ }
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyParameterSpecReader.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyParameterSpecReader.java
new file mode 100644
index 0000000000..85c573a3ce
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyParameterSpecReader.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.detection;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpecReader;
+import org.apache.nifi.security.crypto.key.argon2.Argon2DerivedKeyParameterSpecReader;
+import org.apache.nifi.security.crypto.key.bcrypt.BcryptDerivedKeyParameterSpecReader;
+import org.apache.nifi.security.crypto.key.io.ByteBufferSearch;
+import org.apache.nifi.security.crypto.key.pbkdf2.Pbkdf2DerivedKeyParameterSpecReader;
+import org.apache.nifi.security.crypto.key.scrypt.ScryptDerivedKeyParameterSpecReader;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Delegating implementation capable of selecting a reader based on detected header bytes of serialized parameters
+ */
+public class DetectedDerivedKeyParameterSpecReader implements DerivedKeyParameterSpecReader {
+ /** Argon2id header as implemented in NiFi 1.12.0 */
+ private static final byte[] ARGON2_ID_DELIMITER = {'$', 'a', 'r', 'g', 'o', 'n', '2', 'i', 'd', '$'};
+
+ /** bcrypt 2a header as implemented in NiFi 0.5.0 */
+ private static final byte[] BCRYPT_2A_DELIMITER = {'$', '2', 'a', '$'};
+
+ /** scrypt header as implemented in NiFi 0.5.0 */
+ private static final byte[] SCRYPT_DELIMITER = {'$', 's', '0', '$'};
+
+ private static final Argon2DerivedKeyParameterSpecReader argon2Reader = new Argon2DerivedKeyParameterSpecReader();
+
+ private static final BcryptDerivedKeyParameterSpecReader bcryptReader = new BcryptDerivedKeyParameterSpecReader();
+
+ private static final ScryptDerivedKeyParameterSpecReader scryptReader = new ScryptDerivedKeyParameterSpecReader();
+
+ private static final Pbkdf2DerivedKeyParameterSpecReader pbkdf2Reader = new Pbkdf2DerivedKeyParameterSpecReader();
+
+ /**
+ * Read Parameter Specification selects a Reader based on header bytes defaulting to PBKDF2 in absence of a matched pattern
+ *
+ * @param serializedParameters Serialized parameters
+ * @return Derived Key Parameter Specification read from serialized parameters
+ */
+ @Override
+ public DerivedKeyParameterSpec read(final byte[] serializedParameters) {
+ final ByteBuffer buffer = ByteBuffer.wrap(serializedParameters);
+ final DerivedKeyParameterSpecReader extends DerivedKeyParameterSpec> reader = getReader(buffer);
+ return reader.read(serializedParameters);
+ }
+
+ private DerivedKeyParameterSpecReader extends DerivedKeyParameterSpec> getReader(final ByteBuffer buffer) {
+ final DerivedKeyParameterSpecReader extends DerivedKeyParameterSpec> reader;
+ if (ByteBufferSearch.indexOf(buffer, ARGON2_ID_DELIMITER) == 0) {
+ reader = argon2Reader;
+ } else if (ByteBufferSearch.indexOf(buffer, BCRYPT_2A_DELIMITER) == 0) {
+ reader = bcryptReader;
+ } else if (ByteBufferSearch.indexOf(buffer, SCRYPT_DELIMITER) == 0) {
+ reader = scryptReader;
+ } else {
+ reader = pbkdf2Reader;
+ }
+ return reader;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyProvider.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyProvider.java
new file mode 100644
index 0000000000..d60c13eda4
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.detection;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.argon2.Argon2DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.argon2.Argon2DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.bcrypt.BcryptDerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.bcrypt.BcryptDerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.pbkdf2.Pbkdf2DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.pbkdf2.Pbkdf2DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.scrypt.ScryptDerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.scrypt.ScryptDerivedKeyProvider;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provider delegating to configured implementations based on Parameter Specification class
+ */
+public class DetectedDerivedKeyProvider implements DerivedKeyProvider {
+ private static final Map, DerivedKeyProvider extends DerivedKeyParameterSpec>> providers = new LinkedHashMap<>();
+
+ static {
+ providers.put(Argon2DerivedKeyParameterSpec.class, new Argon2DerivedKeyProvider());
+ providers.put(BcryptDerivedKeyParameterSpec.class, new BcryptDerivedKeyProvider());
+ providers.put(ScryptDerivedKeyParameterSpec.class, new ScryptDerivedKeyProvider());
+ providers.put(Pbkdf2DerivedKeyParameterSpec.class, new Pbkdf2DerivedKeyProvider());
+ }
+
+ /**
+ * Get Derived Key using implementation selected based on assignable Parameter Specification class mapped to Provider
+ *
+ * @param derivedKeySpec Derived Key Specification
+ * @return Derived Key
+ */
+ @Override
+ public DerivedKey getDerivedKey(final DerivedKeySpec derivedKeySpec) {
+ Objects.requireNonNull(derivedKeySpec, "Specification required");
+
+ final Class extends DerivedKeyParameterSpec> parameterSpecClass = derivedKeySpec.getParameterSpec().getClass();
+ final DerivedKeyProvider derivedKeyProvider = findProvider(parameterSpecClass);
+
+ return derivedKeyProvider.getDerivedKey(derivedKeySpec);
+ }
+
+ @SuppressWarnings("unchecked")
+ private DerivedKeyProvider findProvider(final Class extends DerivedKeyParameterSpec> parameterSpecClass) {
+ final Class extends DerivedKeyParameterSpec> foundSpecClass = providers.keySet()
+ .stream()
+ .filter(specClass -> specClass.isAssignableFrom(parameterSpecClass))
+ .findFirst()
+ .orElseThrow(() -> new UnsupportedOperationException(String.format("Parameter Specification [%s] not supported", parameterSpecClass)));
+
+ final DerivedKeyProvider extends DerivedKeyParameterSpec> derivedKeyProvider = providers.get(foundSpecClass);
+ return (DerivedKeyProvider) derivedKeyProvider;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/io/ByteBufferSearch.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/io/ByteBufferSearch.java
new file mode 100644
index 0000000000..512adb9015
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/io/ByteBufferSearch.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.io;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Byte Buffer Search utilities
+ */
+public class ByteBufferSearch {
+
+ private static final int END_OF_FILE = -1;
+
+ /**
+ * Get starting index of delimiter in buffer
+ *
+ * @param buffer Byte Buffer to be searched
+ * @param delimiter Delimiter
+ * @return Starting index of delimiter or -1 when not found
+ */
+ public static int indexOf(final ByteBuffer buffer, final byte[] delimiter) {
+ final int bufferSearchLength = buffer.limit() - delimiter.length + 1;
+ bufferSearch:
+ for (int i = 0; i < bufferSearchLength; i++) {
+ for (int j = 0; j < delimiter.length; j++) {
+ final int bufferIndex = i + j;
+ final byte indexByte = buffer.get(bufferIndex);
+ final byte delimiterByte = delimiter[j];
+ if (indexByte != delimiterByte) {
+ continue bufferSearch;
+ }
+ }
+ return i;
+ }
+ return END_OF_FILE;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpec.java
new file mode 100644
index 0000000000..02eaa4c48e
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpec.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.pbkdf2;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+
+/**
+ * PBKDF2 key derivation function parameter specification
+ */
+public class Pbkdf2DerivedKeyParameterSpec implements DerivedKeyParameterSpec {
+ private final int iterations;
+
+ private final byte[] salt;
+
+ /**
+ * PBKDF2 Parameter Specification constructor with required properties
+ *
+ * @param iterations Cost parameter
+ * @param salt Array of random salt bytes
+ */
+ public Pbkdf2DerivedKeyParameterSpec(
+ final int iterations,
+ final byte[] salt
+ ) {
+ this.iterations = iterations;
+ this.salt = salt;
+ }
+
+ @Override
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getIterations() {
+ return iterations;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpecReader.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpecReader.java
new file mode 100644
index 0000000000..812ef4e5f7
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpecReader.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.pbkdf2;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpecReader;
+
+import java.util.Objects;
+
+/**
+ * PBKDF2 implementation uses the serialized parameters as the salt with a hard-coded number of iterations as defined in NiFi 0.5.0
+ */
+public class Pbkdf2DerivedKeyParameterSpecReader implements DerivedKeyParameterSpecReader {
+ protected static final int VERSION_0_5_0_ITERATIONS = 160000;
+
+ /**
+ * Read serialized parameters and return as salt bytes with 160,000 iterations as defined in NiFi 0.5.0
+ *
+ * @param serializedParameters Serialized parameters
+ * @return PBKDF2 Parameter Specification
+ */
+ @Override
+ public Pbkdf2DerivedKeyParameterSpec read(final byte[] serializedParameters) {
+ Objects.requireNonNull(serializedParameters, "Parameters required");
+ return new Pbkdf2DerivedKeyParameterSpec(VERSION_0_5_0_ITERATIONS, serializedParameters);
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyProvider.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyProvider.java
new file mode 100644
index 0000000000..67a7827d9e
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.pbkdf2;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.DerivedSecretKey;
+import org.bouncycastle.crypto.CipherParameters;
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.SHA512Digest;
+import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * PBKDF2 implementation of Derived Key Provider based on Bouncy Castle components with HMAC SHA-512 pseudorandom function
+ */
+public class Pbkdf2DerivedKeyProvider implements DerivedKeyProvider {
+ private static final Charset PASSWORD_CHARACTER_SET = StandardCharsets.UTF_8;
+
+ private static final int BITS = 8;
+
+ private static final Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
+
+ /**
+ * Get Derived Key using PBKDF2 with HMAC SHA-512 and provided specification
+ *
+ * @param derivedKeySpec Derived Key Specification
+ * @return Derived Secret Key
+ */
+ @Override
+ public DerivedKey getDerivedKey(final DerivedKeySpec derivedKeySpec) {
+ final byte[] derivedKeyBytes = getDerivedKeyBytes(derivedKeySpec);
+ final String serialized = encoder.encodeToString(derivedKeyBytes);
+ return new DerivedSecretKey(derivedKeyBytes, derivedKeySpec.getAlgorithm(), serialized);
+ }
+
+ private byte[] getDerivedKeyBytes(final DerivedKeySpec derivedKeySpec) {
+ final Digest digest = new SHA512Digest();
+ final PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator(digest);
+
+ final byte[] password = new String(derivedKeySpec.getPassword()).getBytes(PASSWORD_CHARACTER_SET);
+ final Pbkdf2DerivedKeyParameterSpec parameterSpec = derivedKeySpec.getParameterSpec();
+ final byte[] salt = parameterSpec.getSalt();
+ final int iterations = parameterSpec.getIterations();
+ generator.init(password, salt, iterations);
+
+ final int derivedKeyLengthBits = derivedKeySpec.getDerivedKeyLength() * BITS;
+ final CipherParameters cipherParameters = generator.generateDerivedParameters(derivedKeyLengthBits);
+ final KeyParameter keyParameter = (KeyParameter) cipherParameters;
+ return keyParameter.getKey();
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpec.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpec.java
new file mode 100644
index 0000000000..b0f0d8f2f7
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpec.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.scrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+
+/**
+ * scrypt key derivation function parameter specification
+ */
+public class ScryptDerivedKeyParameterSpec implements DerivedKeyParameterSpec {
+ private final int cost;
+
+ private final int blockSize;
+
+ private final int parallelization;
+
+ private final byte[] salt;
+
+ /**
+ * scrypt Parameter Specification constructor with required properties
+ *
+ * @param cost CPU and memory cost parameter
+ * @param blockSize Block size parameter
+ * @param parallelization Parallelization parameter
+ * @param salt Array of random salt bytes
+ */
+ public ScryptDerivedKeyParameterSpec(
+ final int cost,
+ final int blockSize,
+ final int parallelization,
+ final byte[] salt
+ ) {
+ this.cost = cost;
+ this.blockSize = blockSize;
+ this.parallelization = parallelization;
+ this.salt = salt;
+ }
+
+ @Override
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getCost() {
+ return cost;
+ }
+
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ public int getParallelization() {
+ return parallelization;
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpecReader.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpecReader.java
new file mode 100644
index 0000000000..a59522b292
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpecReader.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.scrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpecReader;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * scrypt implementation reads a US-ASCII string of bytes using a Modular Crypt Format defined according to the com.lambdaworks:scrypt library
+ */
+public class ScryptDerivedKeyParameterSpecReader implements DerivedKeyParameterSpecReader {
+ /** Modular Crypt Format containing the parameters encoded as a 32-bit hexadecimal number with a trailing Base64 encoded salt and optional hash */
+ private static final Pattern MODULAR_CRYPT_FORMAT = Pattern.compile("^\\$s0\\$([a-f0-9]{5,})\\$([\\w/=+]{11,64})\\$?([\\w/=+]{1,256})?$");
+
+ private static final int PARAMETERS_GROUP = 1;
+
+ private static final int SALT_GROUP = 2;
+
+ private static final Charset PARAMETERS_CHARACTER_SET = StandardCharsets.US_ASCII;
+
+ private static final int HEXADECIMAL_RADIX = 16;
+
+ private static final int LOG_BASE_2 = 2;
+
+ private static final int COST_BITS = 16;
+
+ private static final int SIZE_BITS = 8;
+
+ private static final int SIXTEEN_BIT_SHIFT = 0xffff;
+
+ private static final int EIGHT_BIT_SHIFT = 0xff;
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ @Override
+ public ScryptDerivedKeyParameterSpec read(final byte[] serializedParameters) {
+ Objects.requireNonNull(serializedParameters, "Parameters required");
+ final String parameters = new String(serializedParameters, PARAMETERS_CHARACTER_SET);
+
+ final Matcher matcher = MODULAR_CRYPT_FORMAT.matcher(parameters);
+ if (matcher.matches()) {
+ final String parametersGroup = matcher.group(PARAMETERS_GROUP);
+ final String saltGroup = matcher.group(SALT_GROUP);
+ return readParameters(parametersGroup, saltGroup);
+ } else {
+ final String message = String.format("scrypt serialized parameters [%s] format not matched", parameters);
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ private ScryptDerivedKeyParameterSpec readParameters(final String parametersEncoded, final String saltEncoded) {
+ final long parameters = Long.parseLong(parametersEncoded, HEXADECIMAL_RADIX);
+ final long costExponent = parameters >> COST_BITS & SIXTEEN_BIT_SHIFT;
+
+ final int cost = (int) Math.pow(LOG_BASE_2, costExponent);
+ final int blockSize = (int) parameters >> SIZE_BITS & EIGHT_BIT_SHIFT;
+ final int parallelization = (int) parameters & EIGHT_BIT_SHIFT;
+
+ final byte[] salt = decoder.decode(saltEncoded);
+ return new ScryptDerivedKeyParameterSpec(cost, blockSize, parallelization, salt);
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyProvider.java b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyProvider.java
new file mode 100644
index 0000000000..46dd20a731
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/main/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyProvider.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.scrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.DerivedSecretKey;
+import org.bouncycastle.crypto.generators.SCrypt;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * scrypt implementation of Derived Key Provider based on Bouncy Castle scrypt components described in RFC 7914
+ */
+public class ScryptDerivedKeyProvider implements DerivedKeyProvider {
+ private static final String SERIALIZED_FORMAT = "$s0$%s$%s$%s";
+
+ private static final int HEXADECIMAL_RADIX = 16;
+
+ private static final int LOG_BASE_2 = 2;
+
+ private static final int COST_BITS = 16;
+
+ private static final int SIZE_BITS = 8;
+
+ private static final Charset PASSWORD_CHARACTER_SET = StandardCharsets.UTF_8;
+
+ private static final Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
+
+ /**
+ * Get Derived Key using scrypt and provided specification
+ *
+ * @param derivedKeySpec Derived Key Specification
+ * @return Derived Secret Key
+ */
+ @Override
+ public DerivedKey getDerivedKey(final DerivedKeySpec derivedKeySpec) {
+ final byte[] password = new String(derivedKeySpec.getPassword()).getBytes(PASSWORD_CHARACTER_SET);
+
+ final ScryptDerivedKeyParameterSpec parameterSpec = derivedKeySpec.getParameterSpec();
+ final byte[] salt = parameterSpec.getSalt();
+ final int cost = parameterSpec.getCost();
+ final int blockSize = parameterSpec.getBlockSize();
+ final int parallelization = parameterSpec.getParallelization();
+ final int derivedKeyLength = derivedKeySpec.getDerivedKeyLength();
+
+ final byte[] derivedKeyBytes = SCrypt.generate(password, salt, cost, blockSize, parallelization, derivedKeyLength);
+
+ final String serialized = getSerialized(derivedKeyBytes, parameterSpec);
+ return new DerivedSecretKey(derivedKeyBytes, derivedKeySpec.getAlgorithm(), serialized);
+ }
+
+ private String getSerialized(final byte[] derivedKeyBytes, final ScryptDerivedKeyParameterSpec parameterSpec) {
+ final String parametersEncoded = getParametersEncoded(parameterSpec);
+ final String derivedKeyEncoded = encoder.encodeToString(derivedKeyBytes);
+ final String saltEncoded = encoder.encodeToString(parameterSpec.getSalt());
+ return String.format(
+ SERIALIZED_FORMAT,
+ parametersEncoded,
+ saltEncoded,
+ derivedKeyEncoded
+ );
+ }
+
+ private String getParametersEncoded(final ScryptDerivedKeyParameterSpec parameterSpec) {
+ final long cost = log2(parameterSpec.getCost()) << COST_BITS;
+ final long blockSize = parameterSpec.getBlockSize() << SIZE_BITS;
+ final long parallelization = parameterSpec.getParallelization();
+ final long parameters = cost | blockSize | parallelization;
+ return Long.toString(parameters, HEXADECIMAL_RADIX);
+ }
+
+ private long log2(final int number) {
+ final double log = Math.log(number);
+ final double logBase2 = Math.log(LOG_BASE_2);
+ final double log2 = log / logBase2;
+ return Math.round(log2);
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpecReaderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpecReaderTest.java
new file mode 100644
index 0000000000..7da2bdaa99
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyParameterSpecReaderTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.argon2;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class Argon2DerivedKeyParameterSpecReaderTest {
+ private static final byte[] STRING_PARAMETERS = String.class.getSimpleName().getBytes(StandardCharsets.US_ASCII);
+
+ private static final int MEMORY = 65536;
+
+ private static final int ITERATIONS = 2;
+
+ private static final int PARALLELISM = 1;
+
+ private static final String ARGON2_SALT_STRING = "QXJnb24yU2FsdFN0cmluZw";
+
+ private static final String PARAMETERS = String.format("$argon2id$v=19$m=%d,t=%d,p=%d$%s", MEMORY, ITERATIONS, PARALLELISM, ARGON2_SALT_STRING);
+
+ private static final String PARAMETERS_HASH = String.format("%s$6LOmoOXYJV0tXBJtxtD1Mg", PARAMETERS);
+
+ Base64.Decoder decoder = Base64.getDecoder();
+
+ Argon2DerivedKeyParameterSpecReader reader;
+
+ @BeforeEach
+ void setReader() {
+ reader = new Argon2DerivedKeyParameterSpecReader();
+ }
+
+ @Test
+ void testReadException() {
+ assertThrows(IllegalArgumentException.class, () -> reader.read(STRING_PARAMETERS));
+ }
+
+ @Test
+ void testRead() {
+ final byte[] serializedParameters = PARAMETERS.getBytes(StandardCharsets.US_ASCII);
+
+ final Argon2DerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertParameterSpecEquals(parameterSpec);
+ }
+
+ @Test
+ void testReadHash() {
+ final byte[] serializedParameters = PARAMETERS_HASH.getBytes(StandardCharsets.US_ASCII);
+
+ final Argon2DerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertParameterSpecEquals(parameterSpec);
+ }
+
+ private void assertParameterSpecEquals(final Argon2DerivedKeyParameterSpec parameterSpec) {
+ assertNotNull(parameterSpec);
+ assertEquals(MEMORY, parameterSpec.getMemory());
+ assertEquals(ITERATIONS, parameterSpec.getIterations());
+ assertEquals(PARALLELISM, parameterSpec.getParallelism());
+
+ final byte[] salt = decoder.decode(ARGON2_SALT_STRING);
+ assertArrayEquals(salt, parameterSpec.getSalt());
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyProviderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyProviderTest.java
new file mode 100644
index 0000000000..9ed86b070f
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/argon2/Argon2DerivedKeyProviderTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.argon2;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.StandardDerivedKeySpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class Argon2DerivedKeyProviderTest {
+ private static final char[] PASSWORD = String.class.getSimpleName().toCharArray();
+
+ private static final int DERIVED_KEY_LENGTH = 16;
+
+ private static final String ALGORITHM = "AES";
+
+ private static final int MEMORY = 65536;
+
+ private static final int ITERATIONS = 2;
+
+ private static final int PARALLELISM = 1;
+
+ private static final String ARGON2_SALT_STRING = "QXJnb24yU2FsdFN0cmluZw";
+
+ private static final String PARAMETERS = String.format("$argon2id$v=19$m=%d,t=%d,p=%d$%s", MEMORY, ITERATIONS, PARALLELISM, ARGON2_SALT_STRING);
+
+ private static final String HASH = "6LOmoOXYJV0tXBJtxtD1Mg";
+
+ private static final String SERIALIZED = String.format("%s$%s", PARAMETERS, HASH);
+
+ Base64.Decoder decoder = Base64.getDecoder();
+
+ Argon2DerivedKeyProvider provider;
+
+ @BeforeEach
+ void setReader() {
+ provider = new Argon2DerivedKeyProvider();
+ }
+
+ @Test
+ void testGetDerivedKey() {
+ final byte[] salt = decoder.decode(ARGON2_SALT_STRING);
+ final Argon2DerivedKeyParameterSpec parameterSpec = new Argon2DerivedKeyParameterSpec(MEMORY, ITERATIONS, PARALLELISM, salt);
+ final DerivedKeySpec derivedKeySpec = new StandardDerivedKeySpec<>(
+ PASSWORD,
+ DERIVED_KEY_LENGTH,
+ ALGORITHM,
+ parameterSpec
+ );
+
+ final DerivedKey derivedKey = provider.getDerivedKey(derivedKeySpec);
+
+ assertNotNull(derivedKey);
+ assertEquals(ALGORITHM, derivedKey.getAlgorithm());
+ assertEquals(SERIALIZED, derivedKey.getSerialized());
+
+ final byte[] hashBytes = decoder.decode(HASH);
+ assertArrayEquals(hashBytes, derivedKey.getEncoded());
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpecReaderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpecReaderTest.java
new file mode 100644
index 0000000000..3241fe521f
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyParameterSpecReaderTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.bcrypt;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class BcryptDerivedKeyParameterSpecReaderTest {
+ private static final byte[] STRING_PARAMETERS = String.class.getSimpleName().getBytes(StandardCharsets.US_ASCII);
+
+ private static final int COST = 12;
+
+ private static final String SALT_ENCODED = "R9h/cIPz0gi.URNNX3kh2O";
+
+ private static final String HASH = "PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW";
+
+ private static final String SERIALIZED = String.format("$2a$%d$%s", COST, SALT_ENCODED);
+
+ private static final String SERIALIZED_HASH = String.format("%s%s", SERIALIZED, HASH);
+
+ BcryptDerivedKeyParameterSpecReader reader;
+
+ @BeforeEach
+ void setReader() {
+ reader = new BcryptDerivedKeyParameterSpecReader();
+ }
+
+ @Test
+ void testReadException() {
+ assertThrows(IllegalArgumentException.class, () -> reader.read(STRING_PARAMETERS));
+ }
+
+ @Test
+ void testRead() {
+ final byte[] serializedParameters = SERIALIZED.getBytes(StandardCharsets.US_ASCII);
+
+ final BcryptDerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertParameterSpecEquals(parameterSpec);
+ }
+
+ @Test
+ void testReadHash() {
+ final byte[] serializedParameters = SERIALIZED_HASH.getBytes(StandardCharsets.US_ASCII);
+
+ final BcryptDerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertParameterSpecEquals(parameterSpec);
+ }
+
+ private void assertParameterSpecEquals(final BcryptDerivedKeyParameterSpec parameterSpec) {
+ assertNotNull(parameterSpec);
+ assertEquals(COST, parameterSpec.getCost());
+
+ final byte[] salt = BcryptBase64Decoder.decode(SALT_ENCODED);
+ assertArrayEquals(salt, parameterSpec.getSalt());
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyProviderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyProviderTest.java
new file mode 100644
index 0000000000..225c0872aa
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/bcrypt/BcryptDerivedKeyProviderTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.bcrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.StandardDerivedKeySpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class BcryptDerivedKeyProviderTest {
+ private static final String PASSWORD = "abc123xyz";
+
+ private static final int COST = 12;
+
+ private static final String SALT_ENCODED = "R9h/cIPz0gi.URNNX3kh2O";
+
+ private static final String HASH = "PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW";
+
+ private static final String SERIALIZED = String.format("$2a$%d$%s%s", COST, SALT_ENCODED, HASH);
+
+ private static final Charset SERIALIZED_CHARACTER_SET = StandardCharsets.US_ASCII;
+
+ private static final int DERIVED_KEY_LENGTH = 16;
+
+ private static final String ALGORITHM = "AES";
+
+ private static final String DIGEST_ALGORITHM = "SHA-512";
+
+ BcryptDerivedKeyProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new BcryptDerivedKeyProvider();
+ }
+
+ @Test
+ void testGetDerivedKey() {
+ final byte[] salt = BcryptBase64Decoder.decode(SALT_ENCODED);
+ final BcryptDerivedKeyParameterSpec parameterSpec = new BcryptDerivedKeyParameterSpec(COST, salt);
+
+ final DerivedKeySpec derivedKeySpec = new StandardDerivedKeySpec<>(
+ PASSWORD.toCharArray(),
+ DERIVED_KEY_LENGTH,
+ ALGORITHM,
+ parameterSpec
+ );
+
+ final DerivedKey derivedKey = provider.getDerivedKey(derivedKeySpec);
+
+ assertNotNull(derivedKey);
+ assertEquals(ALGORITHM, derivedKey.getAlgorithm());
+ assertEquals(SERIALIZED, derivedKey.getSerialized());
+
+ final byte[] encodedHashBytes = HASH.getBytes(SERIALIZED_CHARACTER_SET);
+ final byte[] digestedDerivedKey = getDerivedKeyBytes(encodedHashBytes);
+ assertArrayEquals(digestedDerivedKey, derivedKey.getEncoded());
+ }
+
+ private byte[] getDerivedKeyBytes(final byte[] hash) {
+ final MessageDigest messageDigest = getMessageDigest();
+ final byte[] digested = messageDigest.digest(hash);
+ return Arrays.copyOf(digested, DERIVED_KEY_LENGTH);
+ }
+
+ private MessageDigest getMessageDigest() {
+ try {
+ return MessageDigest.getInstance(DIGEST_ALGORITHM);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(DIGEST_ALGORITHM, e);
+ }
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyParameterSpecReaderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyParameterSpecReaderTest.java
new file mode 100644
index 0000000000..59323d4cae
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyParameterSpecReaderTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.detection;
+
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.argon2.Argon2DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.bcrypt.BcryptDerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.pbkdf2.Pbkdf2DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.scrypt.ScryptDerivedKeyParameterSpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+class DetectedDerivedKeyParameterSpecReaderTest {
+
+ private static final String UNSPECIFIED_PARAMETERS = "unspecified";
+
+ private static final String ARGON2_PARAMETERS = "$argon2id$v=19$m=65536,t=2,p=1$QXJnb24yU2FsdFN0cmluZw";
+
+ private static final String BCRYPT_PARAMETERS = "$2a$12$R9h/cIPz0gi.URNNX3kh2O";
+
+ private static final String SCRYPT_PARAMETERS = "$s0$e0801$epIxT/h6HbbwHaehFnh/bw";
+
+ DetectedDerivedKeyParameterSpecReader reader;
+
+ @BeforeEach
+ void setReader() {
+ reader = new DetectedDerivedKeyParameterSpecReader();
+ }
+
+ @Test
+ void testArgon2() {
+ assertParameterSpecInstanceOf(Argon2DerivedKeyParameterSpec.class, ARGON2_PARAMETERS);
+ }
+
+ @Test
+ void testBcrypt() {
+ assertParameterSpecInstanceOf(BcryptDerivedKeyParameterSpec.class, BCRYPT_PARAMETERS);
+ }
+
+ @Test
+ void testScrypt() {
+ assertParameterSpecInstanceOf(ScryptDerivedKeyParameterSpec.class, SCRYPT_PARAMETERS);
+ }
+
+ @Test
+ void testPbkdf2() {
+ assertParameterSpecInstanceOf(Pbkdf2DerivedKeyParameterSpec.class, UNSPECIFIED_PARAMETERS);
+ }
+
+ private void assertParameterSpecInstanceOf(final Class extends DerivedKeyParameterSpec> parameterSpecClass, final String parameters) {
+ final byte[] serializedParameters = parameters.getBytes(StandardCharsets.UTF_8);
+
+ final DerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertInstanceOf(parameterSpecClass, parameterSpec);
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyProviderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyProviderTest.java
new file mode 100644
index 0000000000..3f6b87eacd
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/detection/DetectedDerivedKeyProviderTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.detection;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.StandardDerivedKeySpec;
+import org.apache.nifi.security.crypto.key.argon2.Argon2DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.bcrypt.BcryptDerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.pbkdf2.Pbkdf2DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.scrypt.ScryptDerivedKeyParameterSpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ExtendWith(MockitoExtension.class)
+class DetectedDerivedKeyProviderTest {
+
+ private static final char[] PASSWORD = {'w', 'o', 'r', 'd'};
+
+ private static final int DERIVED_KEY_LENGTH = 16;
+
+ private static final String ALGORITHM = "AES";
+
+ private static final byte[] SALT = {'N', 'i', 'F', 'i', 'S', 'A', 'L', 'T'};
+
+ private static final int MINIMUM_ITERATIONS = 1;
+
+ private static final int MINIMUM_ARGON2_MEMORY = 2;
+
+ private static final int MINIMUM_PARALLELISM = 1;
+
+ private static final int MINIMUM_BCRYPT_COST = 4;
+
+ private static final byte[] BCRYPT_SALT = {'S', 'I', 'X', 'T', 'E', 'E', 'N', 'B', 'Y', 'T', 'E', 'S', 'S', 'A', 'L', 'T'};
+
+ private static final int MINIMUM_SCRYPT_COST = 2;
+
+ private static final int SCRYPT_BLOCK_SIZE = 16;
+
+ @Mock
+ DerivedKeyParameterSpec unsupportedDerivedKeyParameterSpec;
+
+ DetectedDerivedKeyProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new DetectedDerivedKeyProvider();
+ }
+
+ @Test
+ void testArgon2() {
+ final Argon2DerivedKeyParameterSpec parameterSpec = new Argon2DerivedKeyParameterSpec(MINIMUM_ARGON2_MEMORY, MINIMUM_ITERATIONS, MINIMUM_PARALLELISM, SALT);
+ assertDerivedKey(parameterSpec);
+ }
+
+ @Test
+ void testBcrypt() {
+ final BcryptDerivedKeyParameterSpec parameterSpec = new BcryptDerivedKeyParameterSpec(MINIMUM_BCRYPT_COST, BCRYPT_SALT);
+ assertDerivedKey(parameterSpec);
+ }
+
+ @Test
+ void testPbkdf2() {
+ final Pbkdf2DerivedKeyParameterSpec parameterSpec = new Pbkdf2DerivedKeyParameterSpec(MINIMUM_ITERATIONS, SALT);
+ assertDerivedKey(parameterSpec);
+ }
+
+ @Test
+ void testScrypt() {
+ final ScryptDerivedKeyParameterSpec parameterSpec = new ScryptDerivedKeyParameterSpec(MINIMUM_SCRYPT_COST, SCRYPT_BLOCK_SIZE, MINIMUM_PARALLELISM, SALT);
+ assertDerivedKey(parameterSpec);
+ }
+
+ @Test
+ void testUnsupported() {
+ final DerivedKeySpec derivedKeySpec = getDerivedKeySpec(unsupportedDerivedKeyParameterSpec);
+ assertThrows(UnsupportedOperationException.class, () -> provider.getDerivedKey(derivedKeySpec));
+ }
+
+ private DerivedKeySpec getDerivedKeySpec(final T parameterSpec) {
+ return new StandardDerivedKeySpec<>(PASSWORD, DERIVED_KEY_LENGTH, ALGORITHM, parameterSpec);
+ }
+
+ private void assertDerivedKey(final T parameterSpec) {
+ final DerivedKeySpec derivedKeySpec = getDerivedKeySpec(parameterSpec);
+
+ final DerivedKey derivedKey = provider.getDerivedKey(derivedKeySpec);
+
+ assertNotNull(derivedKey);
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpecReaderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpecReaderTest.java
new file mode 100644
index 0000000000..c88868dc1c
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyParameterSpecReaderTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.pbkdf2;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class Pbkdf2DerivedKeyParameterSpecReaderTest {
+ private static final String STRING_PARAMETERS = String.class.getName();
+
+ Pbkdf2DerivedKeyParameterSpecReader reader;
+
+ @BeforeEach
+ void setReader() {
+ reader = new Pbkdf2DerivedKeyParameterSpecReader();
+ }
+
+ @Test
+ void testRead() {
+ final byte[] serializedParameters = STRING_PARAMETERS.getBytes(StandardCharsets.US_ASCII);
+
+ final Pbkdf2DerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertNotNull(parameterSpec);
+ assertEquals(Pbkdf2DerivedKeyParameterSpecReader.VERSION_0_5_0_ITERATIONS, parameterSpec.getIterations());
+ assertArrayEquals(serializedParameters, parameterSpec.getSalt());
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyProviderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyProviderTest.java
new file mode 100644
index 0000000000..3c5fa01c97
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/pbkdf2/Pbkdf2DerivedKeyProviderTest.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.security.crypto.key.pbkdf2;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.StandardDerivedKeySpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class Pbkdf2DerivedKeyProviderTest {
+ private static final char[] PASSWORD = String.class.getSimpleName().toCharArray();
+
+ private static final byte[] SALT = String.class.getName().getBytes(StandardCharsets.UTF_8);
+
+ private static final int DERIVED_KEY_LENGTH = 16;
+
+ private static final String SERIALIZED = "yeoZx0KZR9IzcuaOWOJbFg";
+
+ private static final String ALGORITHM = "AES";
+
+ private static final int ITERATIONS = 160000;
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ Pbkdf2DerivedKeyProvider provider;
+
+ @BeforeEach
+ void setReader() {
+ provider = new Pbkdf2DerivedKeyProvider();
+ }
+
+ @Test
+ void testGetDerivedKey() {
+ final Pbkdf2DerivedKeyParameterSpec parameterSpec = new Pbkdf2DerivedKeyParameterSpec(ITERATIONS, SALT);
+ final DerivedKeySpec derivedKeySpec = new StandardDerivedKeySpec<>(
+ PASSWORD,
+ DERIVED_KEY_LENGTH,
+ ALGORITHM,
+ parameterSpec
+ );
+
+ final DerivedKey derivedKey = provider.getDerivedKey(derivedKeySpec);
+
+ assertNotNull(derivedKey);
+ assertEquals(ALGORITHM, derivedKey.getAlgorithm());
+ assertEquals(SERIALIZED, derivedKey.getSerialized());
+
+ final byte[] decoded = decoder.decode(SERIALIZED);
+ assertArrayEquals(decoded, derivedKey.getEncoded());
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpecReaderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpecReaderTest.java
new file mode 100644
index 0000000000..2c82d578b0
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyParameterSpecReaderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.scrypt;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ScryptDerivedKeyParameterSpecReaderTest {
+ private static final byte[] STRING_PARAMETERS = String.class.getSimpleName().getBytes(StandardCharsets.US_ASCII);
+
+ private static final int COST = 16384;
+
+ private static final int BLOCK_SIZE = 8;
+
+ private static final int PARALLELIZATION = 1;
+
+ private static final String SALT_ENCODED = "epIxT/h6HbbwHaehFnh/bw";
+
+ private static final String HASH = "7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0";
+
+ private static final String SERIALIZED = String.format("$s0$e0801$%s", SALT_ENCODED);
+
+ private static final String SERIALIZED_HASH = String.format("%s$%s", SERIALIZED, HASH);
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ ScryptDerivedKeyParameterSpecReader reader;
+
+ @BeforeEach
+ void setReader() {
+ reader = new ScryptDerivedKeyParameterSpecReader();
+ }
+
+ @Test
+ void testReadException() {
+ assertThrows(IllegalArgumentException.class, () -> reader.read(STRING_PARAMETERS));
+ }
+
+ @Test
+ void testRead() {
+ final byte[] serializedParameters = SERIALIZED.getBytes(StandardCharsets.US_ASCII);
+
+ final ScryptDerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertParameterSpecEquals(parameterSpec);
+ }
+
+ @Test
+ void testReadHash() {
+ final byte[] serializedParameters = SERIALIZED_HASH.getBytes(StandardCharsets.US_ASCII);
+
+ final ScryptDerivedKeyParameterSpec parameterSpec = reader.read(serializedParameters);
+
+ assertParameterSpecEquals(parameterSpec);
+ }
+
+ private void assertParameterSpecEquals(final ScryptDerivedKeyParameterSpec parameterSpec) {
+ assertNotNull(parameterSpec);
+ assertEquals(COST, parameterSpec.getCost());
+ assertEquals(BLOCK_SIZE, parameterSpec.getBlockSize());
+ assertEquals(PARALLELIZATION, parameterSpec.getParallelization());
+
+ final byte[] salt = decoder.decode(SALT_ENCODED);
+ assertArrayEquals(salt, parameterSpec.getSalt());
+ }
+}
diff --git a/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyProviderTest.java b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyProviderTest.java
new file mode 100644
index 0000000000..94f846111c
--- /dev/null
+++ b/nifi-commons/nifi-security-crypto-key/src/test/java/org/apache/nifi/security/crypto/key/scrypt/ScryptDerivedKeyProviderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.crypto.key.scrypt;
+
+import org.apache.nifi.security.crypto.key.DerivedKey;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.StandardDerivedKeySpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class ScryptDerivedKeyProviderTest {
+ private static final String PASSWORD = "secret";
+
+ private static final int COST = 16384;
+
+ private static final int BLOCK_SIZE = 8;
+
+ private static final int PARALLELIZATION = 1;
+
+ private static final String SALT_ENCODED = "epIxT/h6HbbwHaehFnh/bw";
+
+ private static final String HASH = "7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0";
+
+ private static final String SERIALIZED = String.format("$s0$e0801$%s$%s", SALT_ENCODED, HASH);
+
+ private static final int DERIVED_KEY_LENGTH = 32;
+
+ private static final String ALGORITHM = "AES";
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ ScryptDerivedKeyProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new ScryptDerivedKeyProvider();
+ }
+
+ @Test
+ void testGetDerivedKey() {
+ final byte[] salt = decoder.decode(SALT_ENCODED);
+ final ScryptDerivedKeyParameterSpec parameterSpec = new ScryptDerivedKeyParameterSpec(COST, BLOCK_SIZE, PARALLELIZATION, salt);
+
+ final DerivedKeySpec derivedKeySpec = new StandardDerivedKeySpec<>(
+ PASSWORD.toCharArray(),
+ DERIVED_KEY_LENGTH,
+ ALGORITHM,
+ parameterSpec
+ );
+
+ final DerivedKey derivedKey = provider.getDerivedKey(derivedKeySpec);
+
+ assertNotNull(derivedKey);
+ assertEquals(ALGORITHM, derivedKey.getAlgorithm());
+ assertEquals(SERIALIZED, derivedKey.getSerialized());
+
+ final byte[] hashBytes = decoder.decode(HASH);
+ assertArrayEquals(hashBytes, derivedKey.getEncoded());
+ }
+}
diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml
index 4e8ad42c0a..8a212b5b16 100644
--- a/nifi-commons/pom.xml
+++ b/nifi-commons/pom.xml
@@ -53,6 +53,7 @@
nifi-record-pathnifi-repository-encryptionnifi-schema-utils
+ nifi-security-crypto-keynifi-security-kerberos-apinifi-security-kerberosnifi-security-kms
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-nar/pom.xml b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-nar/pom.xml
new file mode 100644
index 0000000000..7df58d8e9e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-nar/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.nifi
+ nifi-cipher-bundle
+ 1.20.0-SNAPSHOT
+
+
+ nifi-cipher-nar
+ nar
+
+ true
+ true
+
+
+
+
+ org.apache.nifi
+ nifi-cipher-processors
+ 1.20.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-standard-services-api-nar
+ 1.20.0-SNAPSHOT
+ nar
+
+
+
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/pom.xml b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/pom.xml
new file mode 100644
index 0000000000..a06a318a7f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.nifi
+ nifi-cipher-bundle
+ 1.20.0-SNAPSHOT
+
+
+ nifi-cipher-processors
+
+
+
+ org.apache.nifi
+ nifi-api
+
+
+ org.apache.nifi
+ nifi-utils
+ 1.20.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-security-crypto-key
+ 1.20.0-SNAPSHOT
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+
+
+ org.apache.nifi
+ nifi-mock
+
+
+
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/CipherAttributeKey.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/CipherAttributeKey.java
new file mode 100644
index 0000000000..486bec300d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/CipherAttributeKey.java
@@ -0,0 +1,29 @@
+/*
+ * 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.cipher;
+
+/**
+ * Cipher FlowFile Attribute Key
+ */
+class CipherAttributeKey {
+
+ static final String PBE_SCHEME = "pbe.scheme";
+
+ static final String PBE_SYMMETRIC_CIPHER = "pbe.symmetric.cipher";
+
+ static final String PBE_DIGEST_ALGORITHM = "pbe.digest.algorithm";
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/CipherException.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/CipherException.java
new file mode 100644
index 0000000000..9aa2c95691
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/CipherException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cipher;
+
+/**
+ * Cipher Exception for categorizing cryptographic failures
+ */
+public class CipherException extends RuntimeException {
+ /**
+ * Cipher Exception constructor with message and cause
+ *
+ * @param message Exception message
+ * @param cause Exception cause
+ */
+ public CipherException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/DecryptContent.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/DecryptContent.java
new file mode 100644
index 0000000000..23057f3719
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/DecryptContent.java
@@ -0,0 +1,364 @@
+/*
+ * 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.cipher;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processors.cipher.algorithm.CipherAlgorithmMode;
+import org.apache.nifi.processors.cipher.algorithm.CipherAlgorithmPadding;
+import org.apache.nifi.processors.cipher.algorithm.SymmetricCipher;
+import org.apache.nifi.processors.cipher.encoded.EncodedDelimiter;
+import org.apache.nifi.processors.cipher.encoded.KeySpecificationFormat;
+import org.apache.nifi.processors.cipher.io.DecryptStreamCallback;
+import org.apache.nifi.security.crypto.key.DerivedKeyParameterSpec;
+import org.apache.nifi.security.crypto.key.DerivedKeySpec;
+import org.apache.nifi.security.crypto.key.StandardDerivedKeySpec;
+import org.apache.nifi.security.crypto.key.detection.DetectedDerivedKeyParameterSpecReader;
+import org.apache.nifi.security.crypto.key.detection.DetectedDerivedKeyProvider;
+import org.apache.nifi.security.crypto.key.io.ByteBufferSearch;
+import org.bouncycastle.util.encoders.Hex;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@SideEffectFree
+@SupportsBatching
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"cryptography", "decipher", "decrypt", "AES", "Argon2", "bcrypt", "scrypt", "PBKDF2"})
+@CapabilityDescription("Decrypt content encrypted with AES and encoded according conventions added in NiFi 0.5.0 for the EncryptContent Processor. " +
+ "The Processor reads the first 256 bytes to determine the presence of a cryptographic salt based on finding the 'NiFiSALT' delimiter. " +
+ "The salt is not present for content encrypted with a raw hexadecimal key. " +
+ "The Processor determines the presence of the initialization vector based on finding the 'NiFiIV' delimiter." +
+ "The salt format indicates the Key Derivation Function that the Processor uses to generate a secret key based on a configured password. " +
+ "The Processor derives keys with a size of 128 bits according to the conventions implemented in NiFi 0.5.0."
+)
+public class DecryptContent extends AbstractProcessor {
+
+ static final PropertyDescriptor CIPHER_ALGORITHM_MODE = new PropertyDescriptor.Builder()
+ .name("cipher-algorithm-mode")
+ .displayName("Cipher Algorithm Mode")
+ .description("Block cipher mode of operation for decryption using the Advanced Encryption Standard")
+ .required(true)
+ .allowableValues(CipherAlgorithmMode.class)
+ .defaultValue(CipherAlgorithmMode.GCM.getValue())
+ .build();
+
+ static final PropertyDescriptor CIPHER_ALGORITHM_PADDING = new PropertyDescriptor.Builder()
+ .name("cipher-algorithm-padding")
+ .displayName("Cipher Algorithm Padding")
+ .description("Padding specification used in cipher operation for decryption using the Advanced Encryption Standard")
+ .required(true)
+ .allowableValues(CipherAlgorithmPadding.class)
+ .defaultValue(CipherAlgorithmPadding.NO_PADDING.getValue())
+ .build();
+
+ static final PropertyDescriptor KEY_SPECIFICATION_FORMAT = new PropertyDescriptor.Builder()
+ .name("key-specification-format")
+ .displayName("Key Specification Format")
+ .description("Format describing the configured Key Specification")
+ .required(true)
+ .allowableValues(KeySpecificationFormat.class)
+ .defaultValue(KeySpecificationFormat.PASSWORD.getValue())
+ .build();
+
+ static final PropertyDescriptor KEY_SPECIFICATION = new PropertyDescriptor.Builder()
+ .name("key-specification")
+ .displayName("Key Specification")
+ .description("Specification providing the raw secret key or a password from which to derive a secret key")
+ .required(true)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ static final Relationship SUCCESS = new Relationship.Builder()
+ .name("success")
+ .description("Decryption succeeded")
+ .build();
+
+ static final Relationship FAILURE = new Relationship.Builder()
+ .name("failure")
+ .description("Decryption failed")
+ .build();
+
+ private static final List DESCRIPTORS = Collections.unmodifiableList(Arrays.asList(
+ CIPHER_ALGORITHM_MODE,
+ CIPHER_ALGORITHM_PADDING,
+ KEY_SPECIFICATION_FORMAT,
+ KEY_SPECIFICATION
+ ));
+
+ private static final Set RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ SUCCESS,
+ FAILURE
+ )));
+
+ private static final SymmetricCipher SYMMETRIC_CIPHER = SymmetricCipher.AES;
+
+ private static final String TRANSFORMATION_FORMAT = "%s/%s/%s";
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Processor Property Descriptors
+ */
+ @Override
+ public List getSupportedPropertyDescriptors() {
+ return DESCRIPTORS;
+ }
+
+ /**
+ * Get Relationships
+ *
+ * @return Processor Relationships
+ */
+ @Override
+ public Set getRelationships() {
+ return RELATIONSHIPS;
+ }
+
+ /**
+ * On Trigger decrypts Flow File contents using configured properties
+ *
+ * @param context Process Context
+ * @param session Process Session
+ */
+ @Override
+ public void onTrigger(final ProcessContext context, final ProcessSession session) {
+ FlowFile flowFile = session.get();
+ if (flowFile == null) {
+ return;
+ }
+
+ final String specificationFormat = context.getProperty(KEY_SPECIFICATION_FORMAT).getValue();
+ final KeySpecificationFormat keySpecificationFormat = KeySpecificationFormat.valueOf(specificationFormat);
+
+ final String cipherTransformation = getCipherTransformation(context);
+ final Cipher cipher = getCipher(cipherTransformation);
+ final String algorithmMode = context.getProperty(CIPHER_ALGORITHM_MODE).getValue();
+ final CipherAlgorithmMode cipherAlgorithmMode = CipherAlgorithmMode.valueOf(algorithmMode);
+
+ final KeySpec keySpec = getKeySpec(context, keySpecificationFormat);
+ final StreamCallback callback = new DecryptCallback(cipher, cipherAlgorithmMode, keySpec);
+ try {
+ flowFile = session.write(flowFile, callback);
+ getLogger().debug("Decryption completed using [{}] {}", cipherTransformation, flowFile);
+ session.transfer(flowFile, SUCCESS);
+ } catch (final RuntimeException e) {
+ getLogger().error("Decryption failed using [{}] {}", cipherTransformation, flowFile, e);
+ session.transfer(flowFile, FAILURE);
+ }
+ }
+
+ private String getCipherTransformation(final ProcessContext context) {
+ final String algorithmMode = context.getProperty(CIPHER_ALGORITHM_MODE).getValue();
+ final String algorithmPadding = context.getProperty(CIPHER_ALGORITHM_PADDING).getValue();
+ return String.format(TRANSFORMATION_FORMAT, SYMMETRIC_CIPHER.getValue(), algorithmMode, algorithmPadding);
+ }
+
+ private Cipher getCipher(final String transformation) {
+ try {
+ return Cipher.getInstance(transformation);
+ } catch (final GeneralSecurityException e) {
+ final String message = String.format("Cipher [%s] not found", transformation);
+ throw new CipherException(message, e);
+ }
+ }
+
+ private KeySpec getKeySpec(final ProcessContext context, final KeySpecificationFormat keySpecificationFormat) {
+ final KeySpec keySpec;
+ final String keySpecification = context.getProperty(KEY_SPECIFICATION).getValue();
+ if (KeySpecificationFormat.RAW == keySpecificationFormat) {
+ final byte[] decodedKey = Hex.decode(keySpecification);
+ keySpec = new SecretKeySpec(decodedKey, SYMMETRIC_CIPHER.getValue());
+ } else {
+ final char[] password = keySpecification.toCharArray();
+ keySpec = new PBEKeySpec(password);
+ }
+ return keySpec;
+ }
+
+ private interface SerializedParameterSpec {
+
+ byte[] getParameters();
+ }
+
+ private static class GCMSerializedParameterSpec extends GCMParameterSpec implements SerializedParameterSpec {
+
+ private static final int GCM_TAG_LENGTH_BITS = 128;
+
+ private final byte[] parameters;
+
+ private GCMSerializedParameterSpec(final byte[] iv, final byte[] parameters) {
+ super(GCM_TAG_LENGTH_BITS, iv);
+ this.parameters = parameters;
+ }
+
+ @Override
+ public byte[] getParameters() {
+ return parameters;
+ }
+ }
+
+ private static class IvSerializedParameterSpec extends IvParameterSpec implements SerializedParameterSpec {
+
+ private final byte[] parameters;
+
+ private IvSerializedParameterSpec(final byte[] iv, final byte[] parameters) {
+ super(iv);
+ this.parameters = parameters;
+ }
+
+ @Override
+ public byte[] getParameters() {
+ return parameters;
+ }
+ }
+
+ private static class DecryptCallback extends DecryptStreamCallback {
+
+ private static final int PARAMETERS_BUFFER_LENGTH = 256;
+
+ private static final int DERIVED_KEY_LENGTH_BYTES = 16;
+
+ private static final int END_OF_FILE = -1;
+
+ private static final byte[] EMPTY_BYTES = {};
+
+ private static final DetectedDerivedKeyParameterSpecReader parameterSpecReader = new DetectedDerivedKeyParameterSpecReader();
+
+ private static final DetectedDerivedKeyProvider derivedKeyProvider = new DetectedDerivedKeyProvider();
+
+ private final CipherAlgorithmMode cipherAlgorithmMode;
+
+ private final KeySpec keySpec;
+
+ private DecryptCallback(
+ final Cipher cipher,
+ final CipherAlgorithmMode cipherAlgorithmMode,
+ final KeySpec keySpec
+ ) {
+ super(cipher, PARAMETERS_BUFFER_LENGTH);
+ this.cipherAlgorithmMode = cipherAlgorithmMode;
+ this.keySpec = keySpec;
+ }
+
+ /**
+ * Read Algorithm Parameters including optional salt bytes for derived keys and required initialization vector bytes
+ *
+ * @param parameterBuffer Buffer of parameter bytes
+ * @return Algorithm Parameters Specification based on Cipher Algorithm Mode and parsed bytes
+ */
+ @Override
+ protected AlgorithmParameterSpec readAlgorithmParameterSpec(final ByteBuffer parameterBuffer) {
+ final AlgorithmParameterSpec spec;
+
+ final byte[] salt = readDelimitedBytes(parameterBuffer, EncodedDelimiter.SALT);
+ final byte[] iv = readDelimitedBytes(parameterBuffer, EncodedDelimiter.IV);
+
+ if (CipherAlgorithmMode.GCM == cipherAlgorithmMode) {
+ spec = new GCMSerializedParameterSpec(iv, salt);
+ } else {
+ spec = new IvSerializedParameterSpec(iv, salt);
+ }
+
+ return spec;
+ }
+
+ /**
+ * Get Secret Key for cipher operations using either configured hexadecimal key or key derived from password and parameters
+ *
+ * @param algorithmParameterSpec Algorithm Parameters Specification
+ * @return Secret Key for decryption
+ */
+ @Override
+ protected Key getKey(final AlgorithmParameterSpec algorithmParameterSpec) {
+ final Key key;
+
+ if (keySpec instanceof SecretKeySpec) {
+ key = (SecretKeySpec) keySpec;
+ } else if (algorithmParameterSpec instanceof SerializedParameterSpec) {
+ final SerializedParameterSpec serializedParameterSpec = (SerializedParameterSpec) algorithmParameterSpec;
+ final byte[] parameters = serializedParameterSpec.getParameters();
+ key = getDerivedKey(parameters);
+ } else {
+ final String message = String.format("Key Derivation Function Parameters not provided [%s]", algorithmParameterSpec.getClass());
+ throw new IllegalArgumentException(message);
+ }
+
+ return key;
+ }
+
+ private Key getDerivedKey(final byte[] parameters) {
+ final DerivedKeyParameterSpec derivedKeyParameterSpec = parameterSpecReader.read(parameters);
+ final DerivedKeySpec derivedKeySpec = getDerivedKeySpec(derivedKeyParameterSpec);
+ return derivedKeyProvider.getDerivedKey(derivedKeySpec);
+ }
+
+ private DerivedKeySpec getDerivedKeySpec(final DerivedKeyParameterSpec parameterSpec) {
+ final PBEKeySpec pbeKeySpec = (PBEKeySpec) keySpec;
+ final char[] password = pbeKeySpec.getPassword();
+ return new StandardDerivedKeySpec<>(
+ password,
+ DERIVED_KEY_LENGTH_BYTES,
+ SYMMETRIC_CIPHER.getValue(),
+ parameterSpec
+ );
+ }
+
+ private byte[] readDelimitedBytes(final ByteBuffer buffer, final EncodedDelimiter encodedDelimiter) {
+ final byte[] delimiter = encodedDelimiter.getDelimiter();
+ final int delimiterIndex = ByteBufferSearch.indexOf(buffer, delimiter);
+
+ final byte[] delimitedBytes;
+ if (delimiterIndex == END_OF_FILE) {
+ delimitedBytes = EMPTY_BYTES;
+ } else {
+ final int delimitedLength = delimiterIndex - buffer.position();
+ delimitedBytes = new byte[delimitedLength];
+ buffer.get(delimitedBytes);
+
+ final int newPosition = delimiterIndex + delimiter.length;
+ buffer.position(newPosition);
+ }
+ return delimitedBytes;
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/DecryptContentCompatibility.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/DecryptContentCompatibility.java
new file mode 100644
index 0000000000..e13f284ef9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/DecryptContentCompatibility.java
@@ -0,0 +1,295 @@
+/*
+ * 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.cipher;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.processors.cipher.compatibility.CompatibilityModeEncryptionScheme;
+import org.apache.nifi.processors.cipher.compatibility.CompatibilityModeKeyDerivationStrategy;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processors.cipher.io.DecryptStreamCallback;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+@SideEffectFree
+@SupportsBatching
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"cryptography", "decipher", "decrypt", "Jasypt", "OpenSSL", "PKCS5", "PBES1"})
+@CapabilityDescription("Decrypt content using password-based encryption schemes with legacy algorithms supporting historical compatibility modes.")
+@WritesAttributes({
+ @WritesAttribute(attribute = CipherAttributeKey.PBE_SCHEME, description = "Password-Based Encryption Scheme"),
+ @WritesAttribute(attribute = CipherAttributeKey.PBE_SYMMETRIC_CIPHER, description = "Password-Based Encryption Block Cipher"),
+ @WritesAttribute(attribute = CipherAttributeKey.PBE_DIGEST_ALGORITHM, description = "Password-Based Encryption Digest Algorithm"),
+})
+public class DecryptContentCompatibility extends AbstractProcessor {
+
+ static final PropertyDescriptor ENCRYPTION_SCHEME = new PropertyDescriptor.Builder()
+ .name("encryption-scheme")
+ .displayName("Encryption Scheme")
+ .description("Password-Based Encryption Scheme including PBES1 described in RFC 8018, and others defined according to PKCS12 and Bouncy Castle implementations")
+ .required(true)
+ .allowableValues(CompatibilityModeEncryptionScheme.class)
+ .build();
+
+ static final PropertyDescriptor KEY_DERIVATION_STRATEGY = new PropertyDescriptor.Builder()
+ .name("key-derivation-strategy")
+ .displayName("Key Derivation Strategy")
+ .description("Strategy for reading salt from encoded contents and deriving the decryption key according to the number of function iterations")
+ .required(true)
+ .allowableValues(CompatibilityModeKeyDerivationStrategy.class)
+ .build();
+
+ static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder()
+ .name("password")
+ .displayName("Password")
+ .description("Password required for Password-Based Encryption Schemes")
+ .required(true)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ static final Relationship SUCCESS = new Relationship.Builder()
+ .name("success")
+ .description("Decryption succeeded")
+ .build();
+
+ static final Relationship FAILURE = new Relationship.Builder()
+ .name("failure")
+ .description("Decryption failed")
+ .build();
+
+ private static final List DESCRIPTORS = Collections.unmodifiableList(Arrays.asList(
+ ENCRYPTION_SCHEME,
+ KEY_DERIVATION_STRATEGY,
+ PASSWORD
+ ));
+
+ private static final Set RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ SUCCESS,
+ FAILURE
+ )));
+
+ private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Processor Property Descriptors
+ */
+ @Override
+ public List getSupportedPropertyDescriptors() {
+ return DESCRIPTORS;
+ }
+
+ /**
+ * Get Relationships
+ *
+ * @return Processor Relationships
+ */
+ @Override
+ public Set getRelationships() {
+ return RELATIONSHIPS;
+ }
+
+ /**
+ * On Trigger decrypts Flow File contents using configured properties
+ *
+ * @param context Process Context
+ * @param session Process Session
+ */
+ @Override
+ public void onTrigger(final ProcessContext context, final ProcessSession session) {
+ FlowFile flowFile = session.get();
+ if (flowFile == null) {
+ return;
+ }
+
+ final String scheme = context.getProperty(ENCRYPTION_SCHEME).getValue();
+ final CompatibilityModeEncryptionScheme encryptionScheme = getEncryptionScheme(scheme);
+ final Cipher cipher = getCipher(scheme);
+
+ final char[] password = context.getProperty(PASSWORD).getValue().toCharArray();
+ final PBEKeySpec keySpec = new PBEKeySpec(password);
+
+ final String strategy = context.getProperty(KEY_DERIVATION_STRATEGY).getValue();
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy = CompatibilityModeKeyDerivationStrategy.valueOf(strategy);
+ final StreamCallback callback = new DecryptCallback(cipher, keySpec, keyDerivationStrategy);
+
+ final Map attributes = new LinkedHashMap<>();
+ attributes.put(CipherAttributeKey.PBE_SCHEME, encryptionScheme.getValue());
+ attributes.put(CipherAttributeKey.PBE_SYMMETRIC_CIPHER, encryptionScheme.getSymmetricCipher().getValue());
+ attributes.put(CipherAttributeKey.PBE_DIGEST_ALGORITHM, encryptionScheme.getDigestAlgorithm().getValue());
+
+ try {
+ flowFile = session.write(flowFile, callback);
+ flowFile = session.putAllAttributes(flowFile, attributes);
+ getLogger().debug("Decryption completed using [{}] {}", scheme, flowFile);
+ session.transfer(flowFile, SUCCESS);
+ } catch (final RuntimeException e) {
+ getLogger().error("Decryption failed using [{}] {}", scheme, flowFile, e);
+ session.transfer(flowFile, FAILURE);
+ }
+ }
+
+ private CompatibilityModeEncryptionScheme getEncryptionScheme(final String scheme) {
+ final Optional encryptionSchemeFound = Arrays.stream(CompatibilityModeEncryptionScheme.values())
+ .filter(encryptionScheme -> encryptionScheme.getValue().equals(scheme))
+ .findFirst();
+ return encryptionSchemeFound.orElseThrow(() -> new IllegalArgumentException(String.format("Encryption Scheme [%s] not found", scheme)));
+ }
+
+ private Cipher getCipher(final String transformation) {
+ try {
+ return Cipher.getInstance(transformation, BOUNCY_CASTLE_PROVIDER);
+ } catch (final GeneralSecurityException e) {
+ final String message = String.format("Cipher [%s] not found", transformation);
+ throw new CipherException(message, e);
+ }
+ }
+
+ private static class DecryptCallback extends DecryptStreamCallback {
+ private static final byte[] EMPTY_SALT = {};
+
+ private static final int BLOCK_SIZE_UNDEFINED = 0;
+
+ private final String cipherAlgorithm;
+
+ private final int cipherBlockSize;
+
+ private final PBEKeySpec keySpec;
+
+ private final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy;
+
+ private DecryptCallback(
+ final Cipher cipher,
+ final PBEKeySpec keySpec,
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy
+ ) {
+ super(cipher, keyDerivationStrategy.getSaltBufferLength());
+ this.cipherAlgorithm = cipher.getAlgorithm();
+ this.cipherBlockSize = cipher.getBlockSize();
+ this.keySpec = keySpec;
+ this.keyDerivationStrategy = keyDerivationStrategy;
+ }
+
+ /**
+ * Get Secret Key generated based on configured key specification
+ *
+ * @param algorithmParameterSpec Algorithm Parameters Specification not used
+ * @return Secret Key
+ */
+ @Override
+ protected Key getKey(final AlgorithmParameterSpec algorithmParameterSpec) {
+ final SecretKeyFactory secretKeyFactory = getSecretKeyFactory();
+ try {
+ return secretKeyFactory.generateSecret(keySpec);
+ } catch (final InvalidKeySpecException e) {
+ final String message = String.format("Generate Secret Key Algorithm [%s] invalid specification", cipherAlgorithm);
+ throw new CipherException(message, e);
+ }
+ }
+
+ /**
+ * Read salt bytes based on configured key derivation strategy
+ *
+ * @param parameterBuffer Buffer of parameter bytes
+ * @return Password-Based Encryption specification with salt and configured iterations
+ */
+ @Override
+ protected AlgorithmParameterSpec readAlgorithmParameterSpec(final ByteBuffer parameterBuffer) {
+ final byte[] salt = readSalt(parameterBuffer);
+ return new PBEParameterSpec(salt, keyDerivationStrategy.getIterations());
+ }
+
+ private byte[] readSalt(final ByteBuffer byteBuffer) {
+ final byte[] salt;
+
+ if (CompatibilityModeKeyDerivationStrategy.OPENSSL_EVP_BYTES_TO_KEY == keyDerivationStrategy) {
+ salt = readSaltOpenSsl(byteBuffer);
+ } else {
+ salt = readSaltStandard(byteBuffer);
+ }
+
+ return salt;
+ }
+
+ private byte[] readSaltOpenSsl(final ByteBuffer byteBuffer) {
+ final byte[] salt;
+
+ final int saltHeaderLength = keyDerivationStrategy.getSaltHeader().length;
+ final byte[] saltHeader = new byte[saltHeaderLength];
+ byteBuffer.get(saltHeader);
+
+ if (MessageDigest.isEqual(keyDerivationStrategy.getSaltHeader(), saltHeader)) {
+ salt = new byte[keyDerivationStrategy.getSaltStandardLength()];
+ byteBuffer.get(salt);
+ } else {
+ salt = EMPTY_SALT;
+ byteBuffer.rewind();
+ }
+
+ return salt;
+ }
+
+ private byte[] readSaltStandard(final ByteBuffer byteBuffer) {
+ final int saltLength = cipherBlockSize == BLOCK_SIZE_UNDEFINED ? keyDerivationStrategy.getSaltStandardLength() : cipherBlockSize;
+ final byte[] salt = new byte[saltLength];
+ byteBuffer.get(salt);
+ return salt;
+ }
+
+ private SecretKeyFactory getSecretKeyFactory() {
+ try {
+ return SecretKeyFactory.getInstance(cipherAlgorithm, BOUNCY_CASTLE_PROVIDER);
+ } catch (final NoSuchAlgorithmException e) {
+ final String message = String.format("Secret Key Factory Algorithm [%s] not found", cipherAlgorithm);
+ throw new CipherException(message, e);
+ }
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/CipherAlgorithmMode.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/CipherAlgorithmMode.java
new file mode 100644
index 0000000000..379c255feb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/CipherAlgorithmMode.java
@@ -0,0 +1,51 @@
+/*
+ * 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.cipher.algorithm;
+
+import org.apache.nifi.components.DescribedValue;
+
+/**
+ * Cipher Algorithm Modes as enumerated in Java Cryptography Architecture Standard Algorithm Name Documentation
+ */
+public enum CipherAlgorithmMode implements DescribedValue {
+ CBC("Cipher Blocking Chaining Mode"),
+
+ CTR("Counter Mode"),
+
+ GCM("Galois/Counter Mode supporting Authenticated Encryption with Associated Data");
+
+ private final String description;
+
+ CipherAlgorithmMode(final String description) {
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name();
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/CipherAlgorithmPadding.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/CipherAlgorithmPadding.java
new file mode 100644
index 0000000000..67cb607093
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/CipherAlgorithmPadding.java
@@ -0,0 +1,51 @@
+/*
+ * 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.cipher.algorithm;
+
+import org.apache.nifi.components.DescribedValue;
+
+/**
+ * Cipher Algorithm Paddings as enumerated in Java Cryptography Architecture Standard Algorithm Name Documentation
+ */
+public enum CipherAlgorithmPadding implements DescribedValue {
+ /** No Padding */
+ NO_PADDING("NoPadding"),
+
+ /** PKCS5 Padding described in RFC 8018 */
+ PKCS5_PADDING("PKCS5Padding");
+
+ private final String padding;
+
+ CipherAlgorithmPadding(final String padding) {
+ this.padding = padding;
+ }
+
+ @Override
+ public String getValue() {
+ return padding;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return padding;
+ }
+
+ @Override
+ public String getDescription() {
+ return padding;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/DigestAlgorithm.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/DigestAlgorithm.java
new file mode 100644
index 0000000000..88eefdd278
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/DigestAlgorithm.java
@@ -0,0 +1,54 @@
+/*
+ * 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.cipher.algorithm;
+
+import org.apache.nifi.components.DescribedValue;
+
+/**
+ * Digest Algorithm as enumerated in Java Cryptography Architecture Standard Algorithm Name Documentation
+ */
+public enum DigestAlgorithm implements DescribedValue {
+ MD5("MD5", "Message Digest Algorithm 5"),
+
+ SHA1("SHA-1", "Secure Hash Algorithm 1"),
+
+ SHA256("SHA-256", "Secure Hash Algorithm 2 with 256 bits");
+
+ private String name;
+
+ private String description;
+
+ DigestAlgorithm(final String name, final String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return name;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/SymmetricCipher.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/SymmetricCipher.java
new file mode 100644
index 0000000000..f1b2b739a7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/algorithm/SymmetricCipher.java
@@ -0,0 +1,60 @@
+/*
+ * 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.cipher.algorithm;
+
+import org.apache.nifi.components.DescribedValue;
+
+/**
+ * Symmetric Cipher Algorithm Names as enumerated in Java Cryptography Architecture Standard Algorithm Name Documentation
+ */
+public enum SymmetricCipher implements DescribedValue {
+ AES("AES", "Advanced Encryption Standard defined in FIPS 197"),
+
+ DES("DES", "Data Encryption Standard defined in FIPS 46-3 and withdrawn in 2005"),
+
+ DESEDE("DESede", "Triple Data Encryption Standard also known as 3DES and deprecated in 2023"),
+
+ RC2("RC2", "RSA Rivest Cipher 2 defined in RFC 2268"),
+
+ RC4("RC4", "RSA Rivest Cipher 4"),
+
+ TWOFISH("TWOFISH", "Twofish Block Cipher");
+
+ private final String name;
+
+ private final String description;
+
+ SymmetricCipher(final String name, final String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/compatibility/CompatibilityModeEncryptionScheme.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/compatibility/CompatibilityModeEncryptionScheme.java
new file mode 100644
index 0000000000..92efaedb16
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/compatibility/CompatibilityModeEncryptionScheme.java
@@ -0,0 +1,216 @@
+/*
+ * 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.cipher.compatibility;
+
+import org.apache.nifi.processors.cipher.algorithm.SymmetricCipher;
+import org.apache.nifi.processors.cipher.algorithm.DigestAlgorithm;
+import org.apache.nifi.components.DescribedValue;
+
+import static org.apache.nifi.processors.cipher.algorithm.SymmetricCipher.AES;
+import static org.apache.nifi.processors.cipher.algorithm.SymmetricCipher.DES;
+import static org.apache.nifi.processors.cipher.algorithm.SymmetricCipher.DESEDE;
+import static org.apache.nifi.processors.cipher.algorithm.SymmetricCipher.RC2;
+import static org.apache.nifi.processors.cipher.algorithm.SymmetricCipher.RC4;
+import static org.apache.nifi.processors.cipher.algorithm.SymmetricCipher.TWOFISH;
+
+/**
+ * Compatibility Mode Encryption Schemes supporting decryption using legacy algorithms such as PBES1 defined in RFC 8018 Section 6.1
+ */
+public enum CompatibilityModeEncryptionScheme implements DescribedValue {
+ PBE_WITH_MD5_AND_AES_CBC_128(
+ "PBEWITHMD5AND128BITAES-CBC-OPENSSL",
+ DigestAlgorithm.MD5,
+ AES,
+ "PKCS12 with MD5 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 128 bit keys."
+ ),
+
+ PBE_WITH_MD5_AND_AES_CBC_192(
+ "PBEWITHMD5AND192BITAES-CBC-OPENSSL",
+ DigestAlgorithm.MD5,
+ AES,
+ "PKCS12 with MD5 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 192 bit keys."
+ ),
+
+ PBE_WITH_MD5_AND_AES_CBC_256(
+ "PBEWITHMD5AND256BITAES-CBC-OPENSSL",
+ DigestAlgorithm.MD5,
+ AES,
+ "PKCS12 with MD5 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 256 bit keys."
+ ),
+
+ PBE_WITH_MD5_AND_DES(
+ "PBEWITHMD5ANDDES",
+ DigestAlgorithm.MD5,
+ DES,
+ "PKCS5 Scheme 1 with MD5 digest and Data Encryption Standard 64 bit keys. OID 1.2.840.113549.1.5.3"
+ ),
+
+ PBE_WITH_MD5_AND_RC2(
+ "PBEWITHMD5ANDRC2",
+ DigestAlgorithm.MD5,
+ RC2,
+ "PKCS Scheme 1 with MD5 digest and Rivest Cipher 2. OID 1.2.840.113549.1.5.6"
+ ),
+
+ PBE_WITH_SHA1_AND_AES_CBC_128(
+ "PBEWITHSHAAND128BITAES-CBC-BC",
+ DigestAlgorithm.SHA1,
+ AES,
+ "PKCS12 with SHA-1 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 128 bit keys."
+ ),
+
+ PBE_WITH_SHA1_AND_AES_CBC_192(
+ "PBEWITHSHAAND192BITAES-CBC-BC",
+ DigestAlgorithm.SHA1,
+ AES,
+ "PKCS12 with SHA-1 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 192 bit keys."
+ ),
+
+ PBE_WITH_SHA1_AND_AES_CBC_256(
+ "PBEWITHSHAAND256BITAES-CBC-BC",
+ DigestAlgorithm.SHA1,
+ AES,
+ "PKCS12 with SHA-1 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 256 bit keys."
+ ),
+
+ PBE_WITH_SHA1_AND_DES(
+ "PBEWITHSHA1ANDDES",
+ DigestAlgorithm.SHA1,
+ DES,
+ "PKCS5 Scheme 1 with SHA-1 digest and Data Encryption Standard. OID 1.2.840.113549.1.5.10"
+ ),
+
+ PBE_WITH_SHA1_AND_DESEDE_128(
+ "PBEWITHSHAAND2-KEYTRIPLEDES-CBC",
+ DigestAlgorithm.SHA1,
+ DESEDE,
+ "PKCS12 with SHA-1 digest and Triple Data Encryption Standard 128 bit keys. OID 1.2.840.113549.1.12.1.4"
+ ),
+
+ PBE_WITH_SHA1_AND_DESEDE_192(
+ "PBEWITHSHAAND3-KEYTRIPLEDES-CBC",
+ DigestAlgorithm.SHA1,
+ DESEDE,
+ "PKCS12 with SHA-1 digest and Triple Data Encryption Standard 192 bit keys. OID 1.2.840.113549.1.12.1.3"
+ ),
+
+ PBE_WITH_SHA1_AND_RC2(
+ "PBEWITHSHA1ANDRC2",
+ DigestAlgorithm.SHA1,
+ RC2,
+ "PKCS5 Scheme 1 with SHA-1 digest and Rivest Cipher 2. OID 1.2.840.113549.1.5.11"
+ ),
+
+ PBE_WITH_SHA1_AND_RC2_128(
+ "PBEWITHSHAAND128BITRC2-CBC",
+ DigestAlgorithm.SHA1,
+ RC2,
+ "PKCS12 with SHA-1 digest and Rivest Cipher 2 128 bit keys. OID 1.2.840.113549.1.12.1.5"
+ ),
+
+ PBE_WITH_SHA1_AND_RC2_40(
+ "PBEWITHSHAAND40BITRC2-CBC",
+ DigestAlgorithm.SHA1,
+ RC2,
+ "PKCS12 with SHA-1 digest and Rivest Cipher 2 40 bit keys. OID 1.2.840.113549.1.12.1.6"
+ ),
+
+ PBE_WITH_SHA1_AND_RC4_128(
+ "PBEWITHSHAAND128BITRC4",
+ DigestAlgorithm.SHA1,
+ RC4,
+ "PKCS12 with SHA-1 digest and Rivest Cipher 4 128 bit keys. OID 1.2.840.113549.1.12.1.1"
+ ),
+
+ PBE_WITH_SHA1_AND_RC4_40(
+ "PBEWITHSHAAND40BITRC4",
+ DigestAlgorithm.SHA1,
+ RC4,
+ "PKCS12 with SHA-1 digest and Rivest Cipher 4 40 bit keys. OID 1.2.840.113549.1.12.1.2"
+ ),
+
+ PBE_WITH_SHA1_AND_TWOFISH(
+ "PBEWITHSHAANDTWOFISH-CBC",
+ DigestAlgorithm.SHA1,
+ TWOFISH,
+ "PKCS12 with SHA-1 digest and Twofish in Cipher Block Chaining mode using 256 bit keys."
+ ),
+
+ PBE_WITH_SHA256_AND_AES_CBC_128(
+ "PBEWITHSHA256AND128BITAES-CBC-BC",
+ DigestAlgorithm.SHA256,
+ AES,
+ "PKCS12 with SHA-256 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 128 bit keys."
+ ),
+
+ PBE_WITH_SHA256_AND_AES_CBC_192(
+ "PBEWITHSHA256AND192BITAES-CBC-BC",
+ DigestAlgorithm.SHA256,
+ AES,
+ "PKCS12 with SHA-256 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 192 bit keys."
+ ),
+
+ PBE_WITH_SHA256_AND_AES_CBC_256(
+ "PBEWITHSHA256AND256BITAES-CBC-BC",
+ DigestAlgorithm.SHA256,
+ AES,
+ "PKCS12 with SHA-256 digest and Advanced Encryption Standard in Cipher Block Chaining mode using 256 bit keys."
+ );
+
+ private final String algorithm;
+
+ private final String description;
+
+ private final DigestAlgorithm digestAlgorithm;
+
+ private final SymmetricCipher symmetricCipher;
+
+ CompatibilityModeEncryptionScheme(
+ final String algorithm,
+ final DigestAlgorithm digestAlgorithm,
+ final SymmetricCipher symmetricCipher,
+ final String description
+ ) {
+ this.algorithm = algorithm;
+ this.description = description;
+ this.digestAlgorithm = digestAlgorithm;
+ this.symmetricCipher = symmetricCipher;
+ }
+
+ @Override
+ public String getValue() {
+ return algorithm;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name();
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ public DigestAlgorithm getDigestAlgorithm() {
+ return digestAlgorithm;
+ }
+
+ public SymmetricCipher getSymmetricCipher() {
+ return symmetricCipher;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/compatibility/CompatibilityModeKeyDerivationStrategy.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/compatibility/CompatibilityModeKeyDerivationStrategy.java
new file mode 100644
index 0000000000..c88c540c63
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/compatibility/CompatibilityModeKeyDerivationStrategy.java
@@ -0,0 +1,99 @@
+/*
+ * 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.cipher.compatibility;
+
+import org.apache.nifi.components.DescribedValue;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Compatibility Mode Key Derivation Strategy supporting decryption with legacy schemes
+ */
+public enum CompatibilityModeKeyDerivationStrategy implements DescribedValue {
+ /** Strategy based on OpenSSL EVP_BytesToKey function */
+ OPENSSL_EVP_BYTES_TO_KEY(
+ "OpenSSL Envelope BytesToKey using a digest algorithm with one iteration and optional salt of eight bytes",
+ 0,
+ 8,
+ 16,
+ "Salted__".getBytes(StandardCharsets.US_ASCII)
+ ),
+
+ /** Strategy based on default configuration of org.jasypt.encryption.pbe.StandardPBEByteEncryptor */
+ JASYPT_STANDARD(
+ "Jasypt Java Simplified Encryption using a digest algorithm with 1000 iterations and required salt of eight or sixteen bytes",
+ 1000,
+ 8,
+ 16,
+ new byte[0]
+ );
+
+ private final String description;
+
+ private final int iterations;
+
+ private final byte[] saltHeader;
+
+ private final int saltStandardLength;
+
+ private final int saltBufferLength;
+
+ CompatibilityModeKeyDerivationStrategy(
+ final String description,
+ final int iterations,
+ final int saltStandardLength,
+ final int saltBufferLength,
+ final byte[] saltHeader
+ ) {
+ this.description = description;
+ this.iterations = iterations;
+ this.saltStandardLength = saltStandardLength;
+ this.saltBufferLength = saltBufferLength;
+ this.saltHeader = saltHeader;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name();
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ public int getIterations() {
+ return iterations;
+ }
+
+ public int getSaltStandardLength() {
+ return saltStandardLength;
+ }
+
+ public int getSaltBufferLength() {
+ return saltBufferLength;
+ }
+
+ public byte[] getSaltHeader() {
+ return saltHeader;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/encoded/EncodedDelimiter.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/encoded/EncodedDelimiter.java
new file mode 100644
index 0000000000..4f88f23255
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/encoded/EncodedDelimiter.java
@@ -0,0 +1,61 @@
+/*
+ * 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.cipher.encoded;
+
+import org.apache.nifi.components.DescribedValue;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Delimiters for content encoded according to conventions added in NiFi 0.5.0
+ */
+public enum EncodedDelimiter implements DescribedValue {
+ SALT("NiFiSALT", "Eight bytes appended to a stream after the salt bytes according to NiFi 0.5.0 conventions"),
+
+ IV("NiFiIV", "Six bytes appended to a stream after the initialization vector bytes according to NiFi 0.5.0 conventions");
+
+ private final byte[] delimiter;
+
+ private final String description;
+
+ EncodedDelimiter(
+ final String delimiterEncoded,
+ final String description
+ ) {
+ this.delimiter = delimiterEncoded.getBytes(StandardCharsets.UTF_8);
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name();
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ public byte[] getDelimiter() {
+ return delimiter;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/encoded/KeySpecificationFormat.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/encoded/KeySpecificationFormat.java
new file mode 100644
index 0000000000..6135c600e9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/encoded/KeySpecificationFormat.java
@@ -0,0 +1,49 @@
+/*
+ * 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.cipher.encoded;
+
+import org.apache.nifi.components.DescribedValue;
+
+/**
+ * Key Specification Format supporting raw and derived secret keys
+ */
+public enum KeySpecificationFormat implements DescribedValue {
+ RAW("Raw secret key provided as a hexadecimal string"),
+
+ PASSWORD("Password string for use with a Key Derivation Function to produce a secret key");
+
+ private final String description;
+
+ KeySpecificationFormat(final String description) {
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return name();
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/io/DecryptStreamCallback.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/io/DecryptStreamCallback.java
new file mode 100644
index 0000000000..63087bfa9c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/io/DecryptStreamCallback.java
@@ -0,0 +1,137 @@
+/*
+ * 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.cipher.io;
+
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.processors.cipher.CipherException;
+
+import javax.crypto.Cipher;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.spec.AlgorithmParameterSpec;
+
+/**
+ * Decrypt Stream Callback implements shared methods for cipher operations and parameter buffer processing
+ */
+public abstract class DecryptStreamCallback implements StreamCallback {
+
+ private static final int BUFFER_LENGTH = 4096;
+
+ private static final int BUFFER_INPUT_OFFSET = 0;
+
+ private static final int END_OF_FILE = -1;
+
+ private final Cipher cipher;
+
+ private final int parameterBufferLength;
+
+ public DecryptStreamCallback(
+ final Cipher cipher,
+ final int parameterBufferLength
+ ) {
+ this.cipher = cipher;
+ this.parameterBufferLength = parameterBufferLength;
+ }
+
+ /**
+ * Process streams with parsed algorithm parameters and cipher initialization
+ *
+ * @param inputStream Stream of encrypted bytes
+ * @param outputStream Stream of decrypted bytes
+ * @throws IOException Thrown on read or write failures
+ */
+ @Override
+ public void process(final InputStream inputStream, final OutputStream outputStream) throws IOException {
+ final ByteBuffer parameterBuffer = readParameterBuffer(inputStream);
+ final AlgorithmParameterSpec algorithmParameterSpec = readAlgorithmParameterSpec(parameterBuffer);
+ final Key key = getKey(algorithmParameterSpec);
+ initCipher(key, algorithmParameterSpec);
+
+ processBuffer(parameterBuffer, outputStream);
+ processStream(inputStream, outputStream);
+ }
+
+ /**
+ * Read Cipher Algorithm Parameters from initial buffer of parameter bytes
+ *
+ * @param parameterBuffer Buffer of parameter bytes
+ * @return Algorithm Parameters Specification
+ */
+ protected abstract AlgorithmParameterSpec readAlgorithmParameterSpec(final ByteBuffer parameterBuffer);
+
+ /**
+ * Get Key for Cipher operations using Algorithm Parameters when needed
+ *
+ * @param algorithmParameterSpec Algorithm Parameters Specification
+ * @return Key for decryption processing
+ */
+ protected abstract Key getKey(final AlgorithmParameterSpec algorithmParameterSpec);
+
+ private void initCipher(final Key key, final AlgorithmParameterSpec algorithmParameterSpec) {
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameterSpec);
+ } catch (final GeneralSecurityException e) {
+ final String message = String.format("Cipher [%s] initialization failed", cipher.getAlgorithm());
+ throw new CipherException(message, e);
+ }
+ }
+
+ private ByteBuffer readParameterBuffer(final InputStream inputStream) throws IOException {
+ final byte[] buffer = new byte[parameterBufferLength];
+ final int read = inputStream.read(buffer);
+ if (read == END_OF_FILE) {
+ throw new EOFException("Read parameters buffer failed");
+ }
+ return ByteBuffer.wrap(buffer, BUFFER_INPUT_OFFSET, read);
+ }
+
+ private void processBuffer(final ByteBuffer byteBuffer, final OutputStream outputStream) throws IOException {
+ if (byteBuffer.hasRemaining()) {
+ final int remaining = byteBuffer.remaining();
+ final byte[] buffer = new byte[remaining];
+ byteBuffer.get(buffer);
+ processBytes(buffer, remaining, outputStream);
+ }
+ }
+
+ private void processBytes(final byte[] buffer, final int length, final OutputStream outputStream) throws IOException {
+ final byte[] deciphered = cipher.update(buffer, BUFFER_INPUT_OFFSET, length);
+ if (deciphered != null) {
+ outputStream.write(deciphered);
+ }
+ }
+
+ private void processStream(final InputStream inputStream, final OutputStream outputStream) throws IOException {
+ final byte[] buffer = new byte[BUFFER_LENGTH];
+ int read;
+ while ((read = inputStream.read(buffer)) != END_OF_FILE) {
+ processBytes(buffer, read, outputStream);
+ }
+ try {
+ final byte[] deciphered = cipher.doFinal();
+ outputStream.write(deciphered);
+ } catch (final GeneralSecurityException e) {
+ final String message = String.format("Cipher [%s] verification failed", cipher.getAlgorithm());
+ throw new CipherException(message, e);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
new file mode 100644
index 0000000000..9340751c56
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -0,0 +1,16 @@
+# 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.
+org.apache.nifi.processors.cipher.DecryptContentCompatibility
+org.apache.nifi.processors.cipher.DecryptContent
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/docs/org.apache.nifi.processors.cipher.DecryptContent/additionalDetails.html b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/docs/org.apache.nifi.processors.cipher.DecryptContent/additionalDetails.html
new file mode 100644
index 0000000000..11c1a26f61
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/docs/org.apache.nifi.processors.cipher.DecryptContent/additionalDetails.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ DecryptContent
+
+
+
+
Summary
+
+ This Processor supports decryption using the Advanced Encryption Standard algorithm with modern key derivation
+ functions and formats defined in the EncryptContent Processor.
+ Decryption processing is capable of selecting the necessary key derivation function based on reading the header
+ bytes of input files. The Processor supports password-based decryption or key-based decryption depending on
+ configured properties.
+
+
Configuration
+
+ This Processor can be configured to decrypt information encrypted using the EncryptContent Processor. Successful
+ decryption requires selecting matching algorithm and password properties.
+
+ This Processor supports decryption using legacy formats and Password-Based Encryption Algorithms.
+ RFC 8018 defines the Password-Based Cryptography
+ Specification Version 2.1, including several of the supported encryption schemes described in Section 6.1
+ as PBES1. According to the specification, PBES1 is not recommended for new applications, and this Processor
+ exists for the purpose of providing compatibility with historical information. The supported key derivation
+ strategies align with implementations in OpenSSL and the Java Simplified Encryption library.
+
+
Configuration
+
+ This Processor can be configured to decrypt information encrypted using the EncryptContent Processor. Successful
+ decryption requires selecting matching algorithm and password properties.
+
+
Key Derivation Property Values
+
+
+
+
DecryptContentCompatibility
+
EncryptContent
+
+
+
JASYPT_STANDARD
+
NiFi Legacy KDF
+
+
+
OPENSSL_EVP_BYTES_TO_KEY
+
OpenSSL EVP_BytesToKey
+
+
+
+
Encryption Scheme Property Values
+
+
+
+
DecryptContentCompatibility
+
EncryptContent
+
+
+
PBE_WITH_MD5_AND_AES_CBC_128
+
MD5_128AES
+
+
+
PBE_WITH_MD5_AND_AES_CBC_192
+
MD5_192AES
+
+
+
PBE_WITH_MD5_AND_AES_CBC_256
+
MD5_256AES
+
+
+
PBE_WITH_MD5_AND_DES
+
MD5_DES
+
+
+
PBE_WITH_MD5_AND_RC2
+
MD5_RC2
+
+
+
PBE_WITH_SHA1_AND_AES_CBC_128
+
SHA_128AES
+
+
+
PBE_WITH_SHA1_AND_AES_CBC_192
+
SHA_192AES
+
+
+
PBE_WITH_SHA1_AND_AES_CBC_256
+
SHA_256AES
+
+
+
PBE_WITH_SHA1_AND_DES
+
SHA1_DES
+
+
+
PBE_WITH_SHA1_AND_DESEDE_128
+
SHA_2KEYTRIPLEDES
+
+
+
PBE_WITH_SHA1_AND_DESEDE_192
+
SHA_3KEYTRIPLEDES
+
+
+
PBE_WITH_SHA1_AND_RC2
+
SHA1_RC2
+
+
+
PBE_WITH_SHA1_AND_RC2_40
+
SHA_40RC2
+
+
+
PBE_WITH_SHA1_AND_RC2_128
+
SHA_128RC2
+
+
+
PBE_WITH_SHA1_AND_RC4_40
+
SHA_40RC4
+
+
+
PBE_WITH_SHA1_AND_RC4_128
+
SHA_128RC4
+
+
+
PBE_WITH_SHA1_AND_TWOFISH
+
SHA_TWOFISH
+
+
+
PBE_WITH_SHA256_AND_AES_CBC_128
+
SHA256_128AES
+
+
+
PBE_WITH_SHA256_AND_AES_CBC_192
+
SHA256_192AES
+
+
+
PBE_WITH_SHA256_AND_AES_CBC_256
+
SHA256_256AES
+
+
+
+
+
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/DecryptContentCompatibilityTest.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/DecryptContentCompatibilityTest.java
new file mode 100644
index 0000000000..6f2cfb3186
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/DecryptContentCompatibilityTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.cipher;
+
+import org.apache.nifi.processors.cipher.compatibility.CompatibilityModeEncryptionScheme;
+import org.apache.nifi.processors.cipher.compatibility.CompatibilityModeKeyDerivationStrategy;
+import org.apache.nifi.util.LogMessage;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DecryptContentCompatibilityTest {
+ private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
+
+ private static final String PASSWORD = "password";
+
+ private static final byte[] RAW = DecryptContentCompatibility.class.getSimpleName().getBytes(StandardCharsets.UTF_8);
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private static final CompatibilityModeEncryptionScheme SIMPLE_ENCRYPTION_SCHEME = CompatibilityModeEncryptionScheme.PBE_WITH_MD5_AND_DES;
+
+ private static final byte[] EMPTY_SALT = {};
+
+ private static final byte[] OPENSSL_SALT = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
+
+ private static final int BLOCK_SIZE_UNDEFINED = 0;
+
+ private static final byte[] WORD = {'W', 'O', 'R', 'D'};
+
+ private static final int WORD_COUNT = 2048;
+
+ TestRunner runner;
+
+ @BeforeEach
+ void setRunner() {
+ runner = TestRunners.newTestRunner(DecryptContentCompatibility.class);
+ }
+
+ @ParameterizedTest
+ @EnumSource(CompatibilityModeEncryptionScheme.class)
+ void testRunKeyDerivationOpenSslUnsalted(final CompatibilityModeEncryptionScheme encryptionScheme) throws Exception {
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy = CompatibilityModeKeyDerivationStrategy.OPENSSL_EVP_BYTES_TO_KEY;
+ runner.setProperty(DecryptContentCompatibility.KEY_DERIVATION_STRATEGY, keyDerivationStrategy.getValue());
+ runner.setProperty(DecryptContentCompatibility.ENCRYPTION_SCHEME, encryptionScheme.getValue());
+ runner.setProperty(DecryptContentCompatibility.PASSWORD, PASSWORD);
+
+ final Cipher cipher = getCipher(encryptionScheme);
+ final byte[] encrypted = getEncrypted(cipher, keyDerivationStrategy, EMPTY_SALT, RAW);
+ assertDecrypted(RAW, encrypted, encryptionScheme);
+ }
+
+ @ParameterizedTest
+ @EnumSource(CompatibilityModeEncryptionScheme.class)
+ void testRunKeyDerivationOpenSslSalted(final CompatibilityModeEncryptionScheme encryptionScheme) throws Exception {
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy = CompatibilityModeKeyDerivationStrategy.OPENSSL_EVP_BYTES_TO_KEY;
+ runner.setProperty(DecryptContentCompatibility.KEY_DERIVATION_STRATEGY, keyDerivationStrategy.getValue());
+ runner.setProperty(DecryptContentCompatibility.ENCRYPTION_SCHEME, encryptionScheme.getValue());
+ runner.setProperty(DecryptContentCompatibility.PASSWORD, PASSWORD);
+
+ final Cipher cipher = getCipher(encryptionScheme);
+ final byte[] encrypted = getEncrypted(cipher, keyDerivationStrategy, OPENSSL_SALT, RAW);
+ assertDecrypted(RAW, encrypted, encryptionScheme);
+ }
+
+ @Test
+ void testRunKeyDerivationOpenSslSaltedStream() throws Exception {
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy = CompatibilityModeKeyDerivationStrategy.OPENSSL_EVP_BYTES_TO_KEY;
+ runner.setProperty(DecryptContentCompatibility.KEY_DERIVATION_STRATEGY, keyDerivationStrategy.getValue());
+ runner.setProperty(DecryptContentCompatibility.ENCRYPTION_SCHEME, SIMPLE_ENCRYPTION_SCHEME.getValue());
+ runner.setProperty(DecryptContentCompatibility.PASSWORD, PASSWORD);
+
+ final Cipher cipher = getCipher(SIMPLE_ENCRYPTION_SCHEME);
+
+ final byte[] words = getWords();
+ final byte[] encrypted = getEncrypted(cipher, keyDerivationStrategy, OPENSSL_SALT, words);
+ assertDecrypted(words, encrypted, SIMPLE_ENCRYPTION_SCHEME);
+ }
+
+ @ParameterizedTest
+ @EnumSource(CompatibilityModeEncryptionScheme.class)
+ void testRunKeyDerivationJasyptStandard(final CompatibilityModeEncryptionScheme encryptionScheme) throws Exception {
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy = CompatibilityModeKeyDerivationStrategy.JASYPT_STANDARD;
+ runner.setProperty(DecryptContentCompatibility.KEY_DERIVATION_STRATEGY, keyDerivationStrategy.getValue());
+ runner.setProperty(DecryptContentCompatibility.ENCRYPTION_SCHEME, encryptionScheme.getValue());
+ runner.setProperty(DecryptContentCompatibility.PASSWORD, PASSWORD);
+
+ final Cipher cipher = getCipher(encryptionScheme);
+ final byte[] salt = getSalt(cipher, keyDerivationStrategy);
+ final byte[] encrypted = getEncrypted(cipher, keyDerivationStrategy, salt, RAW);
+ assertDecrypted(RAW, encrypted, encryptionScheme);
+ }
+
+ @Test
+ void testRunKeyDerivationJasyptStandardSaltMissing() throws Exception {
+ setSimpleEncryptionScheme();
+
+ final Cipher cipher = getCipher(SIMPLE_ENCRYPTION_SCHEME);
+ final byte[] encrypted = getEncrypted(cipher, CompatibilityModeKeyDerivationStrategy.JASYPT_STANDARD, EMPTY_SALT, RAW);
+
+ runner.enqueue(encrypted);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContentCompatibility.FAILURE);
+
+ assertErrorLogged();
+ }
+
+ @Test
+ void testRunKeyDerivationJasyptStandardFileEnd() {
+ setSimpleEncryptionScheme();
+
+ runner.enqueue(EMPTY_SALT);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContentCompatibility.FAILURE);
+
+ assertErrorLogged();
+ }
+
+ @Test
+ void testRunKeyDerivationJasyptStandardSaltLengthMissing() {
+ setSimpleEncryptionScheme();
+
+ runner.enqueue(WORD);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContentCompatibility.FAILURE);
+
+ assertErrorLogged();
+ }
+
+ private void setSimpleEncryptionScheme() {
+ runner.setProperty(DecryptContentCompatibility.KEY_DERIVATION_STRATEGY, CompatibilityModeKeyDerivationStrategy.JASYPT_STANDARD.getValue());
+ runner.setProperty(DecryptContentCompatibility.ENCRYPTION_SCHEME, SIMPLE_ENCRYPTION_SCHEME.getValue());
+ runner.setProperty(DecryptContentCompatibility.PASSWORD, PASSWORD);
+ }
+
+ private void assertErrorLogged() {
+ final List errorMessages = runner.getLogger().getErrorMessages();
+ final Optional firstErrorMessage = errorMessages.stream().findFirst();
+ assertTrue(firstErrorMessage.isPresent());
+
+ final LogMessage errorLogMessage = firstErrorMessage.get();
+ final String message = errorLogMessage.getMsg();
+ assertTrue(message.contains(SIMPLE_ENCRYPTION_SCHEME.getValue()));
+ }
+
+ private void assertDecrypted(final byte[] expected, final byte[] encrypted, final CompatibilityModeEncryptionScheme encryptionScheme) throws IOException {
+ runner.enqueue(encrypted);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContentCompatibility.SUCCESS);
+ final MockFlowFile flowFile = runner.getFlowFilesForRelationship(DecryptContentCompatibility.SUCCESS).get(0);
+
+ flowFile.assertContentEquals(expected);
+
+ flowFile.assertAttributeEquals(CipherAttributeKey.PBE_SCHEME, encryptionScheme.getValue());
+ flowFile.assertAttributeEquals(CipherAttributeKey.PBE_SYMMETRIC_CIPHER, encryptionScheme.getSymmetricCipher().getValue());
+ flowFile.assertAttributeEquals(CipherAttributeKey.PBE_DIGEST_ALGORITHM, encryptionScheme.getDigestAlgorithm().getValue());
+ }
+
+ private byte[] getSalt(final Cipher cipher, final CompatibilityModeKeyDerivationStrategy strategy) {
+ final int blockSize = cipher.getBlockSize();
+ final int saltLength = blockSize == BLOCK_SIZE_UNDEFINED ? strategy.getSaltStandardLength() : blockSize;
+ final byte[] salt = new byte[saltLength];
+ SECURE_RANDOM.nextBytes(salt);
+ return salt;
+ }
+
+ private byte[] getEncrypted(
+ final Cipher cipher,
+ final CompatibilityModeKeyDerivationStrategy keyDerivationStrategy,
+ final byte[] salt,
+ final byte[] raw
+ ) throws GeneralSecurityException, IOException {
+ initCipher(cipher, keyDerivationStrategy, salt);
+ final byte[] encrypted = cipher.doFinal(raw);
+
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ if (CompatibilityModeKeyDerivationStrategy.JASYPT_STANDARD == keyDerivationStrategy || salt.length == 0) {
+ outputStream.write(salt);
+ } else {
+ outputStream.write(keyDerivationStrategy.getSaltHeader());
+ outputStream.write(salt);
+ }
+
+ outputStream.write(encrypted);
+ return outputStream.toByteArray();
+ }
+
+ private Cipher getCipher(final CompatibilityModeEncryptionScheme encryptionScheme) throws GeneralSecurityException {
+ final String algorithm = encryptionScheme.getValue();
+ return Cipher.getInstance(algorithm, BOUNCY_CASTLE_PROVIDER);
+ }
+
+ private void initCipher(final Cipher cipher, final CompatibilityModeKeyDerivationStrategy strategy, final byte[] salt) throws GeneralSecurityException {
+ final String algorithm = cipher.getAlgorithm();
+ final Key key = getKey(algorithm);
+ final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, strategy.getIterations());
+ cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
+ }
+
+ private Key getKey(final String algorithm) throws GeneralSecurityException {
+ final SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm, BOUNCY_CASTLE_PROVIDER);
+ final PBEKeySpec keySpec = new PBEKeySpec(PASSWORD.toCharArray());
+ return secretKeyFactory.generateSecret(keySpec);
+ }
+
+ private byte[] getWords() throws IOException {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ for (int i = 0; i < WORD_COUNT; i++) {
+ outputStream.write(WORD);
+ }
+
+ return outputStream.toByteArray();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/DecryptContentTest.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/DecryptContentTest.java
new file mode 100644
index 0000000000..6dcdeffabd
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/DecryptContentTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.cipher;
+
+import org.apache.nifi.processors.cipher.algorithm.CipherAlgorithmMode;
+import org.apache.nifi.processors.cipher.algorithm.CipherAlgorithmPadding;
+import org.apache.nifi.processors.cipher.encoded.EncodedDelimiter;
+import org.apache.nifi.processors.cipher.encoded.KeySpecificationFormat;
+import org.apache.nifi.util.LogMessage;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.bouncycastle.util.Arrays;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DecryptContentTest {
+
+ private static final byte[] WORD = {'W', 'O', 'R', 'D'};
+
+ private static final byte[] INITIALIZATION_VECTOR = {'I', 'N', 'I', 'T', 'I', 'A', 'L', 'I', 'Z', 'A', 'T', 'I', 'O', 'N', 'I', 'V'};
+
+ private static final String HEXADECIMAL_KEY_SPECIFICATION = "0123456789abcdef0123456789abcdef";
+
+ private static final String SECRET_KEY_SPECIFICATION = "ContentEncrypted";
+
+ private static final String UNENCRYPTED = "unencrypted";
+
+ private static final String ARGON2_PARAMETERS = "$argon2id$v=19$m=65536,t=5,p=8$OCXIIeGgaovfpBZXCDxGDg";
+
+ private static final String ARGON2_IV_ENCODED = "veGFZ+EWI0qShgT5AUJ+Qg==";
+
+ private static final String ARGON2_CIPHERED_ENCODED = "I32fSCCeZEkKFwP04MtnK9rbL283yXBTis4T";
+
+ private static final String BCRYPT_PARAMETERS = "$2a$12$JhHkHwk6ojTSWn9seyQV2O";
+
+ private static final String BCRYPT_IV_ENCODED = "8bDMMSPI+dKfNL3iC3hBow==";
+
+ private static final String BCRYPT_CIPHERED_ENCODED = "IeNNMCulpLoehwKg/A0e5dhIztA6fpqtBENl";
+
+ private static final String PBKDF2_SALT_ENCODED = "yoHJ1TbaLMc9qpDNAhV5bQ==";
+
+ private static final String PBKDF2_IV_ENCODED = "tgN6TnR6EbXnjKUBT4mq6Q==";
+
+ private static final String PBKDF2_CIPHERED_ENCODED = "++5mqUPqtG4bXNwJ7ruq4cSWIncOpFhiQRDR";
+
+ private static final String SCRYPT_PARAMETERS = "$s0$e0801$RWV0Tnr5u0fbQ3oPsHG9FA";
+
+ private static final String SCRYPT_IV_ENCODED = "oZxuUWK5le9eCkkckKoLhw==";
+
+ private static final String SCRYPT_CIPHERED_ENCODED = "Z4de2Bo+Zs5tpcgSp8jasXuea8tJl+vj6wqh";
+
+ private static final String HEXADECIMAL_KEY_IV_ENCODED = "z5sz+mXn3GdEWq/qvFUbvw==";
+
+ private static final String HEXADECIMAL_KEY_CIPHERED_ENCODED = "GtpE6cPs8zSYO2U=";
+
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+
+ TestRunner runner;
+
+ @BeforeEach
+ void setRunner() {
+ runner = TestRunners.newTestRunner(DecryptContent.class);
+ }
+
+ @Test
+ void testRunInitializationVectorNotFound() {
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION_FORMAT, KeySpecificationFormat.PASSWORD.getValue());
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION, SECRET_KEY_SPECIFICATION);
+
+ runner.enqueue(WORD);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContent.FAILURE);
+ assertErrorLogged();
+ }
+
+ @Test
+ void testRunInputNotFound() {
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION_FORMAT, KeySpecificationFormat.RAW.getValue());
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION, HEXADECIMAL_KEY_SPECIFICATION);
+
+ final byte[] bytes = Arrays.concatenate(INITIALIZATION_VECTOR, EncodedDelimiter.IV.getDelimiter());
+ runner.enqueue(bytes);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContent.FAILURE);
+ assertErrorLogged();
+ }
+
+ @Test
+ void testRunInputInvalid() {
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION_FORMAT, KeySpecificationFormat.RAW.getValue());
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION, HEXADECIMAL_KEY_SPECIFICATION);
+
+ final byte[] bytes = Arrays.concatenate(INITIALIZATION_VECTOR, EncodedDelimiter.IV.getDelimiter(), INITIALIZATION_VECTOR);
+ runner.enqueue(bytes);
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(DecryptContent.FAILURE);
+ assertErrorLogged();
+ }
+
+ @Test
+ void testRunAesGcmNoPaddingPasswordArgon2() throws IOException {
+ setGcmNoPaddingPassword();
+
+ final byte[] encryptedBytes = getEncryptedBytes(ARGON2_PARAMETERS.getBytes(StandardCharsets.UTF_8), ARGON2_IV_ENCODED, ARGON2_CIPHERED_ENCODED);
+ runner.enqueue(encryptedBytes);
+ runner.run();
+
+ assertDecryptedSuccess();
+ }
+
+ @Test
+ void testRunAesGcmNoPaddingPasswordBcrypt() throws IOException {
+ setGcmNoPaddingPassword();
+
+ final byte[] encryptedBytes = getEncryptedBytes(BCRYPT_PARAMETERS.getBytes(StandardCharsets.UTF_8), BCRYPT_IV_ENCODED, BCRYPT_CIPHERED_ENCODED);
+ runner.enqueue(encryptedBytes);
+ runner.run();
+
+ assertDecryptedSuccess();
+ }
+
+ @Test
+ void testRunAesGcmNoPaddingPasswordPbkdf2() throws IOException {
+ setGcmNoPaddingPassword();
+
+ final byte[] encryptedBytes = getEncryptedBytes(decoder.decode(PBKDF2_SALT_ENCODED), PBKDF2_IV_ENCODED, PBKDF2_CIPHERED_ENCODED);
+ runner.enqueue(encryptedBytes);
+ runner.run();
+
+ assertDecryptedSuccess();
+ }
+
+ @Test
+ void testRunAesGcmNoPaddingPasswordScrypt() throws IOException {
+ setGcmNoPaddingPassword();
+
+ final byte[] encryptedBytes = getEncryptedBytes(SCRYPT_PARAMETERS.getBytes(StandardCharsets.UTF_8), SCRYPT_IV_ENCODED, SCRYPT_CIPHERED_ENCODED);
+ runner.enqueue(encryptedBytes);
+ runner.run();
+
+ assertDecryptedSuccess();
+ }
+
+ @Test
+ void testRunAesCtrNoPaddingRaw() throws IOException {
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION_FORMAT, KeySpecificationFormat.RAW.getValue());
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION, HEXADECIMAL_KEY_SPECIFICATION);
+ runner.setProperty(DecryptContent.CIPHER_ALGORITHM_MODE, CipherAlgorithmMode.CTR.getValue());
+ runner.setProperty(DecryptContent.CIPHER_ALGORITHM_PADDING, CipherAlgorithmPadding.NO_PADDING.getValue());
+
+ final byte[] encryptedBytes = getHexadecimalKeyEncryptedBytes();
+ runner.enqueue(encryptedBytes);
+ runner.run();
+
+ assertDecryptedSuccess();
+ }
+
+ private void setGcmNoPaddingPassword() {
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION_FORMAT, KeySpecificationFormat.PASSWORD.getValue());
+ runner.setProperty(DecryptContent.KEY_SPECIFICATION, SECRET_KEY_SPECIFICATION);
+ runner.setProperty(DecryptContent.CIPHER_ALGORITHM_MODE, CipherAlgorithmMode.GCM.getValue());
+ runner.setProperty(DecryptContent.CIPHER_ALGORITHM_PADDING, CipherAlgorithmPadding.NO_PADDING.getValue());
+ }
+
+ private void assertDecryptedSuccess() {
+ runner.assertAllFlowFilesTransferred(DecryptContent.SUCCESS);
+ final MockFlowFile flowFile = runner.getFlowFilesForRelationship(DecryptContent.SUCCESS).get(0);
+ flowFile.assertContentEquals(UNENCRYPTED);
+ }
+
+ private void assertErrorLogged() {
+ final List errorMessages = runner.getLogger().getErrorMessages();
+ final Optional firstErrorMessage = errorMessages.stream().findFirst();
+ assertTrue(firstErrorMessage.isPresent());
+ }
+
+ private byte[] getEncryptedBytes(
+ final byte[] serializedParameters,
+ final String initializationVectorEncoded,
+ final String cipheredEncoded
+ ) throws IOException {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ outputStream.write(serializedParameters);
+ outputStream.write(EncodedDelimiter.SALT.getDelimiter());
+
+ final byte[] initializationVector = decoder.decode(initializationVectorEncoded);
+ outputStream.write(initializationVector);
+ outputStream.write(EncodedDelimiter.IV.getDelimiter());
+
+ final byte[] ciphered = decoder.decode(cipheredEncoded);
+ outputStream.write(ciphered);
+
+ return outputStream.toByteArray();
+ }
+
+ private byte[] getHexadecimalKeyEncryptedBytes() throws IOException {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ final byte[] initializationVector = decoder.decode(HEXADECIMAL_KEY_IV_ENCODED);
+ outputStream.write(initializationVector);
+ outputStream.write(EncodedDelimiter.IV.getDelimiter());
+
+ final byte[] ciphered = decoder.decode(HEXADECIMAL_KEY_CIPHERED_ENCODED);
+ outputStream.write(ciphered);
+
+ return outputStream.toByteArray();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-cipher-bundle/pom.xml b/nifi-nar-bundles/nifi-cipher-bundle/pom.xml
new file mode 100644
index 0000000000..f9291460c8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-cipher-bundle/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.nifi
+ nifi-nar-bundles
+ 1.20.0-SNAPSHOT
+
+
+ nifi-cipher-bundle
+ pom
+
+
+ nifi-cipher-processors
+ nifi-cipher-nar
+
+
diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml
index c8a53d5b6b..d1c149be4f 100755
--- a/nifi-nar-bundles/pom.xml
+++ b/nifi-nar-bundles/pom.xml
@@ -123,6 +123,7 @@
nifi-iceberg-bundlenifi-jslt-bundlenifi-iotdb-bundle
+ nifi-cipher-bundle