diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/VerifyContentMAC.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/VerifyContentMAC.java new file mode 100644 index 0000000000..0dc406dad0 --- /dev/null +++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/main/java/org/apache/nifi/processors/cipher/VerifyContentMAC.java @@ -0,0 +1,279 @@ +/* + * 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 static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.Encoding.BASE64; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.Encoding.HEXADECIMAL; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.Encoding.UTF8; + +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.nifi.annotation.behavior.InputRequirement; +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.annotation.lifecycle.OnScheduled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.expression.ExpressionLanguageScope; +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.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; +import org.bouncycastle.util.encoders.Hex; + +@SupportsBatching +@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) +@Tags({"Authentication", "Signing", "MAC", "HMAC"}) +@CapabilityDescription("Calculates a Message Authentication Code using the provided Secret Key and compares it with the provided MAC property") +@WritesAttributes({ + @WritesAttribute(attribute = VerifyContentMAC.MAC_CALCULATED_ATTRIBUTE, description = "Calculated Message Authentication Code encoded by the selected encoding"), + @WritesAttribute(attribute = VerifyContentMAC.MAC_ENCODING_ATTRIBUTE, description = "The Encoding of the Hashed Message Authentication Code"), + @WritesAttribute(attribute = VerifyContentMAC.MAC_ALGORITHM_ATTRIBUTE, description = "Hashed Message Authentication Code Algorithm") +}) +public class VerifyContentMAC extends AbstractProcessor { + + protected static final String HMAC_SHA256 = "HmacSHA256"; + protected static final String HMAC_SHA512 = "HmacSHA512"; + + protected static final PropertyDescriptor MAC_ALGORITHM = new PropertyDescriptor.Builder() + .name("mac-algorithm") + .displayName("Message Authentication Code Algorithm") + .description("Hashed Message Authentication Code Function") + .allowableValues(HMAC_SHA256, HMAC_SHA512) + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected static final PropertyDescriptor MAC_ENCODING = new PropertyDescriptor.Builder() + .name("message-authentication-code-encoding") + .displayName("Message Authentication Code Encoding") + .description("Encoding of the Message Authentication Code") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .allowableValues(HEXADECIMAL.name(), BASE64.name()) + .defaultValue(HEXADECIMAL.name()) + .build(); + + protected static final PropertyDescriptor MAC = new PropertyDescriptor.Builder() + .name("message-authentication-code") + .displayName("Message Authentication Code") + .description("The MAC to compare with the calculated value") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .build(); + + protected static final PropertyDescriptor SECRET_KEY_ENCODING = new PropertyDescriptor.Builder() + .name("secret-key-encoding") + .displayName("Secret Key Encoding") + .description("Encoding of the Secret Key") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .allowableValues(UTF8.name(), HEXADECIMAL.name(), BASE64.name()) + .defaultValue(HEXADECIMAL.name()) + .build(); + + protected static final PropertyDescriptor SECRET_KEY = new PropertyDescriptor.Builder() + .name("secret-key") + .displayName("Secret Key") + .description("Secret key to calculate the hash") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .sensitive(true) + .build(); + + protected static final Relationship FAILURE = new Relationship.Builder() + .name("failure") + .description("Signature Verification Failed") + .build(); + + protected static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .description("Signature Verification Succeeded") + .build(); + + protected static final String MAC_CALCULATED_ATTRIBUTE = "mac.calculated"; + protected static final String MAC_ALGORITHM_ATTRIBUTE = "mac.algorithm"; + protected static final String MAC_ENCODING_ATTRIBUTE = "mac.encoding"; + private static final List PROPERTIES = Collections.unmodifiableList( + Arrays.asList( + MAC_ALGORITHM, + MAC_ENCODING, + MAC, + SECRET_KEY_ENCODING, + SECRET_KEY + ) + ); + private static final Set RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(SUCCESS, FAILURE))); + private static final int BUFFER_SIZE = 512000; + + private SecretKeySpec secretKeySpec; + private String macAlgorithm; + private String macEncoding; + + @OnScheduled + public void setUp(ProcessContext context) { + macAlgorithm = context.getProperty(MAC_ALGORITHM).getValue(); + macEncoding = context.getProperty(MAC_ENCODING).getValue(); + String secretKeyEncoding = context.getProperty(SECRET_KEY_ENCODING).getValue(); + String inputSecretKey = context.getProperty(SECRET_KEY).getValue(); + + byte[] secretKey = Encoding.valueOf(secretKeyEncoding).decode(inputSecretKey); + + secretKeySpec = new SecretKeySpec(secretKey, macAlgorithm); + } + + @Override + public Set getRelationships() { + return RELATIONSHIPS; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) { + FlowFile flowFile = session.get(); + if (flowFile == null) { + return; + } + + final String macEncoded = context.getProperty(MAC).evaluateAttributeExpressions(flowFile).getValue(); + + try { + final byte[] macDecoded = Encoding.valueOf(macEncoding).decode(macEncoded); + final byte[] macCalculated = getCalculatedMac(session, flowFile); + + flowFile = setFlowFileAttributes(session, flowFile, macCalculated); + + if (MessageDigest.isEqual(macDecoded, macCalculated)) { + session.transfer(flowFile, SUCCESS); + } else { + getLogger().info("Verification Failed with Message Authentication Code Algorithm [{}]", macAlgorithm); + session.transfer(flowFile, FAILURE); + } + } catch (final Exception e) { + getLogger().error("Processing Failed with Message Authentication Code Algorithm [{}]", macAlgorithm, e); + session.transfer(flowFile, FAILURE); + } + } + + @Override + protected List getSupportedPropertyDescriptors() { + return PROPERTIES; + } + + @Override + protected Collection customValidate(ValidationContext validationContext) { + final List results = new ArrayList<>(super.customValidate(validationContext)); + + final String secretKeyEncoding = validationContext.getProperty(SECRET_KEY_ENCODING).getValue(); + final String encodedSecretKey = validationContext.getProperty(SECRET_KEY).getValue(); + + try { + Encoding.valueOf(secretKeyEncoding).decode(encodedSecretKey); + } catch (Exception e) { + results.add(new ValidationResult.Builder() + .valid(false) + .subject(SECRET_KEY.getDisplayName()) + .explanation("The provided Secret Key is not a valid " + secretKeyEncoding + " value") + .build()); + } + + return results; + } + + private FlowFile setFlowFileAttributes(ProcessSession session, FlowFile flowFile, byte[] calculatedMac) { + Map attributes = new HashMap<>(); + attributes.put(MAC_ALGORITHM_ATTRIBUTE, macAlgorithm); + attributes.put(MAC_ENCODING_ATTRIBUTE, macEncoding); + attributes.put(MAC_CALCULATED_ATTRIBUTE, Encoding.valueOf(macEncoding).encode(calculatedMac)); + return session.putAllAttributes(flowFile, attributes); + } + + private Mac getInitializedMac() { + try { + Mac mac = Mac.getInstance(macAlgorithm); + mac.init(secretKeySpec); + return mac; + } catch (final NoSuchAlgorithmException | InvalidKeyException e) { + throw new ProcessException("HMAC initialization failed", e); + } + } + + private byte[] getCalculatedMac(ProcessSession session, FlowFile flowFile) { + Mac mac = getInitializedMac(); + + byte[] contents = new byte[BUFFER_SIZE]; + int readSize; + + try (InputStream is = session.read(flowFile)) { + while ((readSize = is.read(contents)) != -1) { + mac.update(contents, 0, readSize); + } + } catch (final IOException e) { + throw new ProcessException("File processing failed", e); + } + + return mac.doFinal(); + } + + enum Encoding { + HEXADECIMAL(Hex::toHexString, Hex::decode), + BASE64(value -> Base64.getEncoder().encodeToString(value), value -> Base64.getDecoder().decode(value)), + UTF8(value -> new String(value, UTF_8), value -> value.getBytes(UTF_8)); + + private final Function encodeFunction; + private final Function decodeFunction; + + Encoding(Function encodeFunction, Function decodeFunction) { + this.decodeFunction = decodeFunction; + this.encodeFunction = encodeFunction; + } + + public byte[] decode(String value) { + return decodeFunction.apply(value); + } + + public String encode(byte[] value) { + return encodeFunction.apply(value); + } + } +} 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 index 9340751c56..9952db3a9f 100644 --- 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 @@ -14,3 +14,4 @@ # limitations under the License. org.apache.nifi.processors.cipher.DecryptContentCompatibility org.apache.nifi.processors.cipher.DecryptContent +org.apache.nifi.processors.cipher.VerifyContentMAC diff --git a/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/VerifyContentMACTest.java b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/VerifyContentMACTest.java new file mode 100644 index 0000000000..38b35a232e --- /dev/null +++ b/nifi-nar-bundles/nifi-cipher-bundle/nifi-cipher-processors/src/test/java/org/apache/nifi/processors/cipher/VerifyContentMACTest.java @@ -0,0 +1,179 @@ +/* + * 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 static org.apache.nifi.processors.cipher.VerifyContentMAC.FAILURE; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.HMAC_SHA256; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.HMAC_SHA512; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.MAC_ALGORITHM_ATTRIBUTE; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.MAC_CALCULATED_ATTRIBUTE; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.MAC_ENCODING_ATTRIBUTE; +import static org.apache.nifi.processors.cipher.VerifyContentMAC.SUCCESS; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.nifi.processors.cipher.VerifyContentMAC.Encoding; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class VerifyContentMACTest { + + private static final String INVALID = "invalid"; + private static final String FLOW_CONTENT = "content"; + private static final String CONTENT_HMAC_SHA256_HEXADECIMAL = "3c81220b10838ab972fd4f6796304dab1bb3ccdf65a0edc8c5ce7eef30191b6c"; + private static final String CONTENT_HMAC_SHA512_HEXADECIMAL = + "1e85c66a5ade958b0282632c9e0654c9a9f5985170ee53fbf9b55c536dbab768ddf835fe1afe11c37aeaec5f9751ab3762852fd33ea3c279a21ca98db99b3c62"; + private static final String CONTENT_HMAC_SHA256_BASE64 = "PIEiCxCDirly/U9nljBNqxuzzN9loO3Ixc5+7zAZG2w="; + private static final String CONTENT_HMAC_SHA512_BASE64 = "HoXGalrelYsCgmMsngZUyan1mFFw7lP7+bVcU226t2jd+DX+Gv4Rw3rq7F+XUas3YoUv0z6jwnmiHKmNuZs8Yg=="; + private static final String TEST_SECRET_KEY_UTF8 = "test"; + private static final String TEST_SECRET_KEY_HEXADECIMAL = "74657374"; + private static final String TEST_SECRET_KEY_BASE64 = "dGVzdA=="; + + private TestRunner runner; + + @BeforeEach + public void setRunner() { + runner = TestRunners.newTestRunner(new VerifyContentMAC()); + } + + @Test + void testNotValidWithoutMACAlgorithm() { + runner.setProperty(VerifyContentMAC.SECRET_KEY, TEST_SECRET_KEY_HEXADECIMAL); + runner.setProperty(VerifyContentMAC.MAC, CONTENT_HMAC_SHA256_HEXADECIMAL); + + runner.assertNotValid(); + } + + @Test + void testNotValidWithInvalidAlgorithm() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, INVALID); + runner.setProperty(VerifyContentMAC.SECRET_KEY, TEST_SECRET_KEY_HEXADECIMAL); + runner.setProperty(VerifyContentMAC.MAC, CONTENT_HMAC_SHA256_HEXADECIMAL); + + runner.assertNotValid(); + } + + @Test + void testNotValidWithoutSecretKey() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, HMAC_SHA256); + runner.setProperty(VerifyContentMAC.MAC, CONTENT_HMAC_SHA256_HEXADECIMAL); + + runner.assertNotValid(); + } + + @Test + void testNotValidWithoutProvidedMac() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, HMAC_SHA256); + runner.setProperty(VerifyContentMAC.SECRET_KEY, TEST_SECRET_KEY_HEXADECIMAL); + + runner.assertNotValid(); + } + + @Test + void testNotValidWhenSecretKeyEncodingIsHexadecimalButProvidedKeyIsNotValidHexadecimal() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, HMAC_SHA256); + runner.setProperty(VerifyContentMAC.SECRET_KEY, "not_hexadecimal"); + runner.setProperty(VerifyContentMAC.SECRET_KEY_ENCODING, Encoding.HEXADECIMAL.name()); + runner.setProperty(VerifyContentMAC.MAC, CONTENT_HMAC_SHA256_HEXADECIMAL); + + runner.assertNotValid(); + } + + @Test + void testNotValidWhenSecretKeyEncodingIsBase64ButProvidedKeyIsNotValidBase64() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, HMAC_SHA256); + runner.setProperty(VerifyContentMAC.SECRET_KEY, "not_base64"); + runner.setProperty(VerifyContentMAC.SECRET_KEY_ENCODING, Encoding.BASE64.name()); + runner.setProperty(VerifyContentMAC.MAC, CONTENT_HMAC_SHA256_HEXADECIMAL); + + runner.assertNotValid(); + } + + @ParameterizedTest(name = "macAlgorithm={0} secretKeyEncoding={1} secretKey={2} inputMac={3}") + @MethodSource("invalidConstructorArguments") + void testFlowFileTransferredToSuccessWhenMacMatch(String macAlgorithm, Encoding secretKeyEncoding, String secretKey, String inputMac, Encoding macEncoding) { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, macAlgorithm); + runner.setProperty(VerifyContentMAC.SECRET_KEY, secretKey); + runner.setProperty(VerifyContentMAC.SECRET_KEY_ENCODING, secretKeyEncoding.name()); + runner.setProperty(VerifyContentMAC.MAC_ENCODING, macEncoding.name()); + runner.setProperty(VerifyContentMAC.MAC, inputMac); + + runner.enqueue(FLOW_CONTENT); + + runner.run(); + + runner.assertAllFlowFilesTransferred(SUCCESS); + Map expectedAttributes = new HashMap<>(); + expectedAttributes.put(MAC_CALCULATED_ATTRIBUTE, inputMac); + expectedAttributes.put(MAC_ALGORITHM_ATTRIBUTE, macAlgorithm); + expectedAttributes.put(MAC_ENCODING_ATTRIBUTE, macEncoding.name()); + + runner.assertAttributes(SUCCESS, expectedAttributes.keySet(), Collections.singleton(expectedAttributes)); + } + + private static Stream invalidConstructorArguments() { + return Stream.of( + Arguments.of(HMAC_SHA256, Encoding.UTF8, TEST_SECRET_KEY_UTF8, CONTENT_HMAC_SHA256_HEXADECIMAL, Encoding.HEXADECIMAL), + Arguments.of(HMAC_SHA512, Encoding.UTF8, TEST_SECRET_KEY_UTF8, CONTENT_HMAC_SHA512_HEXADECIMAL, Encoding.HEXADECIMAL), + Arguments.of(HMAC_SHA256, Encoding.UTF8, TEST_SECRET_KEY_UTF8, CONTENT_HMAC_SHA256_BASE64, Encoding.BASE64), + Arguments.of(HMAC_SHA512, Encoding.UTF8, TEST_SECRET_KEY_UTF8, CONTENT_HMAC_SHA512_BASE64, Encoding.BASE64), + Arguments.of(HMAC_SHA256, Encoding.HEXADECIMAL, TEST_SECRET_KEY_HEXADECIMAL, CONTENT_HMAC_SHA256_HEXADECIMAL, Encoding.HEXADECIMAL), + Arguments.of(HMAC_SHA512, Encoding.HEXADECIMAL, TEST_SECRET_KEY_HEXADECIMAL, CONTENT_HMAC_SHA512_HEXADECIMAL, Encoding.HEXADECIMAL), + Arguments.of(HMAC_SHA256, Encoding.HEXADECIMAL, TEST_SECRET_KEY_HEXADECIMAL, CONTENT_HMAC_SHA256_BASE64, Encoding.BASE64), + Arguments.of(HMAC_SHA512, Encoding.HEXADECIMAL, TEST_SECRET_KEY_HEXADECIMAL, CONTENT_HMAC_SHA512_BASE64, Encoding.BASE64), + Arguments.of(HMAC_SHA256, Encoding.BASE64, TEST_SECRET_KEY_BASE64, CONTENT_HMAC_SHA256_HEXADECIMAL, Encoding.HEXADECIMAL), + Arguments.of(HMAC_SHA512, Encoding.BASE64, TEST_SECRET_KEY_BASE64, CONTENT_HMAC_SHA512_HEXADECIMAL, Encoding.HEXADECIMAL), + Arguments.of(HMAC_SHA256, Encoding.BASE64, TEST_SECRET_KEY_BASE64, CONTENT_HMAC_SHA256_BASE64, Encoding.BASE64), + Arguments.of(HMAC_SHA512, Encoding.BASE64, TEST_SECRET_KEY_BASE64, CONTENT_HMAC_SHA512_BASE64, Encoding.BASE64) + ); + } + + @Test + void testFlowFileTransferredToFailureInCaseOfVerificationFailure() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, HMAC_SHA256); + runner.setProperty(VerifyContentMAC.SECRET_KEY, TEST_SECRET_KEY_HEXADECIMAL); + runner.setProperty(VerifyContentMAC.MAC, CONTENT_HMAC_SHA512_HEXADECIMAL); + + runner.enqueue(FLOW_CONTENT); + + runner.run(); + + runner.assertAllFlowFilesTransferred(FAILURE); + } + + @Test + void testFlowFileTransferredToFailureInCaseOfException() { + runner.setProperty(VerifyContentMAC.MAC_ALGORITHM, HMAC_SHA256); + runner.setProperty(VerifyContentMAC.SECRET_KEY, TEST_SECRET_KEY_HEXADECIMAL); + runner.setProperty(VerifyContentMAC.MAC, INVALID); + + runner.enqueue(FLOW_CONTENT); + + runner.run(); + + runner.assertAllFlowFilesTransferred(FAILURE); + } + +} \ No newline at end of file