diff --git a/.github/references/ubuntu_22_04_clang_arm_manifest.json b/.github/references/ubuntu_22_04_clang_arm_manifest.json index 54225fe9c8..ff474ed25a 100644 --- a/.github/references/ubuntu_22_04_clang_arm_manifest.json +++ b/.github/references/ubuntu_22_04_clang_arm_manifest.json @@ -12059,6 +12059,203 @@ "version": "1.0.0" } }, +{ + "bundles": { + "componentManifest": { + "processors": [ + { + "propertyDescriptors": { + "Decryption Strategy": { + "name": "Decryption Strategy", + "description": "Strategy for writing files to success after decryption", + "validator": "VALID", + "required": "true", + "sensitive": "false", + "expressionLanguageScope": "NONE", + "defaultValue": "DECRYPTED", + "allowableValues": [ + { + "value": "DECRYPTED", + "displayName": "DECRYPTED" + }, + { + "value": "PACKAGED", + "displayName": "PACKAGED" + } + ] + }, + "Private Key Service": { + "typeProvidedByValue": { + "type": "minifi_pgp.controller_services.private_key_service.PGPPrivateKeyService", + "group": "org.apache.nifi.minifi.rust", + "artifact": "minifi_pgp" + }, + "name": "Private Key Service", + "description": "PGP Private Key Service for decrypting data encrypted with Public Key Encryption", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "NONE" + }, + "Symmetric Password": { + "name": "Symmetric Password", + "description": "Password used for decrypting data encrypted with Password-Based Encryption", + "validator": "VALID", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + } + }, + "inputRequirement": "INPUT_REQUIRED", + "isSingleThreaded": "false", + "supportedRelationships": [ + { + "name": "failure", + "description": "Decryption Failed" + }, + { + "name": "success", + "description": "Decryption Succeeded" + } + ], + "typeDescription": "Decrypt contents of OpenPGP messages. Using the Packaged Decryption Strategy preserves OpenPGP encoding to support subsequent signature verification.", + "supportsDynamicRelationships": "false", + "supportsDynamicProperties": "false", + "type": "minifi_pgp.processors.decrypt_content.DecryptContentPGP" + }, + { + "propertyDescriptors": { + "File Encoding": { + "name": "File Encoding", + "description": "File Encoding for encryption", + "validator": "VALID", + "required": "true", + "sensitive": "false", + "expressionLanguageScope": "NONE", + "defaultValue": "BINARY", + "allowableValues": [ + { + "value": "ASCII", + "displayName": "ASCII" + }, + { + "value": "BINARY", + "displayName": "BINARY" + } + ] + }, + "Public Key Search": { + "name": "Public Key Search", + "description": "PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "FLOWFILE_ATTRIBUTES" + }, + "Public Key Service": { + "typeProvidedByValue": { + "type": "minifi_pgp.controller_services.public_key_service.PGPPublicKeyService", + "group": "org.apache.nifi.minifi.rust", + "artifact": "minifi_pgp" + }, + "name": "Public Key Service", + "description": "PGP Public Key Service for encrypting data with Public Key Encryption", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "NONE" + }, + "Symmetric Password": { + "name": "Symmetric Password", + "description": "Password used for encrypting data with Password-Based Encryption", + "validator": "VALID", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + } + }, + "inputRequirement": "INPUT_REQUIRED", + "isSingleThreaded": "false", + "supportedRelationships": [ + { + "name": "failure", + "description": "Encryption Failed" + }, + { + "name": "success", + "description": "Encryption Succeeded" + } + ], + "typeDescription": "Encrypt contents using OpenPGP.", + "supportsDynamicRelationships": "false", + "supportsDynamicProperties": "false", + "type": "minifi_pgp.processors.encrypt_content.EncryptContentPGP" + } + ], + "controllerServices": [ + { + "propertyDescriptors": { + "Key": { + "name": "Key", + "description": "Secret Key encoded in ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + }, + "Key File": { + "name": "Key File", + "description": "File path to PGP Secret Key encoded in binary or ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "FLOWFILE_ATTRIBUTES" + }, + "Key Passphrase": { + "name": "Key Passphrase", + "description": "Passphrase used for decrypting Private Keys", + "validator": "VALID", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + } + }, + "typeDescription": "PGP Private Key Service provides Private Keys loaded from files or properties", + "supportsDynamicRelationships": "false", + "supportsDynamicProperties": "false", + "type": "minifi_pgp.controller_services.private_key_service.PGPPrivateKeyService" + }, + { + "propertyDescriptors": { + "Keyring": { + "name": "Keyring", + "description": "PGP Keyring or Secret Key encoded in ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + }, + "Keyring File": { + "name": "Keyring File", + "description": "File path to PGP Keyring or Secret Key encoded in binary or ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "FLOWFILE_ATTRIBUTES" + } + }, + "typeDescription": "PGP Public Key Service providing Public Keys loaded from files", + "supportsDynamicRelationships": "false", + "supportsDynamicProperties": "false", + "type": "minifi_pgp.controller_services.public_key_service.PGPPublicKeyService" + } + ] + }, + "group": "org.apache.nifi.minifi.rust", + "artifact": "minifi_pgp", + "version": "1.0.0" + } +}, { "bundles": { "componentManifest": { diff --git a/minifi_rust/extensions/minifi_pgp/.cargo/config.toml b/minifi_rust/extensions/minifi_pgp/.cargo/config.toml new file mode 100644 index 0000000000..cb8c02ddc4 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.aarch64-apple-darwin] +rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] + +[target.x86_64-apple-darwin] +rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] diff --git a/minifi_rust/extensions/minifi_pgp/.gitignore b/minifi_rust/extensions/minifi_pgp/.gitignore new file mode 100644 index 0000000000..f9f6d205fa --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/.gitignore @@ -0,0 +1,7 @@ +target +output +features/.venv +features/output +integration_tests/features/.venv +integration_tests/features/linux_so +integration_tests/.venv \ No newline at end of file diff --git a/minifi_rust/extensions/minifi_pgp/Cargo.toml b/minifi_rust/extensions/minifi_pgp/Cargo.toml new file mode 100644 index 0000000000..6edd4d3b72 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "minifi_pgp" +version = "1.0.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +minifi_native = { path = "../../minifi_native" } +strum_macros = "0.28.0" +strum = "0.28.0" +pgp = "0.20.0" +rand = "0.8.6" # pgp 0.20.0 doesnt support >= 0.9 rand yet diff --git a/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature b/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature new file mode 100644 index 0000000000..3914e71308 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature @@ -0,0 +1,54 @@ +@SUPPORTS_WINDOWS +Feature: Test PGP extension's encryption and decryption capabilities + + Background: The pgp library is successfully built on linux + + Scenario: The pgp library is loaded into minifi + Given log property "logger.org::apache::nifi::minifi::core::extension::ExtensionManager" is set to "TRACE,stderr" + And log property "logger.org::apache::nifi::minifi::core::ClassLoader" is set to "TRACE,stderr" + + When the MiNiFi instance starts up + + Then the Minifi logs contain the following message: "Registering class 'EncryptContentPGP' at '/minifi_pgp'" in less than 10 seconds + And the Minifi logs contain the following message: "Registering class 'DecryptContentPGP' at '/minifi_pgp'" in less than 1 seconds + And the Minifi logs contain the following message: "Registering class 'PGPPublicKeyService' at '/minifi_pgp'" in less than 1 seconds + And the Minifi logs contain the following message: "Registering class 'PGPPrivateKeyService' at '/minifi_pgp'" in less than 1 seconds + And the Minifi logs do not contain errors + And the Minifi logs do not contain warnings + + Scenario: Encrypted for Alice but not for Bob + Given log property "logger.minifi_pgp::processors::decrypt_content::DecryptContentPGP" is set to "TRACE,stderr" + And log property "logger.minifi_pgp::processors::encrypt_content::EncryptContentPGP" is set to "TRACE,stderr" + + And a GetFile processor with the "Input Directory" property set to "/tmp/input" + And an EncryptContentPGP processor with a PGPPublicKeyService is set up + And a DecryptContentPGP processor named DecryptAlice with a PGPPrivateKeyService is set up for Alice + And a DecryptContentPGP processor named DecryptBob with a PGPPrivateKeyService is set up for Bob + And a PutFile processor with the name "AliceSuccess" + And a PutFile processor with the name "BobFailure" + + And these processor properties are set + | processor name | property name | property value | + | EncryptContentPGP | File Encoding | ASCII | + | EncryptContentPGP | Public Key Search | Alice | + | AliceSuccess | Directory | /tmp/output/alice_ok | + | BobFailure | Directory | /tmp/output/bob_fail | + + And the processors are connected up as described here + | source name | relationship name | destination name | + | GetFile | success | EncryptContentPGP | + | EncryptContentPGP | success | DecryptAlice | + | EncryptContentPGP | success | DecryptBob | + | DecryptAlice | success | AliceSuccess | + | DecryptBob | failure | BobFailure | + + And AliceSuccess's success relationship is auto-terminated + And BobFailure's success relationship is auto-terminated + + And a directory at "/tmp/input" has a file "test_file.log" with the content "test content" + + When the MiNiFi instance starts up + + Then at least one file with the content "test content" is placed in the "/tmp/output/alice_ok" directory in less than 5 seconds + And an encrypted armored pgp file is placed in the "/tmp/output/bob_fail" directory in less than 5 seconds + And the Minifi logs do not contain errors diff --git a/minifi_rust/extensions/minifi_pgp/features/environment.py b/minifi_rust/extensions/minifi_pgp/features/environment.py new file mode 100644 index 0000000000..cdec449069 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/features/environment.py @@ -0,0 +1,73 @@ +import os +from typing import List + +from minifi_behave.containers.docker_image_builder import DockerImageBuilder +from minifi_behave.core.hooks import common_after_scenario +from minifi_behave.core.hooks import common_before_scenario, get_minifi_container_image +from minifi_behave.core.minifi_test_context import MinifiTestContext + + +def add_extension_to_minifi_container( + extension_name: str, possible_paths: List[str], context: MinifiTestContext +): + new_container_name = f"apacheminificpp:{extension_name}" + is_windows = os.name == "nt" + if is_windows: + lib_filename = f"{extension_name}.dll" + container_extension_dir = ( + "C:/Program Files/ApacheNiFiMiNiFi/nifi-minifi-cpp/extensions" + ) + else: + lib_filename = f"lib{extension_name}.so" + container_extension_dir = "/opt/minifi/minifi-current/extensions/" + + host_path = None + for path in possible_paths: + if os.path.exists(os.path.join(path, lib_filename)): + host_path = os.path.join(path, lib_filename) + break + + assert host_path is not None, ( + f"Could not find {lib_filename} in {[p for p in possible_paths]}" + ) + + with open(host_path, "rb") as f: + lib_content = f.read() + + base_img = get_minifi_container_image() + + if is_windows: + dockerfile = f""" +FROM {base_img} +COPY ["{lib_filename}", "{container_extension_dir}/{lib_filename}"] +""" + else: + dockerfile = f""" +FROM {base_img} +COPY --chown=minificpp:minificpp {lib_filename} {container_extension_dir} +RUN chmod 755 {container_extension_dir}{lib_filename} +""" + + builder = DockerImageBuilder( + image_tag=new_container_name, + dockerfile_content=dockerfile, + files_on_context={lib_filename: lib_content}, + ) + + builder.build() + return new_container_name + + +def before_all(context): + dir_path = os.path.dirname(os.path.realpath(__file__)) + build_path = os.path.normpath(os.path.join(dir_path, "../../../target/release/")) + add_extension_to_minifi_container("minifi_pgp", [build_path], context) + + +def before_scenario(context, scenario): + context.minifi_container_image = "apacheminificpp:minifi_pgp" + common_before_scenario(context, scenario) + + +def after_scenario(context, scenario): + common_after_scenario(context, scenario) diff --git a/minifi_rust/extensions/minifi_pgp/features/steps/steps.py b/minifi_rust/extensions/minifi_pgp/features/steps/steps.py new file mode 100644 index 0000000000..b2a4d6e632 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/features/steps/steps.py @@ -0,0 +1,100 @@ +import os +from pathlib import Path + +import humanfriendly +from behave import step, then + +from minifi_behave.steps import checking_steps # noqa: F401 +from minifi_behave.steps import configuration_steps # noqa: F401 +from minifi_behave.steps import core_steps # noqa: F401 +from minifi_behave.steps import flow_building_steps # noqa: F401 +from minifi_behave.core.helpers import wait_for_condition +from minifi_behave.core.minifi_test_context import MinifiTestContext +from minifi_behave.minifi.controller_service import ControllerService +from minifi_behave.minifi.processor import Processor + + +@step("an EncryptContentPGP processor with a PGPPublicKeyService is set up") +def step_encrypt_content_with_service(context: MinifiTestContext): + dir_path = os.path.dirname(os.path.realpath(__file__)) + + public_key_service = ControllerService( + class_name="PGPPublicKeyService", service_name="my_public_keys" + ) + alice_public_key = Path(f"{dir_path}/../../test_keys/keyring.asc").read_text() + public_key_service.add_property("Keyring", alice_public_key) + context.get_or_create_default_minifi_container().flow_definition.controller_services.append( + public_key_service + ) + + processor = Processor("EncryptContentPGP", "EncryptContentPGP") + processor.add_property("Public Key Service", "my_public_keys") + context.get_or_create_default_minifi_container().flow_definition.processors.append( + processor + ) + + +@step( + "a DecryptContentPGP processor named DecryptAlice with a PGPPrivateKeyService is set up for Alice" +) +def step_decrypt_content_for_alice(context: MinifiTestContext): + dir_path = os.path.dirname(os.path.realpath(__file__)) + + private_key_service = ControllerService( + class_name="PGPPrivateKeyService", service_name="alice_private_key" + ) + alice_private_key = Path( + f"{dir_path}/../../test_keys/alice_private.asc" + ).read_text() + private_key_service.add_property("Key", alice_private_key) + private_key_service.add_property("Key Passphrase", "whiterabbit") + context.get_or_create_default_minifi_container().flow_definition.controller_services.append( + private_key_service + ) + + processor = Processor("DecryptContentPGP", "DecryptAlice") + processor.add_property("Private Key Service", "alice_private_key") + context.get_or_create_default_minifi_container().flow_definition.processors.append( + processor + ) + + +@step( + "a DecryptContentPGP processor named DecryptBob with a PGPPrivateKeyService is set up for Bob" +) +def step_decrypt_content_for_bob(context: MinifiTestContext): + dir_path = os.path.dirname(os.path.realpath(__file__)) + + private_key_service = ControllerService( + class_name="PGPPrivateKeyService", service_name="bob_private_key" + ) + bob_private_key = Path(f"{dir_path}/../../test_keys/bob_private.asc").read_text() + private_key_service.add_property("Key", bob_private_key) + context.get_or_create_default_minifi_container().flow_definition.controller_services.append( + private_key_service + ) + + processor = Processor("DecryptContentPGP", "DecryptBob") + processor.add_property("Private Key Service", "bob_private_key") + context.get_or_create_default_minifi_container().flow_definition.processors.append( + processor + ) + + +@then( + 'an encrypted armored pgp file is placed in the "{directory}" directory in less than {duration}' +) +def then_armored_pgp_file_in_dir( + context: MinifiTestContext, directory: str, duration: str +): + duration_seconds = humanfriendly.parse_timespan(duration) + assert wait_for_condition( + condition=lambda: ( + context.get_or_create_default_minifi_container().directory_contains_file_with_regex( + directory, "-----BEGIN PGP MESSAGE-----" + ) + ), + timeout_seconds=duration_seconds, + bail_condition=lambda: False, + context=context, + ) diff --git a/minifi_rust/extensions/minifi_pgp/minifi_pgp.md b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md new file mode 100644 index 0000000000..f2d9cbeab7 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md @@ -0,0 +1,120 @@ + + +## Table of Contents + +### Processors + +- [DecryptContentPGP](#DecryptContentPGP) +- [EncryptContentPGP](#EncryptContentPGP) +### Controller Services + +- [PGPPrivateKeyService](#PGPPrivateKeyService) +- [PGPPublicKeyService](#PGPPublicKeyService) + + +## DecryptContentPGP + +### Description + +Decrypt contents of OpenPGP messages. Using the Packaged Decryption Strategy preserves OpenPGP encoding to support subsequent signature verification. + +### Properties + +In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language. + +| Name | Default Value | Allowable Values | Description | +|---------------------|---------------|------------------------|-------------------------------------------------------------------------------------------------------------| +| Decryption Strategy | DECRYPTED | DECRYPTED
PACKAGED | Strategy for writing files to success after decryption | +| Symmetric Password | | | Password used for decrypting data encrypted with Password-Based Encryption
**Sensitive Property: true** | +| Private Key Service | | | PGP Private Key Service for decrypting data encrypted with Public Key Encryption | + +### Relationships + +| Name | Description | +|---------|----------------------| +| success | Decryption Succeeded | +| failure | Decryption Failed | + +### Output Attributes + +| Attribute | Relationship | Description | +|---------------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| pgp.literal.data.filename | success | Filename from decrypted Literal Data (Note that OpenPGP signatures do not include the formatting octet, the file name, and the date field of the Literal Data packet in a signature hash; therefore, those fields are not protected against tampering in a signed document. Therefore a lot of implementation omit these inherently malleable metadata) | +| pgp.literal.data.modified | success | Modified Date from decrypted Literal Data (Note that OpenPGP signatures do not include the formatting octet, the file name, and the date field of the Literal Data packet in a signature hash; therefore, those fields are not protected against tampering in a signed document. Therefore a lot of implementation omit these inherently malleable metadata) | + + +## EncryptContentPGP + +### Description + +Encrypt contents using OpenPGP. + +### Properties + +In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language. + +| Name | Default Value | Allowable Values | Description | +|--------------------|---------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **File Encoding** | BINARY | ASCII
BINARY | File Encoding for encryption | +| Symmetric Password | | | Password used for encrypting data with Password-Based Encryption
**Sensitive Property: true** | +| Public Key Search | | | PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters
**Supports Expression Language: true** | +| Public Key Service | | | PGP Public Key Service for encrypting data with Public Key Encryption | + +### Relationships + +| Name | Description | +|---------|----------------------| +| success | Encryption Succeeded | +| failure | Encryption Failed | + +### Output Attributes + +| Attribute | Relationship | Description | +|-------------------|--------------|---------------| +| pgp.file.encoding | success | File Encoding | + + +## PGPPrivateKeyService + +### Description + +PGP Private Key Service provides Private Keys loaded from files or properties + +### Properties + +In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language. + +| Name | Default Value | Allowable Values | Description | +|----------------|---------------|------------------|---------------------------------------------------------------------------------------------------------| +| Key File | | | File path to PGP Secret Key encoded in binary or ASCII Armor
**Supports Expression Language: true** | +| Key | | | Secret Key encoded in ASCII Armor
**Sensitive Property: true** | +| Key Passphrase | | | Passphrase used for decrypting Private Keys
**Sensitive Property: true** | + + +## PGPPublicKeyService + +### Description + +PGP Public Key Service providing Public Keys loaded from files + +### Properties + +In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language. + +| Name | Default Value | Allowable Values | Description | +|--------------|---------------|------------------|--------------------------------------------------------------------------------------------------------------------| +| Keyring File | | | File path to PGP Keyring or Secret Key encoded in binary or ASCII Armor
**Supports Expression Language: true** | +| Keyring | | | PGP Keyring or Secret Key encoded in ASCII Armor
**Sensitive Property: true** | diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs new file mode 100644 index 0000000000..4e0c876f01 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs @@ -0,0 +1,124 @@ +use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType}; +use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey}; + +pub(crate) struct SecretKeyFile {} + +impl PropertySchema for SecretKeyFile { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = false; +} + +impl PropertyType for SecretKeyFile { + type Output = Vec; + + fn parse(s: &str) -> Result { + let mut result: Vec = Vec::new(); + if let Ok((keys, _headers)) = SignedSecretKey::from_armor_file_many(s) { + result.extend(keys.filter_map(Result::ok)); + } else if let Ok(keys) = SignedSecretKey::from_file_many(s) { + result.extend(keys.filter_map(Result::ok)); + } + if result.is_empty() { + Err(MinifiError::validation( + "Couldnt load any valid secret keys", + )) + } else { + Ok(result) + } + } +} + +pub(crate) struct PublicKeyFile {} +impl PropertySchema for PublicKeyFile { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = false; +} + +impl PropertyType for PublicKeyFile { + type Output = Vec; + + fn parse(s: &str) -> Result { + let mut result: Vec = Vec::new(); + if let Ok((keys, _headers)) = SignedPublicKey::from_armor_file_many(s) { + result.extend(keys.filter_map(Result::ok)); + } else if let Ok(keys) = SignedPublicKey::from_file_many(s) { + result.extend(keys.filter_map(Result::ok)); + } + if result.is_empty() { + Err(MinifiError::validation( + "Couldnt load any valid public keys", + )) + } else { + Ok(result) + } + } +} + +#[cfg(test)] +mod secret_key_file_tests { + use super::*; + use crate::test_utils::get_test_key_path; + + fn assert_invalid_secret_key_file(file_name: &str) { + assert!(SecretKeyFile::parse(&get_test_key_path(file_name)).is_err()) + } + fn assert_valid_secret_key_file(file_name: &str) { + assert!( + !SecretKeyFile::parse(&get_test_key_path(file_name)) + .unwrap() + .is_empty() + ) + } + #[test] + fn test_invalid_secret_keyfiles() { + assert_invalid_secret_key_file("alice.asc"); + assert_invalid_secret_key_file("alice.gpg"); + assert_invalid_secret_key_file("garbage.gpg"); + assert_invalid_secret_key_file("truncated_private.asc"); + assert_invalid_secret_key_file("non_existent.asc"); + } + + #[test] + fn test_valid_secret_keyfiles() { + assert_valid_secret_key_file("alice_private.asc"); + assert_valid_secret_key_file("alice_private.gpg"); + assert_valid_secret_key_file("bob_private.asc"); + assert_valid_secret_key_file("bob_private.gpg"); + assert_valid_secret_key_file("secret_keyring.asc"); + assert_valid_secret_key_file("secret_keyring.gpg"); + } +} + +#[cfg(test)] +mod public_key_file_tests { + use crate::controller_services::key_file_property::PublicKeyFile; + use crate::test_utils::get_test_key_path; + use minifi_native::PropertyType; + + fn assert_invalid_public_key_file(file_name: &str) { + assert!(PublicKeyFile::parse(&get_test_key_path(file_name)).is_err()) + } + fn assert_valid_public_key_file(file_name: &str) { + assert!( + !PublicKeyFile::parse(&get_test_key_path(file_name)) + .unwrap() + .is_empty() + ) + } + #[test] + fn test_invalid_public_keyfiles() { + assert_invalid_public_key_file("alice_private.asc"); + assert_invalid_public_key_file("alice_private.gpg"); + assert_invalid_public_key_file("garbage.gpg"); + assert_invalid_public_key_file("truncated.asc"); + assert_invalid_public_key_file("non_existent.asc"); + } + + #[test] + fn test_valid_public_keyfiles() { + assert_valid_public_key_file("alice.asc"); + assert_valid_public_key_file("alice.gpg"); + assert_valid_public_key_file("keyring.asc"); + assert_valid_public_key_file("keyring.gpg"); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs new file mode 100644 index 0000000000..cff723a718 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs @@ -0,0 +1,56 @@ +use pgp::composed::SignedKeyDetails; +use pgp::types::KeyId; + +/// Returns true when `target_id` matches either: +/// - the key's Key ID formatted as 16-character hex (case-insensitive), or +/// - any of its User IDs as a case-insensitive substring match. +pub(crate) fn key_matches(key_id: &KeyId, details: &SignedKeyDetails, target_id: &str) -> bool { + let target = target_id.trim(); + if target.is_empty() { + return false; + } + + if key_id.to_string().eq_ignore_ascii_case(target) { + return true; + } + + let target_lower = target.to_ascii_lowercase(); + details.users.iter().any(|user| { + user.id + .as_str() + .map(|user_id| user_id.to_ascii_lowercase().contains(&target_lower)) + .unwrap_or(false) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn key_id_from_hex(hex: &str) -> KeyId { + let mut bytes = [0u8; 8]; + for (i, chunk) in hex.as_bytes().chunks(2).take(8).enumerate() { + bytes[i] = u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap(); + } + KeyId::from(bytes) + } + + #[test] + fn empty_target_never_matches() { + let details = SignedKeyDetails::new(vec![], vec![], vec![], vec![]); + let key_id = key_id_from_hex("1122334455667788"); + assert!(!key_matches(&key_id, &details, "")); + assert!(!key_matches(&key_id, &details, " ")); + } + + #[test] + fn matches_key_id_case_insensitive() { + let details = SignedKeyDetails::new(vec![], vec![], vec![], vec![]); + let key_id = key_id_from_hex("11ABcdEF33445566"); + + assert!(key_matches(&key_id, &details, "11abcdef33445566")); + assert!(key_matches(&key_id, &details, "11ABCDEF33445566")); + assert!(!key_matches(&key_id, &details, "11abcdef3344556")); // 15 chars + assert!(!key_matches(&key_id, &details, "abcdef33445566")); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs new file mode 100644 index 0000000000..b3d9aa07d0 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs @@ -0,0 +1,49 @@ +use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType}; +use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey}; + +pub(crate) struct SecretKey {} + +impl PropertySchema for SecretKey { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = false; +} + +impl PropertyType for SecretKey { + type Output = Vec; + + fn parse(s: &str) -> Result { + let mut secret_keys: Vec = Vec::new(); + if let Ok((keys, _headers)) = SignedSecretKey::from_armor_many(s.as_bytes()) { + secret_keys.extend(keys.filter_map(Result::ok)); + } + if secret_keys.is_empty() { + return Err(MinifiError::validation( + "Couldnt load any valid secrey keys", + )); + } + Ok(secret_keys) + } +} + +pub(crate) struct PublicKey {} +impl PropertySchema for PublicKey { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = false; +} + +impl PropertyType for PublicKey { + type Output = Vec; + + fn parse(s: &str) -> Result { + let mut public_keys: Vec = Vec::new(); + if let Ok((keys, _headers)) = SignedPublicKey::from_armor_many(s.as_bytes()) { + public_keys.extend(keys.filter_map(Result::ok)); + } + if public_keys.is_empty() { + return Err(MinifiError::validation( + "Couldnt load any valid public keys", + )); + } + Ok(public_keys) + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs new file mode 100644 index 0000000000..930207f53f --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs @@ -0,0 +1,5 @@ +mod key_file_property; +mod key_lookup; +mod key_property; +pub(crate) mod private_key_service; +pub(crate) mod public_key_service; diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs new file mode 100644 index 0000000000..848fa5603c --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs @@ -0,0 +1,203 @@ +mod controller_service_definition; +use controller_service_definition::*; + +#[cfg(test)] +use crate::controller_services::key_lookup::key_matches; +use minifi_native::macros::ComponentIdentifier; +use minifi_native::{EnableControllerService, GetProperty, Logger, MinifiError}; +use pgp::composed::{SignedSecretKey, TheRing}; +#[cfg(test)] +use pgp::types::KeyDetails; + +#[derive(Debug, ComponentIdentifier)] +pub(crate) struct PGPPrivateKeyService { + private_keys: Vec, + passphrase: pgp::types::Password, +} + +impl EnableControllerService for PGPPrivateKeyService { + fn enable(context: &P, _logger: &L) -> Result + where + Self: Sized, + { + let mut private_keys = context.get_property(&KEY_FILE)?.unwrap_or_default(); + private_keys.extend(context.get_property(&KEY)?.unwrap_or_default()); + + let passphrase = context.get_property(&KEY_PASSPHRASE)?.unwrap_or_default(); + + if private_keys.is_empty() { + return Err(MinifiError::validation("Could not load any valid keys")); + } + Ok(Self { + private_keys, + passphrase, + }) + } +} + +impl PGPPrivateKeyService { + pub fn get_the_ring(&'_ self) -> TheRing<'_> { + TheRing { + secret_keys: self.private_keys.iter().collect(), + key_passwords: vec![&self.passphrase], + message_password: vec![], + session_keys: vec![], + decrypt_options: Default::default(), + } + } + + #[cfg(test)] + pub fn get_secret_key(&self, target_id: &str) -> Option<&SignedSecretKey> { + self.private_keys.iter().find(|private_key| { + key_matches( + &private_key.primary_key.legacy_key_id(), + &private_key.details, + target_id, + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::get_test_key_path; + use minifi_native::{ComponentIdentifier, MockControllerServiceContext, MockLogger}; + + #[test] + fn test_component_id() { + assert_eq!( + PGPPrivateKeyService::CLASS_NAME, + "minifi_pgp::controller_services::private_key_service::PGPPrivateKeyService" + ); + assert_eq!(PGPPrivateKeyService::GROUP_NAME, "minifi_pgp"); + assert_eq!(PGPPrivateKeyService::VERSION, "1.0.0"); + } + + #[test] + fn default_fails() { + let context = MockControllerServiceContext::new(); + assert!(PGPPrivateKeyService::enable(&context, &MockLogger::new()).is_err()); + } + + #[test] + fn single_armored_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Key File".to_string(), + get_test_key_path("alice_private.asc"), + ); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_some()); + assert!(service.get_secret_key("alice@example.com").is_some()); + + assert!(service.get_secret_key("Bob").is_none()); + assert!(service.get_secret_key("Carol").is_none()); + } + + #[test] + fn single_binary_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Key File".to_string(), + get_test_key_path("alice_private.gpg"), + ); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("A").is_some()); + assert!(service.get_secret_key("Alice").is_some()); + assert!( + service + .get_secret_key("Alice ") + .is_some() + ); + + assert!(service.get_secret_key("").is_none()); + + assert!(service.get_secret_key("Bob").is_none()); + assert!(service.get_secret_key("Carol").is_none()); + } + + #[test] + fn armored_keyring_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Key File".to_string(), + get_test_key_path("secret_keyring.asc"), + ); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_some()); + assert!(service.get_secret_key("Bob").is_some()); + assert!(service.get_secret_key("bob@home.io").is_some()); + assert!(service.get_secret_key("bob@work.com").is_some()); + assert!(service.get_secret_key("Carol").is_none()); + } + + #[test] + fn binary_keyring_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Key File".to_string(), + get_test_key_path("secret_keyring.gpg"), + ); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_some()); + assert!(service.get_secret_key("Bob").is_some()); + assert!(service.get_secret_key("bob@home.io").is_some()); + assert!(service.get_secret_key("bob@work.com").is_some()); + assert!(service.get_secret_key("Carol").is_none()); + } + + #[test] + fn armored_keyring() { + let mut context = MockControllerServiceContext::new(); + + let file_content = std::fs::read_to_string(get_test_key_path("secret_keyring.asc")) + .expect("required for test"); + + context.properties.insert("Key".to_string(), file_content); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_some()); + assert!(service.get_secret_key("Bob").is_some()); + assert!(service.get_secret_key("bob@home.io").is_some()); + assert!(service.get_secret_key("bob@work.com").is_some()); + assert!(service.get_secret_key("Carol").is_none()); + } + + #[test] + fn armored_single_key() { + let mut context = MockControllerServiceContext::new(); + + let file_content = std::fs::read_to_string(get_test_key_path("alice_private.asc")) + .expect("required for test"); + + context.properties.insert("Key".to_string(), file_content); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_some()); + assert!(service.get_secret_key("Bob").is_none()); + assert!(service.get_secret_key("Carol").is_none()); + } + + #[test] + fn public_ascii_key() { + let mut context = MockControllerServiceContext::new(); + + let file_content = + std::fs::read_to_string(get_test_key_path("alice.asc")).expect("required for test"); + + context.properties.insert("Key".to_string(), file_content); + + assert!(PGPPrivateKeyService::enable(&context, &MockLogger::new()).is_err()); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service/controller_service_definition.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service/controller_service_definition.rs new file mode 100644 index 0000000000..fe421553ad --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service/controller_service_definition.rs @@ -0,0 +1,31 @@ +use super::PGPPrivateKeyService; +use crate::controller_services::key_file_property::SecretKeyFile; +use crate::controller_services::key_property::SecretKey; +use crate::utils; +use minifi_native::{ + ControllerServiceDefinition, Property, PropertyDefinition, ProvidedInterface, + property_definitions, +}; + +pub(super) const KEY_FILE: Property> = Property::new( + "Key File", + "File path to PGP Secret Key encoded in binary or ASCII Armor", +) +.supports_expression_language(); + +pub(super) const KEY: Property> = + Property::new("Key", "Secret Key encoded in ASCII Armor").sensitive(); + +pub(super) const KEY_PASSPHRASE: Property> = Property::new( + "Key Passphrase", + "Passphrase used for decrypting Private Keys", +) +.sensitive(); + +impl ControllerServiceDefinition for PGPPrivateKeyService { + const DESCRIPTION: &'static str = + "PGP Private Key Service provides Private Keys loaded from files or properties"; + const PROPERTIES: &'static [PropertyDefinition] = + property_definitions![KEY_FILE, KEY, KEY_PASSPHRASE]; + const PROVIDED_APIS: &'static [ProvidedInterface] = &[]; +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs new file mode 100644 index 0000000000..117abd1198 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs @@ -0,0 +1,214 @@ +mod controller_service_definition; +use controller_service_definition::*; + +use crate::controller_services::key_lookup::key_matches; +use minifi_native::macros::ComponentIdentifier; +use minifi_native::{EnableControllerService, GetProperty, Logger, MinifiError}; +use pgp::composed::SignedPublicKey; +use pgp::types::KeyDetails; + +#[derive(Debug, ComponentIdentifier, PartialEq)] +pub(crate) struct PGPPublicKeyService { + public_keys: Vec, +} + +impl EnableControllerService for PGPPublicKeyService { + fn enable(context: &P, _logger: &L) -> Result + where + Self: Sized, + { + let mut public_keys = context.get_property(&KEYRING_FILE)?.unwrap_or_default(); + public_keys.extend(context.get_property(&KEYRING)?.unwrap_or_default()); + + if public_keys.is_empty() { + return Err(MinifiError::validation("Could not load any valid keys")); + } + Ok(Self { public_keys }) + } +} + +impl PGPPublicKeyService { + pub fn get(&self, target_id: &str) -> Option<&SignedPublicKey> { + self.public_keys.iter().find(|public_key| { + key_matches( + &public_key.primary_key.legacy_key_id(), + &public_key.details, + target_id, + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::get_test_key_path; + + use minifi_native::{ComponentIdentifier, MockControllerServiceContext, MockLogger}; + + #[test] + fn test_component_id() { + assert_eq!( + PGPPublicKeyService::CLASS_NAME, + "minifi_pgp::controller_services::public_key_service::PGPPublicKeyService" + ); + assert_eq!(PGPPublicKeyService::GROUP_NAME, "minifi_pgp"); + assert_eq!(PGPPublicKeyService::VERSION, "1.0.0"); + } + + #[test] + fn default_fails() { + let context = MockControllerServiceContext::new(); + assert!(PGPPublicKeyService::enable(&context, &MockLogger::new()).is_err()); + } + + #[test] + fn armored_private_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Keyring File".to_string(), + get_test_key_path("alice_private.asc"), + ); + + assert!(PGPPublicKeyService::enable(&context, &MockLogger::new()).is_err()); + } + + #[test] + fn single_armored_key_file() { + let mut context = MockControllerServiceContext::new(); + context + .properties + .insert("Keyring File".to_string(), get_test_key_path("alice.asc")); + + let controller_service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + + assert!(controller_service.get("Alice").is_some()); + assert!(controller_service.get("alice@example.com").is_some()); + + assert!(controller_service.get("Bob").is_none()); + assert!(controller_service.get("Carol").is_none()); + } + + #[test] + fn single_binary_key_file() { + let mut context = MockControllerServiceContext::new(); + context + .properties + .insert("Keyring File".to_string(), get_test_key_path("alice.gpg")); + + let service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + assert!(service.get("A").is_some()); + assert!(service.get("Alice").is_some()); + assert!(service.get("Alice ").is_some()); + + assert!(service.get("").is_none()); + + assert!(service.get("Bob").is_none()); + assert!(service.get("Carol").is_none()); + } + + #[test] + fn armored_keyring_key_file() { + let mut context = MockControllerServiceContext::new(); + context + .properties + .insert("Keyring File".to_string(), get_test_key_path("keyring.asc")); + + let service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + assert!(service.get("Alice").is_some()); + assert!(service.get("Bob").is_some()); + assert!(service.get("bob@home.io").is_some()); + assert!(service.get("bob@work.com").is_some()); + assert!(service.get("Carol").is_none()); + } + + #[test] + fn binary_keyring_key_file() { + let mut context = MockControllerServiceContext::new(); + context + .properties + .insert("Keyring File".to_string(), get_test_key_path("keyring.gpg")); + + let service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + assert!(service.get("Alice").is_some()); + assert!(service.get("Bob").is_some()); + assert!(service.get("bob@home.io").is_some()); + assert!(service.get("bob@work.com").is_some()); + assert!(service.get("Carol").is_none()); + } + + #[test] + fn armored_keyring() { + let mut context = MockControllerServiceContext::new(); + + let file_content = + std::fs::read_to_string(get_test_key_path("keyring.asc")).expect("required for test"); + + context + .properties + .insert("Keyring".to_string(), file_content); + + let service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + assert!(service.get("Alice").is_some()); + assert!(service.get("Bob").is_some()); + assert!(service.get("bob@home.io").is_some()); + assert!(service.get("bob@work.com").is_some()); + assert!(service.get("Carol").is_none()); + } + + #[test] + fn armored_single_key() { + let mut context = MockControllerServiceContext::new(); + + let file_content = + std::fs::read_to_string(get_test_key_path("alice.asc")).expect("required for test"); + + context + .properties + .insert("Keyring".to_string(), file_content); + + let service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + assert!(service.get("Alice").is_some()); + assert!(service.get("Bob").is_none()); + assert!(service.get("Carol").is_none()); + } + + #[test] + fn private_ascii_key() { + let mut context = MockControllerServiceContext::new(); + + let file_content = std::fs::read_to_string(get_test_key_path("alice_private.asc")) + .expect("required for test"); + + context + .properties + .insert("Keyring".to_string(), file_content); + + assert!(PGPPublicKeyService::enable(&context, &MockLogger::new()).is_err()); + } + + #[test] + fn looks_up_by_key_id_hex() { + let mut context = MockControllerServiceContext::new(); + context + .properties + .insert("Keyring File".to_string(), get_test_key_path("alice.asc")); + + let service = PGPPublicKeyService::enable(&context, &MockLogger::new()) + .expect("enable should succeed"); + + let alice = service.get("Alice").expect("Alice should exist"); + let key_id_hex = alice.primary_key.legacy_key_id().to_string(); + assert_eq!(key_id_hex.len(), 16); + assert!(service.get(&key_id_hex).is_some()); + assert!(service.get(&key_id_hex.to_ascii_uppercase()).is_some()); + assert!(service.get(&key_id_hex[..8]).is_none()); + assert!(service.get("0123456789abcdef").is_none()); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service/controller_service_definition.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service/controller_service_definition.rs new file mode 100644 index 0000000000..099cd5fc1e --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service/controller_service_definition.rs @@ -0,0 +1,26 @@ +use super::PGPPublicKeyService; +use crate::controller_services::key_file_property::PublicKeyFile; +use crate::controller_services::key_property::PublicKey; +use minifi_native::{ + ControllerServiceDefinition, Property, PropertyDefinition, ProvidedInterface, + property_definitions, +}; + +pub(crate) const KEYRING_FILE: Property> = Property::new( + "Keyring File", + "File path to PGP Keyring or Secret Key encoded in binary or ASCII Armor", +) +.supports_expression_language(); + +pub(crate) const KEYRING: Property> = Property::new( + "Keyring", + "PGP Keyring or Secret Key encoded in ASCII Armor", +) +.sensitive(); + +impl ControllerServiceDefinition for PGPPublicKeyService { + const DESCRIPTION: &'static str = + "PGP Public Key Service providing Public Keys loaded from files"; + const PROPERTIES: &'static [PropertyDefinition] = property_definitions![KEYRING_FILE, KEYRING]; + const PROVIDED_APIS: &'static [ProvidedInterface] = &[]; +} diff --git a/minifi_rust/extensions/minifi_pgp/src/lib.rs b/minifi_rust/extensions/minifi_pgp/src/lib.rs new file mode 100644 index 0000000000..f8085bb072 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/lib.rs @@ -0,0 +1,24 @@ +mod controller_services; +mod processors; + +use crate::controller_services::private_key_service::PGPPrivateKeyService; +use crate::controller_services::public_key_service::PGPPublicKeyService; +use crate::processors::decrypt_content::DecryptContentPGP; +use crate::processors::encrypt_content::EncryptContentPGP; +use minifi_native::{FlowFileStreamTransformProcessorType, MultiThreaded}; + +minifi_native::declare_minifi_extension!( + group_name: "org.apache.nifi.minifi.rust", + processors: [ + (FlowFileStreamTransformProcessorType, MultiThreaded, EncryptContentPGP), + (FlowFileStreamTransformProcessorType, MultiThreaded, DecryptContentPGP), + ], + controllers: [ + PGPPublicKeyService, + PGPPrivateKeyService, + ] +); + +#[cfg(test)] +mod test_utils; +mod utils; diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs new file mode 100644 index 0000000000..506bc24ba6 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs @@ -0,0 +1,356 @@ +mod processor_definition; + +use processor_definition::*; + +use crate::controller_services::private_key_service::PGPPrivateKeyService; + +use minifi_native::macros::{ComponentIdentifier, PropertyType}; +use minifi_native::{ + FlowFileStreamTransform, GetControllerService, GetProperty, InputStream, Logger, MinifiError, + OutputStream, ProcessError, RouteErrorExt, Schedule, TransformStreamResult, +}; +use pgp::composed::{Message, TheRing}; +use std::fmt::Debug; +use strum_macros::{Display, EnumString, IntoStaticStr, VariantNames}; + +#[derive( + Debug, Clone, Copy, PartialEq, Display, EnumString, VariantNames, IntoStaticStr, PropertyType, +)] +#[strum(serialize_all = "UPPERCASE", const_into_str)] +enum DecryptionStrategy { + Decrypted, + Packaged, +} + +#[derive(Debug, ComponentIdentifier)] +pub(crate) struct DecryptContentPGP { + decompress_data: bool, + symmetric_password: Option, +} + +impl Schedule for DecryptContentPGP { + fn schedule(context: &P, _logger: &L) -> Result + where + Self: Sized, + L: Logger, + { + let decryption_strategy = context.get_property(&DECRYPTION_STRATEGY)?; + + let symmetric_password = context.get_property(&SYMMETRIC_PASSWORD)?; + let has_context_service = context.get_raw_property(&PRIVATE_KEY_SERVICE)?.is_some(); + if !has_context_service && symmetric_password.is_none() { + Err(MinifiError::validation( + "Either Symmetric Password or Private Key Service must be set", + )) + } else { + Ok(DecryptContentPGP { + decompress_data: decryption_strategy == DecryptionStrategy::Decrypted, + symmetric_password, + }) + } + } +} + +impl DecryptContentPGP { + fn decrypt_msg<'a>( + &'a self, + msg: Message<'a>, + private_key_service: Option<&'a PGPPrivateKeyService>, + ) -> pgp::errors::Result> { + let mut ring = if let Some(pks) = private_key_service { + pks.get_the_ring() + } else { + TheRing::default() + }; + + ring.decrypt_options = ring.decrypt_options.enable_gnupg_aead(); + + if let Some(sym_passwd) = &self.symmetric_password { + ring.message_password.push(sym_passwd); + } + let (decrypted_msg, _ring_result) = msg.decrypt_the_ring(ring, false)?; + Ok(decrypted_msg) + } + + fn extract_attributes_from_decrypted_message( + decrypted_msg: &Message, + ) -> Vec<(&'static str, String)> { + let mut res = Vec::new(); + if let Some(literal_data_header) = decrypted_msg.literal_data_header() { + if let Ok(file_name) = str::from_utf8(literal_data_header.file_name()) { + res.push((LITERAL_DATA_FILENAME.name, file_name.to_string())); + } + // NiFi uses ms timestamp + res.push(( + LITERAL_DATA_MODIFIED.name, + (1000u64 * literal_data_header.created().as_secs() as u64).to_string(), + )); + } + res + } +} + +impl FlowFileStreamTransform for DecryptContentPGP { + fn transform( + &self, + context: &Ctx, + input_stream: &mut dyn InputStream, + output_stream: &mut dyn OutputStream, + _logger: &LoggerImpl, + ) -> Result { + let private_key_service = context.get_controller_service(&PRIVATE_KEY_SERVICE)?; + + let msg = Message::from_reader(input_stream) + .map(|(msg, _header)| msg) + .route_err_to_failure()?; + + let mut decrypted_msg = self + .decrypt_msg(msg, private_key_service) + .route_err_to_failure()?; + + if self.decompress_data && decrypted_msg.is_compressed() { + decrypted_msg = decrypted_msg + .decompress() + .map_err(MinifiError::other) + .route_err_to_failure()? + }; + + let attributes_to_add = Self::extract_attributes_from_decrypted_message(&decrypted_msg); + let _written_bytes = + std::io::copy(&mut decrypted_msg.into_inner(), output_stream).route_err_to_failure()?; + + Ok(TransformStreamResult::new(&SUCCESS).with_attributes(attributes_to_add)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils; + use crate::test_utils::get_test_message; + use minifi_native::{ + ComponentIdentifier, EnableControllerService, IoState, MockControllerServiceContext, + MockLogger, MockProcessContext, test, + }; + #[test] + fn test_ids() { + assert_eq!( + DecryptContentPGP::CLASS_NAME, + "minifi_pgp::processors::decrypt_content::DecryptContentPGP" + ); + assert_eq!(DecryptContentPGP::GROUP_NAME, "minifi_pgp"); + assert_eq!(DecryptContentPGP::VERSION, "1.0.0"); + } + + #[test] + fn fails_to_schedule_by_default() { + let decrypt_content = + DecryptContentPGP::schedule(&MockProcessContext::new(), &MockLogger::new()); + assert!(decrypt_content.is_err()); + } + + #[test] + fn schedules_with_password() { + let mut context = MockProcessContext::new(); + context + .properties + .insert(SYMMETRIC_PASSWORD.name(), "my_secret_password".to_string()); + let decrypt_content = DecryptContentPGP::schedule(&context, &MockLogger::new()); + assert!(decrypt_content.is_ok()); + } + + #[test] + fn schedules_with_controller() { + let mut context = MockProcessContext::new(); + context.properties.insert( + PRIVATE_KEY_SERVICE.name(), + "my_private_key_service".to_string(), + ); + let decrypt_content = DecryptContentPGP::schedule(&context, &MockLogger::new()); + assert!(decrypt_content.is_ok()); + } + + #[test] + fn schedule_rejects_invalid_strategy_without_panicking() { + let mut context = MockProcessContext::new(); + context + .properties + .insert(DECRYPTION_STRATEGY.name(), "NOT_A_STRATEGY".to_string()); + context + .properties + .insert(SYMMETRIC_PASSWORD.name(), "my_secret_password".to_string()); + // Must return Err, not panic. + let result = DecryptContentPGP::schedule(&context, &MockLogger::new()); + assert!(result.is_err(), "expected schedule to fail on bad strategy"); + } + + #[derive(Copy, Clone)] + struct PrivateKeyData { + key_filename: &'static str, + passphrase: Option<&'static str>, + } + + impl PrivateKeyData { + fn into_controller(self) -> PGPPrivateKeyService { + let mut context = MockControllerServiceContext::new(); + context + .properties + .insert("Key File", test_utils::get_test_key_path(self.key_filename)); + + if let Some(passphrase) = self.passphrase { + context.properties.insert("Key Passphrase", passphrase); + } + + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable") + } + } + + fn test_decryption( + message_file_name: &str, + private_key_data: Option, + symmetric_password: Option<&'static str>, + expected_result: Result<&[u8], ()>, + ) { + let mut processor_context = MockProcessContext::new(); + if let Some(private_key) = private_key_data { + processor_context.controller_services.insert( + "my_private_key_service".to_string(), + Box::new(private_key.into_controller()), + ); + processor_context.properties.insert( + PRIVATE_KEY_SERVICE.name(), + "my_private_key_service".to_string(), + ); + } + if let Some(symmetric_password) = symmetric_password { + processor_context + .properties + .insert(SYMMETRIC_PASSWORD.name(), symmetric_password.to_string()); + } + + let decrypt_content = DecryptContentPGP::schedule(&processor_context, &MockLogger::new()) + .expect("Should schedule with the configured properties"); + let mut output: Vec = Vec::new(); + let mut flow_file_stream = std::io::Cursor::new(get_test_message(message_file_name)); + let res = decrypt_content.transform( + &processor_context, + &mut flow_file_stream, + &mut output, + &MockLogger::new(), + ); + + match expected_result { + Ok(_result_bytes) => { + let res = res.expect("Should be able to transform"); + assert_eq!(res.target_relationship_name(), SUCCESS.name); + assert_eq!(res.write_status(), IoState::Ok); + let data_modified = res + .get_attribute(LITERAL_DATA_MODIFIED.name) + .unwrap() + .parse::() + .expect("Should be u64"); + assert!(data_modified > 1770000000000); + assert!(data_modified < 1780000000000); + assert!(res.get_attribute(LITERAL_DATA_FILENAME.name).is_some()); + } + Err(_) => test::assert_routed_to(res, &FAILURE), + } + } + + #[test] + fn decrypts_with_password() { + test_decryption( + "password_encrypted_foo.gpg", + None, + Some("my_secret_password"), + Ok("foo\n".as_bytes()), + ); + test_decryption( + "password_encrypted_foo.asc", + None, + Some("my_secret_password"), + Ok("foo\n".as_bytes()), + ); + test_decryption( + "foo_for_alice.gpg", + None, + Some("my_secret_password"), + Err(()), + ); + test_decryption( + "foo_for_alice.asc", + None, + Some("my_secret_password"), + Err(()), + ); + } + + #[test] + fn decrypts_for_alice() { + let alice_private_key_data = PrivateKeyData { + key_filename: "alice_private.asc", + passphrase: Some("whiterabbit"), + }; + + test_decryption( + "foo_for_alice.asc", + Some(alice_private_key_data), + None, + Ok("foo\n".as_bytes()), + ); + + test_decryption( + "foo_for_alice.gpg", + Some(alice_private_key_data), + None, + Ok("foo\n".as_bytes()), + ); + + test_decryption( + "password_encrypted_foo.gpg", + Some(alice_private_key_data), + None, + Err(()), + ); + + test_decryption( + "password_encrypted_foo.asc", + Some(alice_private_key_data), + None, + Err(()), + ); + } + + #[test] + fn decryption_of_not_encrypted_data() { + let alice_private_key = PrivateKeyData { + key_filename: "alice_private.asc", + passphrase: Some("whiterabbit"), + }; + + let mut processor_context = MockProcessContext::new(); + processor_context.controller_services.insert( + "my_private_key_service".to_string(), + Box::new(alice_private_key.into_controller()), + ); + processor_context.properties.insert( + PRIVATE_KEY_SERVICE.name(), + "my_private_key_service".to_string(), + ); + + let logger = MockLogger::new(); + + let decrypt_content = DecryptContentPGP::schedule(&processor_context, &logger) + .expect("Should schedule without any properties"); + let mut result: Vec = vec![]; + let mut flow_file_stream = std::io::Cursor::new("something not encrypted".as_bytes()); + let res = decrypt_content.transform( + &processor_context, + &mut flow_file_stream, + &mut result, + &logger, + ); + + test::assert_routed_to(res, &FAILURE); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content/processor_definition.rs b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content/processor_definition.rs new file mode 100644 index 0000000000..62b4126925 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content/processor_definition.rs @@ -0,0 +1,62 @@ +use super::{DecryptContentPGP, DecryptionStrategy}; +use crate::controller_services::private_key_service::PGPPrivateKeyService; +use crate::utils; +use minifi_native::{ + OutputAttribute, ProcessorDefinition, ProcessorInputRequirement, Property, PropertyDefinition, + Relationship, property_definitions, +}; + +pub(super) const LITERAL_DATA_FILENAME: OutputAttribute = OutputAttribute { + name: "pgp.literal.data.filename", + relationships: &["success"], + description: "Filename from decrypted Literal Data (Note that OpenPGP signatures do not include the formatting octet, the file name, and the date field of the Literal Data packet in a signature hash; therefore, those fields are not protected against tampering in a signed document. Therefore a lot of implementations omit these inherently malleable metadata)", +}; + +pub(super) const LITERAL_DATA_MODIFIED: OutputAttribute = OutputAttribute { + name: "pgp.literal.data.modified", + relationships: &["success"], + description: "Modified Date from decrypted Literal Data (Note that OpenPGP signatures do not include the formatting octet, the file name, and the date field of the Literal Data packet in a signature hash; therefore, those fields are not protected against tampering in a signed document. Therefore a lot of implementations omit these inherently malleable metadata)", +}; + +pub(super) const DECRYPTION_STRATEGY: Property = Property::new( + "Decryption Strategy", + "Strategy for writing files to success after decryption", +) +.with_default(DecryptionStrategy::Decrypted.into_str()); + +pub(super) const SYMMETRIC_PASSWORD: Property> = Property::new( + "Symmetric Password", + "Password used for decrypting data encrypted with Password-Based Encryption", +) +.sensitive(); + +pub(super) const PRIVATE_KEY_SERVICE: Property> = Property::new( + "Private Key Service", + "PGP Private Key Service for decrypting data encrypted with Public Key Encryption", +); + +pub(super) const SUCCESS: Relationship = Relationship { + name: "success", + description: "Decryption Succeeded", +}; + +pub(super) const FAILURE: Relationship = Relationship { + name: "failure", + description: "Decryption Failed", +}; + +impl ProcessorDefinition for DecryptContentPGP { + const DESCRIPTION: &'static str = "Decrypt contents of OpenPGP messages. Using the Packaged Decryption Strategy preserves OpenPGP encoding to support subsequent signature verification."; + const INPUT_REQUIREMENT: ProcessorInputRequirement = ProcessorInputRequirement::Required; + const SUPPORTS_DYNAMIC_PROPERTIES: bool = false; + const SUPPORTS_DYNAMIC_RELATIONSHIPS: bool = false; + const OUTPUT_ATTRIBUTES: &'static [OutputAttribute] = + &[LITERAL_DATA_FILENAME, LITERAL_DATA_MODIFIED]; + const RELATIONSHIPS: &'static [Relationship] = &[SUCCESS, FAILURE]; + + fn properties() -> &'static [PropertyDefinition] { + const PROPERTIES: &[PropertyDefinition] = + property_definitions![DECRYPTION_STRATEGY, SYMMETRIC_PASSWORD, PRIVATE_KEY_SERVICE,]; + PROPERTIES + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs new file mode 100644 index 0000000000..4c0c391565 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs @@ -0,0 +1,286 @@ +use minifi_native::{ + FlowFileStreamTransform, GetAttribute, GetControllerService, GetId, GetProperty, InputStream, + Logger, MinifiError, OutputStream, ProcessError, RouteErrorExt, Schedule, + TransformStreamResult, +}; +use pgp::composed::{ArmorOptions, MessageBuilder, SignedPublicKey}; +use pgp::types::{Password, StringToKey}; + +mod processor_definition; + +use processor_definition::*; + +use minifi_native::macros::{ComponentIdentifier, PropertyType}; +use strum_macros::{Display, EnumString, IntoStaticStr, VariantNames}; + +#[derive( + Debug, Clone, Copy, PartialEq, Display, EnumString, VariantNames, IntoStaticStr, PropertyType, +)] +#[strum(serialize_all = "UPPERCASE", const_into_str)] +enum FileEncoding { + Ascii, + Binary, +} + +#[derive(Debug, ComponentIdentifier)] +pub(crate) struct EncryptContentPGP { + file_encoding: FileEncoding, + symmetric_password: Option, +} + +#[cfg(not(test))] +fn string_to_key() -> StringToKey { + StringToKey::new_argon2(rand::thread_rng(), 3, 4, 16) // 64 MiB with rpgp's recommended parameter choice +} + +#[cfg(test)] +fn string_to_key() -> StringToKey { + StringToKey::new_argon2(rand::thread_rng(), 1, 1, 10) // fast for unit tests +} + +impl EncryptContentPGP { + fn encrypt_bytes( + &self, + input_stream: &mut dyn InputStream, + output_stream: &mut dyn OutputStream, + pub_key: Option<&SignedPublicKey>, + file_name: String, + ) -> Result<(), MinifiError> { + if pub_key.is_none() && self.symmetric_password.is_none() { + return Err(MinifiError::custom( + "No password or public key to encrypt with", + )); + } + + let mut builder = MessageBuilder::from_reader(file_name, input_stream).seipd_v1( + rand::thread_rng(), + pgp::crypto::sym::SymmetricKeyAlgorithm::AES256, + ); + + if let Some(pub_key) = pub_key { + builder + .encrypt_to_key(rand::thread_rng(), pub_key) + .map_err(MinifiError::other)?; + } + + if let Some(password) = &self.symmetric_password { + builder + .encrypt_with_password(string_to_key(), password) + .map_err(MinifiError::other)?; + } + + match self.file_encoding { + FileEncoding::Ascii => builder + .to_armored_writer(rand::thread_rng(), ArmorOptions::default(), output_stream) + .map_err(MinifiError::other), + FileEncoding::Binary => builder + .to_writer(rand::thread_rng(), output_stream) + .map_err(MinifiError::other), + } + } + + fn check_validity(password: &Option, has_pub_key: bool) -> Result<(), MinifiError> { + if password.is_none() && !has_pub_key { + Err(MinifiError::custom( + "Either a password or Public Key Service with Public Key Search should be configured to encrypt files", + )) + } else { + Ok(()) + } + } +} + +impl Schedule for EncryptContentPGP { + fn schedule(context: &P, _logger: &L) -> Result + where + Self: Sized, + { + let file_encoding = context.get_property::(&FILE_ENCODING)?; + let symmetric_password = context.get_property(&PASSWORD)?; + + let has_public_key = context.get_raw_property(&PUBLIC_KEY_SERVICE)?.is_some() + && context.get_property(&PUBLIC_KEY_SEARCH)?.is_some(); + + Self::check_validity(&symmetric_password, has_public_key)?; + Ok(EncryptContentPGP { + file_encoding, + symmetric_password, + }) + } +} + +impl EncryptContentPGP { + fn get_public_key( + context: &Ctx, + ) -> Result, MinifiError> { + if let (Some(pub_key_search), Some(public_key_service)) = ( + context.get_property(&PUBLIC_KEY_SEARCH)?, + context.get_controller_service(&PUBLIC_KEY_SERVICE)?, + ) { + Ok(public_key_service.get(&pub_key_search)) + } else { + Ok(None) + } + } +} + +impl FlowFileStreamTransform for EncryptContentPGP { + fn transform< + Ctx: GetProperty + GetControllerService + GetAttribute + GetId, + LoggerImpl: Logger, + >( + &self, + context: &Ctx, + input_stream: &mut dyn InputStream, + output_stream: &mut dyn OutputStream, + _logger: &LoggerImpl, + ) -> Result { + let file_name = context + .get_attribute("filename")? + .unwrap_or(context.get_id()?); + let public_key = Self::get_public_key(context).route_err_to_failure()?; + + self.encrypt_bytes(input_stream, output_stream, public_key, file_name) + .route_err_to_failure()?; + + Ok(TransformStreamResult::new(&SUCCESS) + .with_attribute(FILE_ENCODING_ATTR.name, self.file_encoding.into_str())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::controller_services::public_key_service::PGPPublicKeyService; + use crate::test_utils; + use minifi_native::{ + ComponentIdentifier, EnableControllerService, IoState, MockControllerServiceContext, + MockLogger, MockProcessContext, test, + }; + #[test] + fn test_ids() { + assert_eq!( + EncryptContentPGP::CLASS_NAME, + "minifi_pgp::processors::encrypt_content::EncryptContentPGP" + ); + assert_eq!(EncryptContentPGP::GROUP_NAME, "minifi_pgp"); + assert_eq!(EncryptContentPGP::VERSION, "1.0.0"); + } + + #[test] + fn cannot_schedule_without_password_or_public_key() { + assert!( + EncryptContentPGP::schedule(&MockProcessContext::new(), &MockLogger::new()).is_err() + ); + } + + fn assert_content(transform_result: &TransformStreamResult, is_ascii: bool) { + assert_eq!(transform_result.target_relationship_name(), SUCCESS.name); + assert_eq!(transform_result.write_status(), IoState::Ok); + assert_eq!( + transform_result.get_attribute("pgp.file.encoding").unwrap(), + if is_ascii { "ASCII" } else { "BINARY" } + ); + } + + #[test] + fn encrypts_via_passphrase() { + let mut context = MockProcessContext::new(); + context.properties.insert(PASSWORD.name(), "password"); + + let mut result: Vec = Vec::new(); + let mut input_stream = std::io::Cursor::new("foo".as_bytes()); + let processor = + EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + let transformed_ff = processor + .transform(&context, &mut input_stream, &mut result, &MockLogger::new()) + .expect("should transform"); + + assert!(!result.is_ascii()); + assert_content(&transformed_ff, false); + } + + fn public_key_service() -> PGPPublicKeyService { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Keyring File".to_string(), + test_utils::get_test_key_path("keyring.asc"), + ); + + PGPPublicKeyService::enable(&context, &MockLogger::new()).expect("should enable") + } + + #[test] + fn encrypts_ascii_for_alice() { + let mut context = MockProcessContext::new(); + context.properties.extend([ + ("Public Key Service", "my_controller_service"), + ("Public Key Search", "Alice"), + ("File Encoding", "ASCII"), + ]); + + context.controller_services.insert( + "my_controller_service".to_string(), + Box::new(public_key_service()), + ); + + let mut result: Vec = Vec::new(); + let mut input_stream = std::io::Cursor::new("foo".as_bytes()); + let processor = + EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + let transformed_ff = processor + .transform(&context, &mut input_stream, &mut result, &MockLogger::new()) + .expect("should transform"); + + assert!(result.is_ascii()); + assert_content(&transformed_ff, true); + } + + #[test] + fn encrypts_binary_for_bob() { + let mut context = MockProcessContext::new(); + context.properties.extend([ + ("Public Key Service", "my_controller_service"), + ("Public Key Search", "Bob"), + ("File Encoding", "BINARY"), + ]); + + context.controller_services.insert( + "my_controller_service".to_string(), + Box::new(public_key_service()), + ); + + let mut result: Vec = Vec::new(); + let mut input_stream = std::io::Cursor::new("foo".as_bytes()); + let processor = + EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + let transformed_ff = processor + .transform(&context, &mut input_stream, &mut result, &MockLogger::new()) + .expect("should transform"); + + assert!(!result.is_ascii()); + assert_content(&transformed_ff, false); + } + + #[test] + fn cannot_encrypt_for_carol() { + let mut context = MockProcessContext::new(); + context.properties.extend([ + ("Public Key Service", "my_controller_service"), + ("Public Key Search", "Carol"), + ]); + + context.controller_services.insert( + "my_controller_service".to_string(), + Box::new(public_key_service()), + ); + + let mut result: Vec = Vec::new(); + let mut input_stream = std::io::Cursor::new("foo".as_bytes()); + let processor = + EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + let res = processor.transform(&context, &mut input_stream, &mut result, &MockLogger::new()); + + test::assert_routed_to(res, &FAILURE); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content/processor_definition.rs b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content/processor_definition.rs new file mode 100644 index 0000000000..62b5d07e42 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content/processor_definition.rs @@ -0,0 +1,61 @@ +use super::{EncryptContentPGP, FileEncoding}; +use crate::controller_services::public_key_service::PGPPublicKeyService; +use crate::utils; +use minifi_native::{ + OutputAttribute, ProcessorDefinition, ProcessorInputRequirement, Property, PropertyDefinition, + Relationship, property_definitions, +}; + +pub(crate) const FILE_ENCODING: Property = + Property::new("File Encoding", "File Encoding for encryption") + .with_default(FileEncoding::Binary.into_str()); +pub(crate) const PASSWORD: Property> = Property::new( + "Symmetric Password", + "Password used for encrypting data with Password-Based Encryption", +) +.sensitive(); + +pub(crate) const PUBLIC_KEY_SEARCH: Property> = Property::new( + "Public Key Search", + "PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters", +).supports_expression_language(); + +pub(crate) const PUBLIC_KEY_SERVICE: Property> = Property::new( + "Public Key Service", + "PGP Public Key Service for encrypting data with Public Key Encryption", +); + +pub(super) const FILE_ENCODING_ATTR: OutputAttribute = OutputAttribute { + name: "pgp.file.encoding", + relationships: &["success"], + description: "File Encoding", +}; + +pub(super) const SUCCESS: Relationship = Relationship { + name: "success", + description: "Encryption Succeeded", +}; + +pub(super) const FAILURE: Relationship = Relationship { + name: "failure", + description: "Encryption Failed", +}; + +impl ProcessorDefinition for EncryptContentPGP { + const DESCRIPTION: &'static str = "Encrypt contents using OpenPGP."; + const INPUT_REQUIREMENT: ProcessorInputRequirement = ProcessorInputRequirement::Required; + const SUPPORTS_DYNAMIC_PROPERTIES: bool = false; + const SUPPORTS_DYNAMIC_RELATIONSHIPS: bool = false; + const OUTPUT_ATTRIBUTES: &'static [OutputAttribute] = &[FILE_ENCODING_ATTR]; + const RELATIONSHIPS: &'static [Relationship] = &[SUCCESS, FAILURE]; + + fn properties() -> &'static [PropertyDefinition] { + const PROPERTIES: &[PropertyDefinition] = property_definitions![ + FILE_ENCODING, + PASSWORD, + PUBLIC_KEY_SEARCH, + PUBLIC_KEY_SERVICE, + ]; + PROPERTIES + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs b/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs new file mode 100644 index 0000000000..ac45357fe3 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod decrypt_content; +pub(crate) mod encrypt_content; diff --git a/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs b/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs new file mode 100644 index 0000000000..963af71617 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs @@ -0,0 +1,15 @@ +use std::path::PathBuf; + +pub fn get_test_key_path(filename: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_keys"); + path.push(filename); + path.display().to_string() +} + +pub fn get_test_message(filename: &str) -> Vec { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_messages"); + path.push(filename); + std::fs::read(path).expect("test message should be readable") +} diff --git a/minifi_rust/extensions/minifi_pgp/src/utils.rs b/minifi_rust/extensions/minifi_pgp/src/utils.rs new file mode 100644 index 0000000000..f222127ef5 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/utils.rs @@ -0,0 +1,16 @@ +use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType}; + +pub(crate) struct Password {} + +impl PropertySchema for Password { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = false; +} + +impl PropertyType for Password { + type Output = pgp::types::Password; + + fn parse(s: &str) -> Result { + Ok(pgp::types::Password::from(s)) + } +} diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/README.txt b/minifi_rust/extensions/minifi_pgp/test_keys/README.txt new file mode 100644 index 0000000000..7b8ad1a612 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/README.txt @@ -0,0 +1,8 @@ +Testing keys v2 +------------------------ +uid [ultimate] Alice +passphrase whiterabbit + +uid [ultimate] Bob Personal +uid [ultimate] Bob Primary +no passphrase \ No newline at end of file diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc b/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc new file mode 100644 index 0000000000..8ae976d7b2 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc @@ -0,0 +1,50 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +q+zIM+a7/I/TcOmDRO5s +=EEA6 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/alice.gpg new file mode 100644 index 0000000000..71af439899 Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/alice.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc new file mode 100644 index 0000000000..955e525841 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc @@ -0,0 +1,92 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=6nxq +-----END PGP PRIVATE KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.gpg new file mode 100644 index 0000000000..5f385d9eea Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.asc new file mode 100644 index 0000000000..09fc1bc992 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.asc @@ -0,0 +1,71 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=ePbg +-----END PGP PRIVATE KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.gpg new file mode 100644 index 0000000000..a393702430 Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/garbage.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/garbage.gpg new file mode 100644 index 0000000000..9e603b5c3b Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/garbage.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc new file mode 100644 index 0000000000..0fa816a55d --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc @@ -0,0 +1,89 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=fkCr +-----END PGP PUBLIC KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.gpg new file mode 100644 index 0000000000..6a84506f9d Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc new file mode 100644 index 0000000000..6430facf86 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc @@ -0,0 +1,159 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=f4lJ +-----END PGP PRIVATE KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.gpg new file mode 100644 index 0000000000..4a07229aa2 Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/truncated.asc b/minifi_rust/extensions/minifi_pgp/test_keys/truncated.asc new file mode 100644 index 0000000000..d25ddfb620 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/truncated.asc @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGmJ4OIBCACz9RXNN6lFaUi0b4V6PTyjc27g9G0OCBMy6H/lcjROMGupqPm1 +9QzEzTIrxkc1LlPx31qzb6SwzQWkKiDnmObcZzG43Yiz1aD0YOqJsHBb9klrdWFx +VbGTtaDmZg/xAS+VseYTijiucydURPzIKDb25vWl7r+iAdhZY3eo8Zif7g7LDpU6 +hsqAQOVgIGCokbbS4GFTeOIl6uwS1Gchq40vY5AM7o4/AObANNstyROgQrQqq19Y +QEjnLT6GsxF6jpbrcb+8No6JWJSaqhDjIVug+psaeuqruQkN6o3B85izGk0fu4QD +kSYGW3/A9ArGrLhGMtFnTyo/fg9sEGqWxLoJABEBAAG0GUFsaWNlIDxhbGljZUBl +eGFtcGxlLmNvbT6JAVIEEwEIADwWIQQR1fT4Ba73eK2U4NIbsOxL81Ml9gUCaYng +4gIbLwU \ No newline at end of file diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/truncated_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/truncated_private.asc new file mode 100644 index 0000000000..9cc58539ae --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/truncated_private.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQPGBGmJ4OIBCACz9RXNN6lFaUi0b4V6PTyjc27g9G0OCBMy6H/lcjROMGupqPm1 +9QzEzTIrxkc1LlPx31qzb6SwzQWkKiDnmObcZzG43Yiz1aD0YOqJsHBb9klrdWFx +VbGTtaDmZg/xAS+VseYTijiucydURPzIKDb25vWl7r+iAdhZY3eo8Zif7g7LDpU6 +hsqAQOVgIGCokbbS4GFTeOIl6uwS1Gchq40vY5AM7o4/AObANNstyROgQrQqq19Y +QEjnLT6GsxF6jpbrcb+8No6JWJSaqhDjIVug+psaeuqruQkN6o3B85izGk0fu4QD +kSYGW3/A9ArGrLhGMtFnTyo/fg9sEGqWxLoJABEBAAH+BwMCYkH6w6nO395gtdQ6 +zQJvZ1itO9NCbRtI20iVkqWmwtr2FwgN8AZ9sGZss6zdpfxh85Ef1kHvP1nkbedk +/8OmBljPooqKB7MTwCCxOC53Mf6wNMijlBYsY8YUyi4dwaHoxnDFnaCeITSHHehY +07ifnInvrTkbJ41JzfP124xQ804voehm7merA91Vtpvg/hoYqJ/Sxo22UpTwvuw/ +aKqoetlJWqRk8VmBpcuuVFYcF9jaOPB51WG8fRDj66eINg2zXL49WRvwlUtbAHvS +cbglkBzMFHqljx0KJWX/QMO64X894eFafVFvSiYf+fn80wv9h7IKjj413itlbF0r +X+DckGQ9b50XAD3kZgDOMr5dKTGQ9ytChl7hpy38ucQqJM0qRlTSG2mp05qBO+XZ +CNNE7qvVNJNP5nD/3xkWD8oi+nhmqqg4bZ/QDUcoTVrWW7L1er2fgREJg7xL7xuD +QYu/T0A6N9PxWefYdEN/jjcLqks/Pjdy3DfGtlDysj88GpLh3diNgQ2EnNgQ32pq +JmxwEWIQ4VV30Ms1D0Uh7g4Ksq0lq1/LjOll3FSyjr3Ihpesopj \ No newline at end of file diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.asc b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.asc new file mode 100644 index 0000000000..94b8561c9a --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.asc @@ -0,0 +1,12 @@ +-----BEGIN PGP MESSAGE----- + +hQEMA7zOP9+6AZ1+AQf/WlAEDriFTKHJfn5KXAi123WGeDJBoRi/etl7GJ8MO5+8 +crdou58wMcqRJ8u3wNgKWDYm+QknLhQK5+3dJajwQeKH18uruTkEmFQB/wArHsOX +62UhFf2qbAzvUuTH5kPyt1d/Wt51T9+K/xlEPJr+DiK0uHlXZPu7rEnqk9pcKikC +/dYAuljnkNigDoykHwEBRcBfQu5t/hIe/Bii3wTZPm2w0YneyjOtd7Yq3mDlfmDW +cy3bdDjuwP4npCxcnHi7WkbElTyCJMybKVwwLjugihGI+4r8itO2wknAT5GDGMQ8 +u2FfOfGnIYTK2mBAQgyM7gtBX2qS28uWYlj5gehngdRJAQkCEFMqwWDAaUXAi6QU +xhn/O+wEEpUVYnGuEpIqG9KTW3qYl+vTxLkeNzg2NL255QP9gLbdlKFJYkC60c4I +JJXhkwkPTSC+Xw== +=UAju +-----END PGP MESSAGE----- diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.gpg b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.gpg new file mode 100644 index 0000000000..bd3d9b476e Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.gpg differ diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.asc b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.asc new file mode 100644 index 0000000000..22d639e711 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.asc @@ -0,0 +1,6 @@ +-----BEGIN PGP MESSAGE----- + +jA0ECQMCU2B2LnRTkyNg0jkBhgVPotvo6S9iLOTWhzglgsjR/6QB2v7vUNImzkh7 +fjhd17fG5tjB1RPRgW3bR12BidV6TQKuwLs= +=AXoJ +-----END PGP MESSAGE----- diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.gpg b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.gpg new file mode 100644 index 0000000000..40ab6a354e --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.gpg @@ -0,0 +1 @@ +  t?`9B2;Œg]N)!r2!Fnl./a9._}_2,ɴ \ No newline at end of file diff --git a/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md b/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md index e05adf0e0e..74ecd008fd 100644 --- a/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md +++ b/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md @@ -218,6 +218,7 @@ In the list below, the names of required properties appear in bold. Any other pr | Name | Default Value | Allowable Values | Description | |------------------------------------|---------------|-------------------|--------------------------------------------| +| Dummy Controller Service | | | Optional dummy controller service | | **Lorem Ipsum Controller Service** | | | Name of the lorem ipsum controller service | | **Write Method** | Buffer | Buffer
Stream | Which API to test | diff --git a/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs b/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs index 6ee76de72c..619d40d3d9 100644 --- a/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs +++ b/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs @@ -17,7 +17,7 @@ use super::*; use crate::processors::asciify_german::relationships::SUCCESS; -use minifi_native::{IoState, MockLogger, MockProcessContext}; +use minifi_native::{IoState, MockLogger, MockProcessContext, test}; use std::io::BufReader; #[test] @@ -84,8 +84,5 @@ fn truncated_umlaut_at_eof_routes_to_failure() { let mut output_vec: Vec = Vec::new(); let result = asciify_german.transform(&context, &mut input_stream, &mut output_vec, &logger); - match result { - Err(ProcessError::Route(route)) => assert_eq!(route.relationship.as_ref(), FAILURE.name), - other => panic!("expected a route error to failure, got {other:?}"), - } + test::assert_routed_to(result, &FAILURE); } diff --git a/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs b/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs index 52a6fe0dba..7f0f98974a 100644 --- a/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs +++ b/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs @@ -24,6 +24,7 @@ use crate::{ MultiThreaded, OnTriggerResult, OutputStream, ProcessContext, ProcessError, ProcessSession, Processor, Relationship, Schedule, SingleThreaded, }; +use minifi_native::GetId; use std::borrow::Cow; #[derive(Debug)] @@ -73,7 +74,10 @@ impl TransformStreamResult { impl_with_attributes!(TransformStreamResult); pub trait FlowFileStreamTransform { - fn transform( + fn transform< + Ctx: GetProperty + GetControllerService + GetAttribute + GetId, + LoggerImpl: Logger, + >( &self, context: &Ctx, input_stream: &mut dyn InputStream, diff --git a/minifi_rust/minifi_native/src/lib.rs b/minifi_rust/minifi_native/src/lib.rs index b855291a29..4ae26f9c14 100644 --- a/minifi_rust/minifi_native/src/lib.rs +++ b/minifi_rust/minifi_native/src/lib.rs @@ -19,6 +19,7 @@ extern crate self as minifi_native; mod api; pub mod c_ffi; pub mod mock; +pub mod test_utils; pub use api::errors::{MinifiError, ProcessError, RouteError, RouteErrorExt}; @@ -69,6 +70,7 @@ pub use mock::{ MockControllerServiceContext, MockFlowFile, MockLogger, MockProcessContext, MockProcessSession, StdLogger, }; +pub use test_utils as test; #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] diff --git a/minifi_rust/minifi_native/src/test_utils.rs b/minifi_rust/minifi_native/src/test_utils.rs new file mode 100644 index 0000000000..089860498a --- /dev/null +++ b/minifi_rust/minifi_native/src/test_utils.rs @@ -0,0 +1,16 @@ +use crate::{ProcessError, Relationship, TransformStreamResult}; + +pub fn assert_routed_to( + res: Result, + expected_relationship: &Relationship, +) { + match res { + Err(ProcessError::Route(route)) => { + assert_eq!(route.relationship, expected_relationship.name) + } + Err(other) => { + panic!("expected route to '{expected_relationship}', got fatal error: {other:?}") + } + Ok(_) => panic!("expected route to '{expected_relationship}', got Ok"), + } +} diff --git a/minifi_rust/minifi_rs_behave/Dockerfile.alpine b/minifi_rust/minifi_rs_behave/Dockerfile.alpine index 032c26ec57..e3c1557797 100644 --- a/minifi_rust/minifi_rs_behave/Dockerfile.alpine +++ b/minifi_rust/minifi_rs_behave/Dockerfile.alpine @@ -22,4 +22,4 @@ RUN cargo build --release # Export Stage FROM scratch AS bin-export -COPY --from=builder /app/target/release/libminifi_rs_playground.so / +COPY --from=builder /app/target/release/libminifi_*.so / diff --git a/minifi_rust/minifi_rs_behave/Dockerfile.debian b/minifi_rust/minifi_rs_behave/Dockerfile.debian index 274ed3cb5e..a3c4aa03d6 100644 --- a/minifi_rust/minifi_rs_behave/Dockerfile.debian +++ b/minifi_rust/minifi_rs_behave/Dockerfile.debian @@ -22,4 +22,4 @@ RUN cargo build --release # Export Stage FROM scratch AS bin-export -COPY --from=builder /app/target/release/libminifi_rs_playground.so / +COPY --from=builder /app/target/release/libminifi_*.so /