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-SNAPSHOT nar + + org.apache.nifi + nifi-cipher-nar + 1.20.0-SNAPSHOT + nar + org.apache.nifi nifi-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 reader = getReader(buffer); + return reader.read(serializedParameters); + } + + private DerivedKeyParameterSpecReader getReader(final ByteBuffer buffer) { + final DerivedKeyParameterSpecReader 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> 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 parameterSpecClass = derivedKeySpec.getParameterSpec().getClass(); + final DerivedKeyProvider derivedKeyProvider = findProvider(parameterSpecClass); + + return derivedKeyProvider.getDerivedKey(derivedKeySpec); + } + + @SuppressWarnings("unchecked") + private DerivedKeyProvider findProvider(final Class parameterSpecClass) { + final Class foundSpecClass = providers.keySet() + .stream() + .filter(specClass -> specClass.isAssignableFrom(parameterSpecClass)) + .findFirst() + .orElseThrow(() -> new UnsupportedOperationException(String.format("Parameter Specification [%s] not supported", parameterSpecClass))); + + final DerivedKeyProvider 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 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-path nifi-repository-encryption nifi-schema-utils + nifi-security-crypto-key nifi-security-kerberos-api nifi-security-kerberos nifi-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. +

+

Cipher Algorithm Property Values

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cipher Algorithm ModeCipher Algorithm PaddingEncryptContent Encryption Algorithm
CBCNoPaddingAES_CBC_NO_PADDING
CBCPKCS5PaddingAES_CBC
CTRNoPaddingAES_CTR
GCMNoPaddingAES_GCM
+ + diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/docs/org.apache.nifi.processors.cipher.DecryptContentCompatibility/additionalDetails.html b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/docs/org.apache.nifi.processors.cipher.DecryptContentCompatibility/additionalDetails.html new file mode 100644 index 0000000000..962b8eea5b --- /dev/null +++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/resources/docs/org.apache.nifi.processors.cipher.DecryptContentCompatibility/additionalDetails.html @@ -0,0 +1,144 @@ + + + + + + DecryptContentCompatibility + + + +

Summary

+

+ 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

+ + + + + + + + + + + + + + + +
DecryptContentCompatibilityEncryptContent
JASYPT_STANDARDNiFi Legacy KDF
OPENSSL_EVP_BYTES_TO_KEYOpenSSL EVP_BytesToKey
+

Encryption Scheme Property Values

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DecryptContentCompatibilityEncryptContent
PBE_WITH_MD5_AND_AES_CBC_128MD5_128AES
PBE_WITH_MD5_AND_AES_CBC_192MD5_192AES
PBE_WITH_MD5_AND_AES_CBC_256MD5_256AES
PBE_WITH_MD5_AND_DESMD5_DES
PBE_WITH_MD5_AND_RC2MD5_RC2
PBE_WITH_SHA1_AND_AES_CBC_128SHA_128AES
PBE_WITH_SHA1_AND_AES_CBC_192SHA_192AES
PBE_WITH_SHA1_AND_AES_CBC_256SHA_256AES
PBE_WITH_SHA1_AND_DESSHA1_DES
PBE_WITH_SHA1_AND_DESEDE_128SHA_2KEYTRIPLEDES
PBE_WITH_SHA1_AND_DESEDE_192SHA_3KEYTRIPLEDES
PBE_WITH_SHA1_AND_RC2SHA1_RC2
PBE_WITH_SHA1_AND_RC2_40SHA_40RC2
PBE_WITH_SHA1_AND_RC2_128SHA_128RC2
PBE_WITH_SHA1_AND_RC4_40SHA_40RC4
PBE_WITH_SHA1_AND_RC4_128SHA_128RC4
PBE_WITH_SHA1_AND_TWOFISHSHA_TWOFISH
PBE_WITH_SHA256_AND_AES_CBC_128SHA256_128AES
PBE_WITH_SHA256_AND_AES_CBC_192SHA256_192AES
PBE_WITH_SHA256_AND_AES_CBC_256SHA256_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-bundle nifi-jslt-bundle nifi-iotdb-bundle + nifi-cipher-bundle