diff --git a/.github/references/ubuntu_22_04_clang_arm_manifest.json b/.github/references/ubuntu_22_04_clang_arm_manifest.json index 139b3854d7..efc67a971b 100644 --- a/.github/references/ubuntu_22_04_clang_arm_manifest.json +++ b/.github/references/ubuntu_22_04_clang_arm_manifest.json @@ -12104,6 +12104,184 @@ "version": "1.0.0" } }, +{ + "bundles": { + "componentManifest": { + "processors": [ + { + "propertyDescriptors": { + "Passphrase": { + "name": "Passphrase", + "description": "Passphrase used for decrypting data encrypted with Password-Based Encryption", + "validator": "NON_BLANK_VALIDATOR", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + }, + "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" + } + }, + "inputRequirement": "INPUT_REQUIRED", + "isSingleThreaded": "false", + "supportedRelationships": [ + { + "name": "failure", + "description": "Decryption Failed" + }, + { + "name": "success", + "description": "Decryption Succeeded" + } + ], + "typeDescription": "Decrypt contents of OpenPGP messages.", + "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" + } + ] + }, + "Passphrase": { + "name": "Passphrase", + "description": "Passphrase used for encrypting data with Password-Based Encryption", + "validator": "NON_BLANK_VALIDATOR", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + }, + "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" + } + }, + "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 Password": { + "name": "Key Password", + "description": "Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn", + "validator": "NON_BLANK_VALIDATOR", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + }, + "Keyring": { + "name": "Keyring", + "description": "Secret Key encoded in ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "true", + "expressionLanguageScope": "NONE" + }, + "Keyring File": { + "name": "Keyring File", + "description": "File path to PGP Secret Key encoded in binary or ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "FLOWFILE_ATTRIBUTES" + } + }, + "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 Public Key encoded in ASCII Armor", + "validator": "VALID", + "required": "false", + "sensitive": "false", + "expressionLanguageScope": "NONE" + }, + "Keyring File": { + "name": "Keyring File", + "description": "File path to PGP Keyring or Public 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/CONTROLLERS.md b/CONTROLLERS.md index 28fce43e1e..a888c22a5a 100644 --- a/CONTROLLERS.md +++ b/CONTROLLERS.md @@ -27,6 +27,8 @@ limitations under the License. - [NetworkPrioritizerService](#NetworkPrioritizerService) - [ODBCService](#ODBCService) - [PersistentMapStateStorage](#PersistentMapStateStorage) +- [PGPPrivateKeyService](#PGPPrivateKeyService) +- [PGPPublicKeyService](#PGPPublicKeyService) - [ProxyConfigurationService](#ProxyConfigurationService) - [RocksDbStateStorage](#RocksDbStateStorage) - [SmbConnectionControllerService](#SmbConnectionControllerService) @@ -245,6 +247,39 @@ In the list below, the names of required properties appear in bold. Any other pr | **File** | | | Path to a file to store state | +## 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 Password | | | Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn
**Sensitive Property: true** | +| Keyring | | | Secret Key encoded in ASCII Armor
**Sensitive Property: true** | +| Keyring File | | | File path to PGP Secret Key encoded in binary or ASCII Armor
**Supports Expression Language: 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 | | | PGP Keyring or Public Key encoded in ASCII Armor | +| Keyring File | | | File path to PGP Keyring or Public Key encoded in binary or ASCII Armor
**Supports Expression Language: true** | + + ## ProxyConfigurationService ### Description @@ -393,4 +428,3 @@ In the list below, the names of required properties appear in bold. Any other pr | **Pretty Print XML** | false | true
false | Specifies whether or not the XML should be pretty printed | | **Name of Record Tag** | | | Specifies the name of the XML record tag wrapping the record fields. | | **Name of Root Tag** | | | Specifies the name of the XML root tag wrapping the record set. | - diff --git a/PROCESSORS.md b/PROCESSORS.md index 4c7705b2a6..a2753b42fe 100644 --- a/PROCESSORS.md +++ b/PROCESSORS.md @@ -26,11 +26,13 @@ limitations under the License. - [ConsumeMQTT](#ConsumeMQTT) - [ConsumeWindowsEventLog](#ConsumeWindowsEventLog) - [ConvertRecord](#ConvertRecord) +- [DecryptContentPGP](#DecryptContentPGP) - [DefragmentText](#DefragmentText) - [DeleteAzureBlobStorage](#DeleteAzureBlobStorage) - [DeleteAzureDataLakeStorage](#DeleteAzureDataLakeStorage) - [DeleteGCSObject](#DeleteGCSObject) - [DeleteS3Object](#DeleteS3Object) +- [EncryptContentPGP](#EncryptContentPGP) - [EvaluateJsonPath](#EvaluateJsonPath) - [ExecuteProcess](#ExecuteProcess) - [ExecuteScript](#ExecuteScript) @@ -436,6 +438,29 @@ In the list below, the names of required properties appear in bold. Any other pr | record.error.message | failure | This attribute provides on failure the error message encountered by the Reader or Writer. | +## DecryptContentPGP + +### Description + +Decrypt contents of OpenPGP messages. + +### 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 | +|---------------------|---------------|------------------|---------------------------------------------------------------------------------------------------------------| +| Passphrase | | | Passphrase 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 | +|---------|----------------------| +| failure | Decryption Failed | +| success | Decryption Succeeded | + + ## DefragmentText ### Description @@ -594,6 +619,37 @@ In the list below, the names of required properties appear in bold. Any other pr | failure | FlowFiles are routed to failure relationship | +## 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 | +| Passphrase | | | Passphrase 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 | +|---------|----------------------| +| failure | Encryption Failed | +| success | Encryption Succeeded | + +### Output Attributes + +| Attribute | Relationship | Description | +|-------------------|--------------|---------------| +| pgp.file.encoding | success | File Encoding | + + ## EvaluateJsonPath ### Description diff --git a/README.md b/README.md index a26a995c5e..b7a1b959ed 100644 --- a/README.md +++ b/README.md @@ -75,32 +75,33 @@ The following table lists the base set of processors. The next table outlines CMAKE flags that correspond with MiNiFi extensions. Extensions that are enabled by default ( such as RocksDB ), can be disabled with the respective CMAKE flag on the command line. -| Extension Set | Processors and Controller Services | CMAKE Flag | -|----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------| -| Archive Extensions | [ApplyTemplate](PROCESSORS.md#applytemplate)
[CompressContent](PROCESSORS.md#compresscontent)
[ManipulateArchive](PROCESSORS.md#manipulatearchive)
[MergeContent](PROCESSORS.md#mergecontent)
[FocusArchiveEntry](PROCESSORS.md#focusarchiveentry)
[UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry) | -DBUILD_LIBARCHIVE=ON | -| AWS | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice)
[PutS3Object](PROCESSORS.md#puts3object)
[DeleteS3Object](PROCESSORS.md#deletes3object)
[FetchS3Object](PROCESSORS.md#fetchs3object)
[ListS3](PROCESSORS.md#lists3)
[PutKinesisStream](PROCESSORS.md#putkinesisstream) | -DENABLE_AWS=ON | -| Azure | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice)
[PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage)
[DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage)
[FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage)
[ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage)
[PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage)
[DeleteAzureDataLakeStorage](PROCESSORS.md#deleteazuredatalakestorage)
[FetchAzureDataLakeStorage](PROCESSORS.md#fetchazuredatalakestorage)
[ListAzureDataLakeStorage](PROCESSORS.md#listazuredatalakestorage) | -DENABLE_AZURE=ON | -| CivetWeb | [ListenHTTP](PROCESSORS.md#listenhttp) | -DENABLE_CIVET=ON | -| Couchbase | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice)
[PutCouchbaseKey](PROCESSORS.md#putcouchbasekey)
[GetCouchbaseKey](PROCESSORS.md#getcouchbasekey) | -DENABLE_COUCHBASE=ON | -| Elasticsearch | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice)
[PostElasticsearch](PROCESSORS.md#postelasticsearch) | -DENABLE_ELASTICSEARCH=ON | -| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess) | -DENABLE_EXECUTE_PROCESS=ON | -| Google Cloud Platform | [DeleteGCSObject](PROCESSORS.md#deletegcsobject)
[FetchGCSObject](PROCESSORS.md#fetchgcsobject)
[GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice)
[ListGCSBucket](PROCESSORS.md#listgcsbucket)
[PutGCSObject](PROCESSORS.md#putgcsobject) | -DENABLE_GCP=ON | -| Grafana Loki | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest)
[PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc) | -DENABLE_GRAFANA_LOKI=ON | -| Kafka | [PublishKafka](PROCESSORS.md#publishkafka)
[ConsumeKafka](PROCESSORS.md#consumekafka) | -DENABLE_KAFKA=ON | -| Kubernetes (Linux) | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice) | -DENABLE_KUBERNETES=ON | -| LlamaCpp | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference) | -DENABLE_LLAMACPP=ON | -| Lua Scripting | [ExecuteScript](PROCESSORS.md#executescript) | -DENABLE_LUA_SCRIPTING=ON | -| MQTT | [ConsumeMQTT](PROCESSORS.md#consumemqtt)
[PublishMQTT](PROCESSORS.md#publishmqtt) | -DENABLE_MQTT=ON | -| OPC | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor)
[PutOPCProcessor](PROCESSORS.md#putopcprocessor) | -DENABLE_OPC=ON | -| PDH (Windows) | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor) | -DENABLE_PDH=ON | -| ProcFs (Linux) | [ProcFsMonitor](PROCESSORS.md#procfsmonitor) | -DENABLE_PROCFS=ON | -| Python Scripting | [ExecuteScript](PROCESSORS.md#executescript)
[**Custom Python Processors**](extensions/python/PYTHON.md) | -DENABLE_PYTHON_SCRIPTING=ON | -| SMB (Windows) | [FetchSmb](PROCESSORS.md#fetchsmb)
[ListSmb](PROCESSORS.md#listsmb)
[PutSmb](PROCESSORS.md#putsmb) | -DENABLE_SMB=ON | -| SFTP | [FetchSFTP](PROCESSORS.md#fetchsftp)
[ListSFTP](PROCESSORS.md#listsftp)
[PutSFTP](PROCESSORS.md#putsftp) | -DENABLE_SFTP=ON | -| SQL | [ExecuteSQL](PROCESSORS.md#executesql)
[PutSQL](PROCESSORS.md#putsql)
[QueryDatabaseTable](PROCESSORS.md#querydatabasetable)
| -DENABLE_SQL=ON | -| Splunk | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp)
[QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus) | -DENABLE_SPLUNK=ON | -| Systemd (Linux) | [ConsumeJournald](PROCESSORS.md#consumejournald) | -DENABLE_SYSTEMD=ON | -| Windows Event Log (Windows) | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog)
[TailEventLog](PROCESSORS.md#taileventlog) | -DENABLE_WEL=ON | +| Extension Set | Processors and Controller Services | CMAKE Flag | +|----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------| +| Archive Extensions | [ApplyTemplate](PROCESSORS.md#applytemplate)
[CompressContent](PROCESSORS.md#compresscontent)
[ManipulateArchive](PROCESSORS.md#manipulatearchive)
[MergeContent](PROCESSORS.md#mergecontent)
[FocusArchiveEntry](PROCESSORS.md#focusarchiveentry)
[UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry) | -DBUILD_LIBARCHIVE=ON | +| AWS | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice)
[PutS3Object](PROCESSORS.md#puts3object)
[DeleteS3Object](PROCESSORS.md#deletes3object)
[FetchS3Object](PROCESSORS.md#fetchs3object)
[ListS3](PROCESSORS.md#lists3)
[PutKinesisStream](PROCESSORS.md#putkinesisstream) | -DENABLE_AWS=ON | +| Azure | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice)
[PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage)
[DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage)
[FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage)
[ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage)
[PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage)
[DeleteAzureDataLakeStorage](PROCESSORS.md#deleteazuredatalakestorage)
[FetchAzureDataLakeStorage](PROCESSORS.md#fetchazuredatalakestorage)
[ListAzureDataLakeStorage](PROCESSORS.md#listazuredatalakestorage) | -DENABLE_AZURE=ON | +| CivetWeb | [ListenHTTP](PROCESSORS.md#listenhttp) | -DENABLE_CIVET=ON | +| Couchbase | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice)
[PutCouchbaseKey](PROCESSORS.md#putcouchbasekey)
[GetCouchbaseKey](PROCESSORS.md#getcouchbasekey) | -DENABLE_COUCHBASE=ON | +| Elasticsearch | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice)
[PostElasticsearch](PROCESSORS.md#postelasticsearch) | -DENABLE_ELASTICSEARCH=ON | +| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess) | -DENABLE_EXECUTE_PROCESS=ON | +| Google Cloud Platform | [DeleteGCSObject](PROCESSORS.md#deletegcsobject)
[FetchGCSObject](PROCESSORS.md#fetchgcsobject)
[GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice)
[ListGCSBucket](PROCESSORS.md#listgcsbucket)
[PutGCSObject](PROCESSORS.md#putgcsobject) | -DENABLE_GCP=ON | +| Grafana Loki | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest)
[PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc) | -DENABLE_GRAFANA_LOKI=ON | +| Kafka | [PublishKafka](PROCESSORS.md#publishkafka)
[ConsumeKafka](PROCESSORS.md#consumekafka) | -DENABLE_KAFKA=ON | +| Kubernetes (Linux) | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice) | -DENABLE_KUBERNETES=ON | +| LlamaCpp | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference) | -DENABLE_LLAMACPP=ON | +| Lua Scripting | [ExecuteScript](PROCESSORS.md#executescript) | -DENABLE_LUA_SCRIPTING=ON | +| MQTT | [ConsumeMQTT](PROCESSORS.md#consumemqtt)
[PublishMQTT](PROCESSORS.md#publishmqtt) | -DENABLE_MQTT=ON | +| OPC | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor)
[PutOPCProcessor](PROCESSORS.md#putopcprocessor) | -DENABLE_OPC=ON | +| PDH (Windows) | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor) | -DENABLE_PDH=ON | +| PGP | [EncryptContentPGP](PROCESSORS.md#encryptcontentpgp)
[DecryptContentPGP](PROCESSORS.md#decryptcontentpgp)
[PGPPublicKeyService](CONTROLLERS.md#pgppublickeyservice)
[PGPPrivateKeyService](CONTROLLERS.md#pgpprivatekeyservice) | -DMINIFI_EXTENSION_PGP=ON -DMINIFI_RUST=ON | +| ProcFs (Linux) | [ProcFsMonitor](PROCESSORS.md#procfsmonitor) | -DENABLE_PROCFS=ON | +| Python Scripting | [ExecuteScript](PROCESSORS.md#executescript)
[**Custom Python Processors**](extensions/python/PYTHON.md) | -DENABLE_PYTHON_SCRIPTING=ON | +| SMB (Windows) | [FetchSmb](PROCESSORS.md#fetchsmb)
[ListSmb](PROCESSORS.md#listsmb)
[PutSmb](PROCESSORS.md#putsmb) | -DENABLE_SMB=ON | +| SFTP | [FetchSFTP](PROCESSORS.md#fetchsftp)
[ListSFTP](PROCESSORS.md#listsftp)
[PutSFTP](PROCESSORS.md#putsftp) | -DENABLE_SFTP=ON | +| SQL | [ExecuteSQL](PROCESSORS.md#executesql)
[PutSQL](PROCESSORS.md#putsql)
[QueryDatabaseTable](PROCESSORS.md#querydatabasetable)
| -DENABLE_SQL=ON | +| Splunk | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp)
[QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus) | -DENABLE_SPLUNK=ON | +| Systemd (Linux) | [ConsumeJournald](PROCESSORS.md#consumejournald) | -DENABLE_SYSTEMD=ON | +| Windows Event Log (Windows) | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog)
[TailEventLog](PROCESSORS.md#taileventlog) | -DENABLE_WEL=ON | Please see our [Python guide](extensions/python/PYTHON.md) on how to write Python processors and use them within MiNiFi C++. diff --git a/cmake/MiNiFiOptions.cmake b/cmake/MiNiFiOptions.cmake index 7a898a7658..dcd3a2b265 100644 --- a/cmake/MiNiFiOptions.cmake +++ b/cmake/MiNiFiOptions.cmake @@ -120,6 +120,7 @@ add_minifi_option(ENABLE_CONTROLLER "Enables the build of MiNiFi controller bina add_minifi_option(ENABLE_LLAMACPP "Enables llama.cpp support." ON) add_minifi_option(ENABLE_OPC "Instructs the build system to enable the OPC extension" ON) add_minifi_option(MINIFI_RUST "Enables the build of rust based extensions." OFF) +add_minifi_dependent_option(MINIFI_EXTENSION_PGP "Enables the PGP rust extension." ON "MINIFI_RUST" OFF) add_minifi_option(MINIFI_LMDB "Enables the LMDB extension." OFF) set_minifi_cache_variable(CUSTOM_MALLOC OFF "Overwrite malloc implementation.") diff --git a/minifi_rust/CMakeLists.txt b/minifi_rust/CMakeLists.txt index 63b8a407e1..3166ec5178 100644 --- a/minifi_rust/CMakeLists.txt +++ b/minifi_rust/CMakeLists.txt @@ -39,6 +39,10 @@ if (NOT ENABLE_TEST_PROCESSORS) set_target_properties(cargo-build_minifi_rs_playground PROPERTIES EXCLUDE_FROM_ALL TRUE) endif() +if (NOT MINIFI_EXTENSION_PGP) + set_target_properties(cargo-build_minifi_pgp PROPERTIES EXCLUDE_FROM_ALL TRUE) +endif() + include(CTest) add_test( diff --git a/minifi_rust/Cargo.toml b/minifi_rust/Cargo.toml index 6dc306f11f..8bde74c44c 100644 --- a/minifi_rust/Cargo.toml +++ b/minifi_rust/Cargo.toml @@ -7,3 +7,9 @@ panic = "abort" [profile.dev] panic = "abort" + +# The PGP tests unlock passphrase-protected secret keys, which costs a full S2K hash +# chain per key. Unoptimized that dominates the test suite (6.4s -> 0.4s with this). +# Workspace members are not matched by "*", so our own code stays at opt-level 0. +[profile.dev.package."*"] +opt-level = 2 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..202a013075 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature @@ -0,0 +1,67 @@ +# 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. + +@SUPPORTS_WINDOWS +Feature: Test PGP extension's encryption and decryption capabilities + + 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..4652c748a4 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/features/environment.py @@ -0,0 +1,41 @@ +# 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 +# +# https://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. + +import os +from pathlib import Path + +from minifi_behave.core.hooks import ( + add_extension_to_minifi_container, + common_after_scenario, + common_before_scenario, +) + + +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) + context.resource_dir = Path(f"{os.path.dirname(os.path.realpath(__file__))}/..") + + +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..fd56995537 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/features/steps/steps.py @@ -0,0 +1,80 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://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. + + +import humanfriendly +from behave import step, then +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 +from minifi_behave.steps import ( + checking_steps, # noqa: F401 + configuration_steps, # noqa: F401 + core_steps, # noqa: F401 + flow_building_steps, # noqa: F401 +) + + +@step("an EncryptContentPGP processor with a PGPPublicKeyService is set up") +def step_encrypt_content_with_service(context: MinifiTestContext): + public_key_service = ControllerService(class_name="PGPPublicKeyService", service_name="my_public_keys") + alice_public_key = (context.resource_dir / "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): + private_key_service = ControllerService(class_name="PGPPrivateKeyService", service_name="alice_private_key") + alice_private_key = (context.resource_dir / "test_keys" / "alice_private.asc").read_text() + private_key_service.add_property("Keyring", alice_private_key) + private_key_service.add_property("Key Password", "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): + private_key_service = ControllerService(class_name="PGPPrivateKeyService", service_name="bob_private_key") + bob_private_key = (context.resource_dir / "test_keys" / "bob_private.asc").read_text() + private_key_service.add_property("Keyring", 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..e72bc0b3f3 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md @@ -0,0 +1,112 @@ + + +## Table of Contents + +### Processors + +- [DecryptContentPGP](#DecryptContentPGP) +- [EncryptContentPGP](#EncryptContentPGP) +### Controller Services + +- [PGPPrivateKeyService](#PGPPrivateKeyService) +- [PGPPublicKeyService](#PGPPublicKeyService) + + +## DecryptContentPGP + +### Description + +Decrypt contents of OpenPGP messages. + +### 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 | +|---------------------|---------------|------------------|---------------------------------------------------------------------------------------------------------------| +| Passphrase | | | Passphrase 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 | +|---------|----------------------| +| failure | Decryption Failed | +| success | Decryption Succeeded | + + +## 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 | +| Passphrase | | | Passphrase 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 | +|---------|----------------------| +| failure | Encryption Failed | +| success | Encryption Succeeded | + +### 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 Password | | | Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn
**Sensitive Property: true** | +| Keyring | | | Secret Key encoded in ASCII Armor
**Sensitive Property: true** | +| Keyring File | | | File path to PGP Secret Key encoded in binary or ASCII Armor
**Supports Expression Language: 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 | | | PGP Keyring or Public Key encoded in ASCII Armor | +| Keyring File | | | File path to PGP Keyring or Public Key encoded in binary or ASCII Armor
**Supports Expression Language: true** | diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/encryption_key.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/encryption_key.rs new file mode 100644 index 0000000000..173f7d4365 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/encryption_key.rs @@ -0,0 +1,131 @@ +// 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 +// +// https://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. + +use minifi_native::MinifiError; +use pgp::composed::{SignedPublicKey, SignedPublicSubKey}; +use pgp::packet::{PublicKey, SignatureType}; +use pgp::types::KeyDetails; + +/// The component key of a certificate that a message should actually be encrypted to. +/// +/// `SignedPublicKey`'s own `EncryptionKey` implementation always uses the primary key and +/// ignores subkeys, which fails for the layout `gpg --gen-key` produces nowadays: a sign-only +/// primary key (Ed25519) plus a dedicated encryption subkey (Cv25519). +#[derive(Debug)] +pub(crate) enum EncryptionTarget<'a> { + Primary(&'a PublicKey), + Subkey(&'a SignedPublicSubKey), +} + +/// Picks the key of `certificate` to encrypt to. +/// +/// Encryption subkeys are preferred, newest first, the way GnuPG picks them; the primary key is +/// only used when the certificate has no usable encryption subkey. +pub(crate) fn select_encryption_target( + certificate: &SignedPublicKey, +) -> Result, MinifiError> { + let newest_encryption_subkey = certificate + .public_subkeys + .iter() + .filter(|subkey| is_encryption_subkey(subkey)) + .max_by_key(|subkey| subkey.created_at()); + + if let Some(subkey) = newest_encryption_subkey { + return Ok(EncryptionTarget::Subkey(subkey)); + } + + if certificate.primary_key.algorithm().can_encrypt() { + return Ok(EncryptionTarget::Primary(&certificate.primary_key)); + } + + Err(MinifiError::custom(format!( + "Key {} cannot be used for encryption, it has no encryption subkey and its primary key is {:?} which cannot encrypt", + certificate.primary_key.fingerprint(), + certificate.primary_key.algorithm() + ))) +} + +fn is_encryption_subkey(subkey: &SignedPublicSubKey) -> bool { + if !subkey.key.algorithm().can_encrypt() { + return false; + } + + let is_revoked = subkey + .signatures + .iter() + .any(|signature| signature.typ() == Some(SignatureType::SubkeyRevocation)); + if is_revoked { + return false; + } + + subkey + .signatures + .iter() + .filter(|signature| signature.typ() == Some(SignatureType::SubkeyBinding)) + .any(|signature| { + let key_flags = signature.key_flags(); + key_flags.encrypt_comms() || key_flags.encrypt_storage() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::get_test_key_path; + use pgp::composed::Deserializable; + + fn load_certificate(file_name: &str) -> SignedPublicKey { + let (certificate, _headers) = + SignedPublicKey::from_armor_file(get_test_key_path(file_name)).unwrap(); + certificate + } + + #[test] + fn rsa_primary_key_is_used_when_there_is_no_encryption_subkey() { + // alice.asc is an RSA key whose primary key carries the encrypt capability itself. + let certificate = load_certificate("alice.asc"); + assert!(certificate.primary_key.algorithm().can_encrypt()); + assert!(matches!( + select_encryption_target(&certificate).unwrap(), + EncryptionTarget::Primary(_) + )); + } + + #[test] + fn encryption_subkey_is_preferred_over_a_sign_only_primary_key() { + // dave.asc has an Ed25519 sign-only primary key and a Cv25519 encryption subkey, + // the layout `gpg --gen-key` produces by default. + let certificate = load_certificate("dave.asc"); + assert!(!certificate.primary_key.algorithm().can_encrypt()); + + let target = select_encryption_target(&certificate).unwrap(); + let EncryptionTarget::Subkey(subkey) = target else { + panic!("expected the encryption subkey to be selected"); + }; + assert!(subkey.key.algorithm().can_encrypt()); + } + + #[test] + fn sign_only_key_without_encryption_subkey_is_rejected() { + // erin.asc is an Ed25519 sign-only primary key with no subkeys at all. + let certificate = load_certificate("erin.asc"); + let err = select_encryption_target(&certificate) + .unwrap_err() + .to_string(); + assert!(err.contains("cannot be used for encryption"), "{err}"); + } +} 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..d367d404f2 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs @@ -0,0 +1,118 @@ +// 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 +// +// https://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. + +use crate::controller_services::key_parsing::{KeyKind, parse_key_file}; +use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType}; +use pgp::composed::{SignedPublicKey, SignedSecretKey}; + +pub(crate) struct SecretKeyFile {} + +impl PropertySchema for SecretKeyFile { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = true; +} + +impl PropertyType for SecretKeyFile { + type Output = Vec; + + fn parse(s: &str) -> Result { + parse_key_file(s, KeyKind::Secret) + } +} + +pub(crate) struct PublicKeyFile {} +impl PropertySchema for PublicKeyFile { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = true; +} + +impl PropertyType for PublicKeyFile { + type Output = Vec; + + fn parse(s: &str) -> Result { + parse_key_file(s, KeyKind::Public) + } +} + +#[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..26481bac92 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs @@ -0,0 +1,141 @@ +// 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 +// +// https://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. + +use minifi_native::MinifiError; +use pgp::composed::SignedKeyDetails; +use pgp::types::KeyId; + +fn matches_key_id(key_id: &KeyId, target: &str) -> bool { + key_id.to_string().eq_ignore_ascii_case(target) +} + +fn matches_user_id(details: &SignedKeyDetails, target_lower: &str) -> bool { + details.users.iter().any(|user| { + user.id + .as_str() + .map(|user_id| user_id.to_ascii_lowercase().contains(target_lower)) + .unwrap_or(false) + }) +} + +pub(crate) fn find_unique_key<'a, K, F>( + keys: &'a [K], + target_id: &str, + key_parts: F, +) -> Result<&'a K, MinifiError> +where + F: Fn(&'a K) -> (KeyId, &'a SignedKeyDetails), +{ + let target = target_id.trim(); + if target.is_empty() { + return Err(MinifiError::custom("No key search string was given")); + } + + if let Some(key) = keys + .iter() + .find(|key| matches_key_id(&key_parts(key).0, target)) + { + return Ok(key); + } + + let target_lower = target.to_ascii_lowercase(); + let mut matches = keys + .iter() + .filter(|key| matches_user_id(key_parts(key).1, &target_lower)); + + let Some(first_match) = matches.next() else { + return Err(MinifiError::custom(format!( + "No key matching '{target}' was found" + ))); + }; + + let ambiguous: Vec = std::iter::once(first_match) + .chain(matches) + .map(|key| key_parts(key).0.to_string()) + .collect(); + if ambiguous.len() > 1 { + return Err(MinifiError::custom(format!( + "'{target}' is ambiguous, it matches {} keys: {}", + ambiguous.len(), + ambiguous.join(", ") + ))); + } + + Ok(first_match) +} + +#[cfg(test)] +mod tests { + use super::*; + use pgp::composed::SignedKeyDetails; + + /// A stand-in for a key: just the parts `find_unique_key` looks at. + #[derive(Debug)] + struct TestKey { + key_id: KeyId, + details: SignedKeyDetails, + } + + 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) + } + + fn find<'a>(keys: &'a [TestKey], target: &str) -> Result<&'a TestKey, MinifiError> { + find_unique_key(keys, target, |key| (key.key_id, &key.details)) + } + + fn no_details() -> SignedKeyDetails { + SignedKeyDetails::new(vec![], vec![], vec![], vec![]) + } + + #[test] + fn empty_target_never_matches() { + let keys = [TestKey { + key_id: key_id_from_hex("1122334455667788"), + details: no_details(), + }]; + assert!(find(&keys, "").is_err()); + assert!(find(&keys, " ").is_err()); + } + + #[test] + fn matches_key_id_case_insensitive() { + let keys = [TestKey { + key_id: key_id_from_hex("11ABcdEF33445566"), + details: no_details(), + }]; + + assert!(find(&keys, "11abcdef33445566").is_ok()); + assert!(find(&keys, "11ABCDEF33445566").is_ok()); + assert!(find(&keys, "11abcdef3344556").is_err()); // 15 chars + assert!(find(&keys, "abcdef33445566").is_err()); + } + + #[test] + fn missing_key_reports_the_search_string() { + let keys = [TestKey { + key_id: key_id_from_hex("1122334455667788"), + details: no_details(), + }]; + let err = find(&keys, "99aabbccddeeff00").unwrap_err().to_string(); + assert!(err.contains("99aabbccddeeff00"), "{err}"); + } +} diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_parsing.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_parsing.rs new file mode 100644 index 0000000000..70acdcb27b --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_parsing.rs @@ -0,0 +1,90 @@ +// 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 +// +// https://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. + +use minifi_native::{GetProperty, MinifiError, Property, PropertyType}; +use pgp::composed::Deserializable; + +/// Describes which flavour of key is being loaded, used for error messages only. +#[derive(Copy, Clone)] +pub(crate) enum KeyKind { + Public, + Secret, +} + +impl KeyKind { + fn no_valid_keys(self) -> MinifiError { + match self { + KeyKind::Public => MinifiError::validation("Couldn't load any valid public keys"), + KeyKind::Secret => MinifiError::validation("Couldn't load any valid secret keys"), + } + } +} + +/// Parses every key found in ASCII Armored `input`. +pub(crate) fn parse_armored_keys( + input: &str, + kind: KeyKind, +) -> Result, MinifiError> { + let mut keys: Vec = Vec::new(); + if let Ok((parsed, _headers)) = T::from_armor_many(input.as_bytes()) { + keys.extend(parsed.filter_map(Result::ok)); + } + non_empty(keys, kind) +} + +/// Parses every key found in the file at `path`, which may be ASCII Armored or binary. +pub(crate) fn parse_key_file( + path: &str, + kind: KeyKind, +) -> Result, MinifiError> { + let mut keys: Vec = Vec::new(); + if let Ok((parsed, _headers)) = T::from_armor_file_many(path) { + keys.extend(parsed.filter_map(Result::ok)); + } else if let Ok(parsed) = T::from_file_many(path) { + keys.extend(parsed.filter_map(Result::ok)); + } + non_empty(keys, kind) +} + +/// Loads the keys of a controller service from its file property and its inline property, +/// failing when neither yields a usable key. +pub(crate) fn load_service_keys( + context: &Ctx, + file_property: &Property>, + inline_property: &Property>, +) -> Result, MinifiError> +where + Ctx: GetProperty, + File: PropertyType>, + Inline: PropertyType>, +{ + let mut keys = context.get_property(file_property)?.unwrap_or_default(); + keys.extend(context.get_property(inline_property)?.unwrap_or_default()); + + if keys.is_empty() { + return Err(MinifiError::validation("Could not load any valid keys")); + } + Ok(keys) +} + +fn non_empty(keys: Vec, kind: KeyKind) -> Result, MinifiError> { + if keys.is_empty() { + Err(kind.no_valid_keys()) + } else { + Ok(keys) + } +} 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..fc5c629a23 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// https://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. + +use crate::controller_services::key_parsing::{KeyKind, parse_armored_keys}; +use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType}; +use pgp::composed::{SignedPublicKey, SignedSecretKey}; + +pub(crate) struct SecretKey {} + +impl PropertySchema for SecretKey { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = true; +} + +impl PropertyType for SecretKey { + type Output = Vec; + + fn parse(s: &str) -> Result { + parse_armored_keys(s, KeyKind::Secret) + } +} + +pub(crate) struct PublicKey {} +impl PropertySchema for PublicKey { + const CONSTRAINT: Option = None; + const IS_REQUIRED: bool = true; +} + +impl PropertyType for PublicKey { + type Output = Vec; + + fn parse(s: &str) -> Result { + parse_armored_keys(s, KeyKind::Public) + } +} 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..3aa314bc0d --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs @@ -0,0 +1,24 @@ +// 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 +// +// https://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. + +pub(crate) mod encryption_key; +mod key_file_property; +mod key_lookup; +mod key_parsing; +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..56171434e3 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs @@ -0,0 +1,249 @@ +// 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 +// +// https://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. + +#[cfg(test)] +use crate::controller_services::key_lookup::find_unique_key; +use crate::controller_services::key_parsing::load_service_keys; +use minifi_native::macros::ComponentIdentifier; +use minifi_native::{EnableControllerService, GetProperty, Logger, MinifiError}; +use pgp::composed::{SignedSecretKey, TheRing}; +#[cfg(test)] +use pgp::types::KeyDetails; +use service_def::*; + +#[derive(Debug, ComponentIdentifier)] +pub(crate) struct PGPPrivateKeyService { + private_keys: Vec, + passphrases: Vec, +} + +impl EnableControllerService for PGPPrivateKeyService { + fn enable(context: &P, _logger: &L) -> Result + where + Self: Sized, + { + let private_keys = load_service_keys(context, &KEY_FILE, &KEY)?; + let passphrases = context.get_property(&KEY_PASSWORD)?.unwrap_or_default(); + + Ok(Self { + private_keys, + passphrases, + }) + } +} + +impl PGPPrivateKeyService { + pub fn get_the_ring(&'_ self) -> TheRing<'_> { + TheRing { + secret_keys: self.private_keys.iter().collect(), + key_passwords: self.passphrases.iter().collect(), + message_password: vec![], + session_keys: vec![], + decrypt_options: Default::default(), + } + } + + #[cfg(test)] + pub fn get_secret_key(&self, target_id: &str) -> Result<&SignedSecretKey, MinifiError> { + find_unique_key(&self.private_keys, target_id, |private_key| { + ( + private_key.primary_key.legacy_key_id(), + &private_key.details, + ) + }) + } +} + +mod service_def { + use crate::controller_services::key_file_property::SecretKeyFile; + use crate::controller_services::key_property::SecretKey; + use crate::controller_services::private_key_service::PGPPrivateKeyService; + use crate::utils; + use minifi_native::{ + ControllerServiceDefinition, Property, PropertyDefinition, ProvidedInterface, + property_definitions, + }; + + pub(super) const KEY_FILE: Property> = Property::new( + "Keyring File", + "File path to PGP Secret Key encoded in binary or ASCII Armor", + ) + .supports_expression_language(); + + pub(super) const KEY: Property> = + Property::new("Keyring", "Secret Key encoded in ASCII Armor").sensitive(); + + pub(super) const KEY_PASSWORD: Property> = Property::new( + "Key Password", + "Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn", + ) + .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_PASSWORD]; + const PROVIDED_APIS: &'static [ProvidedInterface] = &[]; + } +} + +#[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( + "Keyring 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_ok()); + assert!(service.get_secret_key("alice@example.com").is_ok()); + + assert!(service.get_secret_key("Bob").is_err()); + assert!(service.get_secret_key("Carol").is_err()); + } + + #[test] + fn single_binary_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Keyring 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_ok()); + assert!(service.get_secret_key("Alice").is_ok()); + assert!(service.get_secret_key("Alice ").is_ok()); + + assert!(service.get_secret_key("").is_err()); + + assert!(service.get_secret_key("Bob").is_err()); + assert!(service.get_secret_key("Carol").is_err()); + } + + #[test] + fn armored_keyring_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Keyring 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_ok()); + assert!(service.get_secret_key("Bob").is_ok()); + assert!(service.get_secret_key("bob@home.io").is_ok()); + assert!(service.get_secret_key("bob@work.com").is_ok()); + assert!(service.get_secret_key("Carol").is_err()); + } + + #[test] + fn binary_keyring_key_file() { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Keyring 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_ok()); + assert!(service.get_secret_key("Bob").is_ok()); + assert!(service.get_secret_key("bob@home.io").is_ok()); + assert!(service.get_secret_key("bob@work.com").is_ok()); + assert!(service.get_secret_key("Carol").is_err()); + } + + #[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("Keyring".to_string(), file_content); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_ok()); + assert!(service.get_secret_key("Bob").is_ok()); + assert!(service.get_secret_key("bob@home.io").is_ok()); + assert!(service.get_secret_key("bob@work.com").is_ok()); + assert!(service.get_secret_key("Carol").is_err()); + } + + #[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("Keyring".to_string(), file_content); + + let service = + PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable"); + assert!(service.get_secret_key("Alice").is_ok()); + assert!(service.get_secret_key("Bob").is_err()); + assert!(service.get_secret_key("Carol").is_err()); + } + + #[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("Keyring".to_string(), file_content); + + assert!(PGPPrivateKeyService::enable(&context, &MockLogger::new()).is_err()); + } +} 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..1fc0987c5f --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs @@ -0,0 +1,296 @@ +// 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 +// +// https://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. + +use crate::controller_services::key_lookup::find_unique_key; +use crate::controller_services::key_parsing::load_service_keys; +use minifi_native::macros::ComponentIdentifier; +use minifi_native::{EnableControllerService, GetProperty, Logger, MinifiError}; +use pgp::composed::SignedPublicKey; +use pgp::types::KeyDetails; +use service_def::*; + +#[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 public_keys = load_service_keys(context, &KEYRING_FILE, &KEYRING)?; + Ok(Self { public_keys }) + } +} + +impl PGPPublicKeyService { + pub fn get(&self, target_id: &str) -> Result<&SignedPublicKey, MinifiError> { + find_unique_key(&self.public_keys, target_id, |public_key| { + (public_key.primary_key.legacy_key_id(), &public_key.details) + }) + } +} + +mod service_def { + use crate::controller_services::key_file_property::PublicKeyFile; + use crate::controller_services::key_property::PublicKey; + use crate::controller_services::public_key_service::PGPPublicKeyService; + 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 Public Key encoded in binary or ASCII Armor", + ) + .supports_expression_language(); + + pub(crate) const KEYRING: Property> = Property::new( + "Keyring", + "PGP Keyring or Public Key encoded in ASCII Armor", + ); + + 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] = &[]; + } +} + +#[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_ok()); + assert!(controller_service.get("alice@example.com").is_ok()); + + assert!(controller_service.get("Bob").is_err()); + assert!(controller_service.get("Carol").is_err()); + } + + #[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_ok()); + assert!(service.get("Alice").is_ok()); + assert!(service.get("Alice ").is_ok()); + + assert!(service.get("").is_err()); + + assert!(service.get("Bob").is_err()); + assert!(service.get("Carol").is_err()); + } + + #[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_ok()); + assert!(service.get("Bob").is_ok()); + assert!(service.get("bob@home.io").is_ok()); + assert!(service.get("bob@work.com").is_ok()); + assert!(service.get("Carol").is_err()); + } + + #[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_ok()); + assert!(service.get("Bob").is_ok()); + assert!(service.get("bob@home.io").is_ok()); + assert!(service.get("bob@work.com").is_ok()); + assert!(service.get("Carol").is_err()); + } + + #[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_ok()); + assert!(service.get("Bob").is_ok()); + assert!(service.get("bob@home.io").is_ok()); + assert!(service.get("bob@work.com").is_ok()); + assert!(service.get("Carol").is_err()); + } + + #[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_ok()); + assert!(service.get("Bob").is_err()); + assert!(service.get("Carol").is_err()); + } + + #[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_ok()); + assert!(service.get(&key_id_hex.to_ascii_uppercase()).is_ok()); + assert!(service.get(&key_id_hex[..8]).is_err()); + assert!(service.get("0123456789abcdef").is_err()); + } + + fn ambiguous_keyring_service() -> PGPPublicKeyService { + let mut context = MockControllerServiceContext::new(); + context.properties.insert( + "Keyring File".to_string(), + get_test_key_path("ambiguous_keyring.gpg"), + ); + + PGPPublicKeyService::enable(&context, &MockLogger::new()).expect("enable should succeed") + } + + /// A User ID search that matches more than one key must fail loudly instead of picking one + /// of them, otherwise a look-alike key silently becomes the recipient. + #[test] + fn a_user_id_matching_several_keys_is_reported_as_ambiguous() { + let service = ambiguous_keyring_service(); + + // "bob@home.io" is a substring of the look-alike "bob@home.io.attacker.test" too. + let err = service.get("bob@home.io").unwrap_err().to_string(); + assert!(err.contains("ambiguous"), "{err}"); + assert!(err.contains("bob@home.io"), "{err}"); + + // Unaffected searches still resolve. + assert!(service.get("Alice").is_ok()); + assert!(service.get("bob@work.com").is_ok()); + assert!(service.get("bob@home.io.attacker.test").is_ok()); + } + + /// An exact Key ID is never ambiguous, so it stays usable as the way to disambiguate. + #[test] + fn a_key_id_search_wins_over_an_ambiguous_user_id() { + let service = ambiguous_keyring_service(); + + let real_bob = service + .get("bob@work.com") + .expect("the real Bob should be found by his unique User ID"); + let real_bob_key_id = real_bob.primary_key.legacy_key_id().to_string(); + + let found = service + .get(&real_bob_key_id) + .expect("a Key ID search should never be ambiguous"); + assert_eq!( + found.primary_key.legacy_key_id(), + real_bob.primary_key.legacy_key_id() + ); + } +} 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..eba818524f --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/lib.rs @@ -0,0 +1,41 @@ +// 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 +// +// https://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. + +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..f801014101 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs @@ -0,0 +1,474 @@ +// 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 +// +// https://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. + +use proc_def::*; + +use crate::controller_services::private_key_service::PGPPrivateKeyService; + +use minifi_native::macros::ComponentIdentifier; +use minifi_native::{ + FlowFileStreamTransform, GetControllerService, GetProperty, InputStream, Logger, MinifiError, + OutputStream, ProcessError, RouteErrorExt, Schedule, TransformStreamResult, +}; +use pgp::composed::{Message, TheRing}; + +#[derive(Debug, ComponentIdentifier)] +pub(crate) struct DecryptContentPGP { + symmetric_password: Option, +} + +impl Schedule for DecryptContentPGP { + fn schedule( + context: &P, + _logger: &L, + ) -> Result + where + Self: Sized, + L: Logger, + { + let symmetric_password = context.get_property(&SYMMETRIC_PASSPHRASE)?; + let private_key_service = context.get_controller_service(&PRIVATE_KEY_SERVICE)?; + if private_key_service.is_none() && symmetric_password.is_none() { + Err(MinifiError::validation( + "Either Symmetric Password or Private Key Service must be set", + )) + } else { + Ok(DecryptContentPGP { 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) + } +} + +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 decrypted_msg.is_compressed() { + decrypted_msg = decrypted_msg + .decompress() + .map_err(MinifiError::other) + .route_err_to_failure()? + }; + + let _written_bytes = + std::io::copy(&mut decrypted_msg.into_inner(), output_stream).route_err_to_failure()?; + + Ok(TransformStreamResult::new(&SUCCESS)) + } +} + +mod proc_def { + use super::*; + 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 SYMMETRIC_PASSPHRASE: Property> = Property::new( + "Passphrase", + "Passphrase 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."; + const INPUT_REQUIREMENT: ProcessorInputRequirement = ProcessorInputRequirement::Required; + const SUPPORTS_DYNAMIC_PROPERTIES: bool = false; + const SUPPORTS_DYNAMIC_RELATIONSHIPS: bool = false; + const OUTPUT_ATTRIBUTES: &'static [OutputAttribute] = &[]; + const RELATIONSHIPS: &'static [Relationship] = &[SUCCESS, FAILURE]; + const PROPERTIES: &[PropertyDefinition] = + property_definitions![SYMMETRIC_PASSPHRASE, PRIVATE_KEY_SERVICE,]; + } +} + +#[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_PASSPHRASE.name(), + "my_secret_password".to_string(), + ); + let decrypt_content = DecryptContentPGP::schedule(&context, &MockLogger::new()); + assert!(decrypt_content.is_ok()); + } + + #[test] + fn schedule_fails_with_invalid_controller() { + let mut context = MockProcessContext::new(); + context.properties.insert( + PRIVATE_KEY_SERVICE.name(), + "invalid_private_key_service".to_string(), + ); + let decrypt_content = DecryptContentPGP::schedule(&context, &MockLogger::new()); + assert!(decrypt_content.is_err()); + } + + #[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( + "Keyring File", + test_utils::get_test_key_path(self.key_filename), + ); + + if let Some(passphrase) = self.passphrase { + context.properties.insert("Key Password", 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_PASSPHRASE.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); + assert_eq!(output, result_bytes); + } + 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(()), + ); + } + + /// Encrypts `content` for `public_key_search` using `keyring_file` and returns the ciphertext. + fn encrypt_for(keyring_file: &str, public_key_search: &str, content: &[u8]) -> Vec { + use crate::controller_services::public_key_service::PGPPublicKeyService; + use crate::processors::encrypt_content::EncryptContentPGP; + + let mut key_service_context = MockControllerServiceContext::new(); + key_service_context.properties.insert( + "Keyring File".to_string(), + test_utils::get_test_key_path(keyring_file), + ); + let key_service = PGPPublicKeyService::enable(&key_service_context, &MockLogger::new()) + .expect("should enable"); + + let mut context = MockProcessContext::new(); + context.properties.extend([ + ("Public Key Service", "my_public_key_service"), + ("Public Key Search", public_key_search), + ]); + context + .controller_services + .insert("my_public_key_service".to_string(), Box::new(key_service)); + + let mut ciphertext: Vec = Vec::new(); + let mut plaintext = std::io::Cursor::new(content); + let encrypt_content = + EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + encrypt_content + .transform( + &context, + &mut plaintext, + &mut ciphertext, + &MockLogger::new(), + ) + .expect("should encrypt"); + ciphertext + } + + fn decrypt_with(private_key_data: PrivateKeyData, ciphertext: Vec) -> Vec { + let mut context = MockProcessContext::new(); + context.controller_services.insert( + "my_private_key_service".to_string(), + Box::new(private_key_data.into_controller()), + ); + context.properties.insert( + PRIVATE_KEY_SERVICE.name(), + "my_private_key_service".to_string(), + ); + + let decrypt_content = + DecryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + let mut output: Vec = Vec::new(); + let mut ciphertext = std::io::Cursor::new(ciphertext); + let res = decrypt_content + .transform(&context, &mut ciphertext, &mut output, &MockLogger::new()) + .expect("should decrypt"); + assert_eq!(res.target_relationship_name(), SUCCESS.name); + assert_eq!(res.write_status(), IoState::Ok); + output + } + + /// Dave's primary key is Ed25519, which cannot encrypt; only his Cv25519 subkey can. This + /// round trip only works if EncryptContentPGP encrypts to the subkey. + #[test] + fn round_trip_with_a_sign_only_primary_key() { + let ciphertext = encrypt_for("dave.asc", "dave@example.com", b"for dave only"); + + let dave_private_key = PrivateKeyData { + key_filename: "dave_private.asc", + passphrase: Some("gardenparty"), + }; + assert_eq!(decrypt_with(dave_private_key, ciphertext), b"for dave only"); + } + + /// A keyring holding keys with different passphrases needs every one of those passphrases, + /// which "Key Password" accepts one per line. + #[test] + fn decrypts_with_one_of_several_key_passwords() { + let ciphertext = encrypt_for("dave.asc", "dave@example.com", b"for dave only"); + + // mixed_secret_keyring.gpg holds Alice (whiterabbit) and Dave (gardenparty). + let both_passwords = PrivateKeyData { + key_filename: "mixed_secret_keyring.gpg", + passphrase: Some("whiterabbit\ngardenparty"), + }; + assert_eq!( + decrypt_with(both_passwords, ciphertext.clone()), + b"for dave only" + ); + + // Alice's passphrase alone cannot unlock Dave's key. + let alice_password_only = PrivateKeyData { + key_filename: "mixed_secret_keyring.gpg", + passphrase: Some("whiterabbit"), + }; + let mut context = MockProcessContext::new(); + context.controller_services.insert( + "my_private_key_service".to_string(), + Box::new(alice_password_only.into_controller()), + ); + context.properties.insert( + PRIVATE_KEY_SERVICE.name(), + "my_private_key_service".to_string(), + ); + let decrypt_content = + DecryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule"); + let mut output: Vec = Vec::new(); + let mut ciphertext = std::io::Cursor::new(ciphertext); + let res = + decrypt_content.transform(&context, &mut ciphertext, &mut output, &MockLogger::new()); + test::assert_routed_to(res, &FAILURE); + } + + #[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/encrypt_content.rs b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs new file mode 100644 index 0000000000..14b6ce3cab --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs @@ -0,0 +1,392 @@ +// 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 +// +// https://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. + +use crate::controller_services::encryption_key::{EncryptionTarget, select_encryption_target}; +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}; + +use proc_def::*; + +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 { + match select_encryption_target(pub_key)? { + EncryptionTarget::Primary(primary_key) => builder + .encrypt_to_key(rand::thread_rng(), primary_key) + .map_err(MinifiError::other)?, + EncryptionTarget::Subkey(subkey) => builder + .encrypt_to_key(rand::thread_rng(), subkey) + .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), + } + } +} + +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(&SYMMETRIC_PASSPHRASE)?; + + let public_key_service = context.get_controller_service(&PUBLIC_KEY_SERVICE)?; + + // Given API support we should check if PUBLIC_KEY_SEARCH is set (without EL) + if symmetric_password.is_none() && public_key_service.is_none() { + return Err(MinifiError::custom( + "Either a password or Public Key Service with Public Key Search should be configured to encrypt files", + )); + } + 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(Some(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 = match context.get_attribute("filename")? { + Some(file_name) => file_name, + None => 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())) + } +} + +mod proc_def { + use super::*; + 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 SYMMETRIC_PASSPHRASE: Property> = Property::new( + "Passphrase", + "Passphrase 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]; + + const PROPERTIES: &[PropertyDefinition] = property_definitions![ + FILE_ENCODING, + SYMMETRIC_PASSPHRASE, + PUBLIC_KEY_SEARCH, + PUBLIC_KEY_SERVICE, + ]; + } +} + +#[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(SYMMETRIC_PASSPHRASE.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); + } + + #[test] + fn configured_public_key_miss_fails_even_with_password() { + let mut context = MockProcessContext::new(); + context.properties.extend([ + ("Public Key Service", "my_controller_service"), + ("Public Key Search", "Carol"), + ("Symmetric Password", "password"), + ]); + + 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/mod.rs b/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs new file mode 100644 index 0000000000..a22c7b1b82 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs @@ -0,0 +1,19 @@ +// 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 +// +// https://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. + +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..1ceb14cab8 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// https://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. + +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..7528e06b3c --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/src/utils.rs @@ -0,0 +1,80 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// https://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. + +use minifi_native::{ + MinifiError, PropertyConstraints, PropertySchema, PropertyType, StandardPropertyValidator, +}; + +pub(crate) struct Password {} + +impl PropertySchema for Password { + const CONSTRAINT: Option = Some(PropertyConstraints::Validator( + StandardPropertyValidator::NonBlankValidator, + )); + const IS_REQUIRED: bool = true; +} + +impl PropertyType for Password { + type Output = pgp::types::Password; + + fn parse(s: &str) -> Result { + Ok(pgp::types::Password::from(s)) + } +} + +/// A newline separated list of passwords, each of which is tried in turn when unlocking a key. +pub(crate) struct Passwords {} + +impl PropertySchema for Passwords { + const CONSTRAINT: Option = Some(PropertyConstraints::Validator( + StandardPropertyValidator::NonBlankValidator, + )); + const IS_REQUIRED: bool = true; +} + +impl PropertyType for Passwords { + type Output = Vec; + + fn parse(s: &str) -> Result { + Ok(s.lines() + .filter(|line| !line.is_empty()) + .map(pgp::types::Password::from) + .collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_password() { + assert_eq!(Passwords::parse("hunter2").unwrap().len(), 1); + } + + #[test] + fn one_password_per_line() { + let passwords = Passwords::parse("alice-pw\nbob-pw").unwrap(); + assert_eq!(passwords.len(), 2); + } + + #[test] + fn blank_lines_are_dropped() { + let passwords = Passwords::parse("alice-pw\n\nbob-pw\n").unwrap(); + assert_eq!(passwords.len(), 2); + } +} 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..86984f128e --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/README.txt @@ -0,0 +1,59 @@ +Testing keys v4 +------------------------ +uid [ultimate] Alice +keyid BCCE3FDFBA019D7E +passphrase whiterabbit +RSA, the primary key carries the encrypt capability itself, no subkeys + +uid [ultimate] Bob Personal +uid [ultimate] Bob Primary +keyid A06749BA4F34B0E5 +no passphrase + +uid [ultimate] Dave +keyid 297B6A88887FB64F +passphrase gardenparty +Ed25519 sign-only primary key plus a Cv25519 encryption subkey, which is the +layout `gpg --gen-key` produces by default. Encrypting to this key only works +if the encryption subkey is selected rather than the primary key. + gpg --quick-generate-key 'Dave ' ed25519 sign never + gpg --quick-add-key cv25519 encr never + +uid [ultimate] Erin +keyid 8C96441440A22F74 +no passphrase +Ed25519 sign-only primary key with no subkeys at all, so it cannot be used for +encryption. Public key only. + gpg --quick-generate-key 'Erin ' ed25519 sign never + +uid [ultimate] Bob Personal +keyid 756FC2EBECB8747C +no passphrase +A look-alike of Bob whose User ID has Bob's own address as a prefix, so that a +"bob@home.io" search matches both keys. Only used to build +ambiguous_keyring.gpg. Public key only. + +Keyrings +------------------------ +keyring.{asc,gpg} public: Alice + Bob +secret_keyring.{asc,gpg} secret: Alice + Bob +ambiguous_keyring.gpg public: Alice + Bob + the look-alike Bob, so + that a "bob@home.io" User ID search is + ambiguous +mixed_secret_keyring.gpg secret: Alice + Dave, i.e. two keys whose + passphrases differ, which requires more than + one Key Password to unlock both + +The binary keyrings are concatenations of the individual dearmored keys: + gpg --dearmor < keyring.asc > keyring.bin + gpg --dearmor < spoofed_bob.asc > spoofed.bin + cat keyring.bin spoofed.bin > ambiguous_keyring.gpg + +Passphrase protection +------------------------ +Every passphrase-protected secret key here is deliberately protected with the +lowest S2K iteration count gpg will emit (65536, its floor). Unlocking a key +runs the full S2K hash chain, and gpg's own calibrated default of 58720256 means +56 MiB of SHA-1 per protected packet. Dave has two protected packets and each +candidate passphrase is tried against each, which made the decrypt_content tests +take seconds apiece. Keep the low count when regenerating. 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..ebe21a684e --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc @@ -0,0 +1,18 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGmJ4mMBCADEYKe0I+Mr/IPKlYUVEgb0yC+iRRBfkhPfMQjbjfM3sD2jyf0g +k983pVWF5I4Tl3AFW+D2wAJCayFnIYQWaMnK133EdxZE61ewhvS9g3Zk2Q1JzO6F +B/w4hfCSlPkK82j3Xrd1U0ZJ+YP60DucYlanFPu8+Iw7reusOT9JOJWazzCo4XQX +ImXomT+KVFsN9UDaKv0mSoH5VgG6thQb6veajcYhrN7rU7isTA2hICxo1nLlnCpy +7LbAcN9sP1r5Ukpw0WsXKItol0f29p72biFrLbL7670lRxl/CuHTbHYpbCo7Cdng +R1ieQ3yEqX4qqrKbNocpylCrSSuc78F7pxYNABEBAAG0GUFsaWNlIDxhbGljZUBl +eGFtcGxlLmNvbT6JAVIEEwEIADwWIQSZnIioWmY7HCCheVW8zj/fugGdfgUCaYni +YwIbLwULCQgHAgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQvM4/37oBnX7RUQf7 +BZV8ZPlZxbQaZ8kMabC9meNVaYTOWIjJpDqJ7XqG6mMwvzeA3hvtgQOEEQTYbUcw ++ItlN9XQVrlNAyJZVGJQ+2UXp4XRmdogyShE/zFv/NkxvccfzC/BD5jMy88BS8lu +Gv8bANioE8bF/IwC538A5/6oPN1weQ4OpjcBfeYoyDD32sMc2P5uAWe9pLTAR+vz +F5mWG4K6TWrfH8/YuB6CCQSCr17N9ZPP2rpcPa2miyzQGJy5wdMOiahBFfdvHACO +bJXeKYcxZdLs7ImMw78IbbVkzmEKzCStLTqntL8Bsh+eu4nvescoYkMPUzW1E3P3 +BavsyDPmu/yP03Dpg0TubA== +=+8XC +-----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..8ef375e68d 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..6b7ca4d44b --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc @@ -0,0 +1,32 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQPGBGmJ4mMBCADEYKe0I+Mr/IPKlYUVEgb0yC+iRRBfkhPfMQjbjfM3sD2jyf0g +k983pVWF5I4Tl3AFW+D2wAJCayFnIYQWaMnK133EdxZE61ewhvS9g3Zk2Q1JzO6F +B/w4hfCSlPkK82j3Xrd1U0ZJ+YP60DucYlanFPu8+Iw7reusOT9JOJWazzCo4XQX +ImXomT+KVFsN9UDaKv0mSoH5VgG6thQb6veajcYhrN7rU7isTA2hICxo1nLlnCpy +7LbAcN9sP1r5Ukpw0WsXKItol0f29p72biFrLbL7670lRxl/CuHTbHYpbCo7Cdng +R1ieQ3yEqX4qqrKbNocpylCrSSuc78F7pxYNABEBAAH+BwMC47UFwy38Rctg1BsL +M8qlY9DTZ4OSEupNBRmlzoBs8VDNNnMpm97qvxJfG4LVb8/rkqTqsPs4BBBB2rkU +afdstA9z6ctk4fAbbP4+EO5ZNWx4vX0+LbwyfACVZe+lV0XUC7UddKmG4lNzi+3N +aOOQSAUsPVYcKBGWh7QvqjQ6FHsLcCTCJVCwJCJ3HfyLx+RVq64k2kakCRpVXpU7 +q2y3VHzICXney7GtlkuGLOZBjxi/82BQCMn4KqH1BjlA/bE6kCnThisV8C02gg75 +7BKKbzSxmoyMDCtLzPoECEoZOihpNtND/px6yh6QOTYIy/bAv8siPTVEe/ht/4Gz +FEOvYMWkYxaZEiTdaBTXyG5BaIs4ZwOnJAjHQX7De3dbbQGCOIdCvHCJwrtnNMCk +878k9dFm68a1NGK3gEWY7b66sYOXbZDA+ADv3wH2uhE/vMG8yuTfVA7Vb7saUqbf +SVAN3kGgd04T1z1bVGRLMG0q6/2Im+pveTTyPBhMW0hAmZMHXioUi49E78FVBYhy +3fa1NbXIVP2VUXISaPyzU5NLoKJ25PQT0paKB83ICaxWBNC7Htljack30pUgsaht +aPVITGBefrU9dUX0dn+ZbvlhKivQvDi/pZ6ybpMUZ7nvBpCchMwGfpTo+9E6gfMI +PDj0WPOw/8wy5GL9zuYCYzVbjCINJRUV2CYDkcIkXiyGAkbdaVyL/e4b1J9Zd32L ++5QL59ZnQZ14NEDTavzTL6QkKG+Pu6Js2zVm23ycCprUC7l7vIYSS4li3hKRMcb7 +izm7rOephgfeCsCJNoGWIBORhSqmBtT3A9Ui6TZcFnyMq2oY6+eW2lppMq8uYH/n +1+LFc3jIEb4CPMCqwItrGfpAmpfqEdzg4rh6g755KADdMmzygk18CiThb42mVZTj +sFRsEKnlv/46tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5jb20+iQFSBBMBCAA8FiEE +mZyIqFpmOxwgoXlVvM4/37oBnX4FAmmJ4mMCGy8FCwkIBwIDIgIBBhUKCQgLAgQW +AgMBAh4HAheAAAoJELzOP9+6AZ1+0VEH+wWVfGT5WcW0GmfJDGmwvZnjVWmEzliI +yaQ6ie16hupjML83gN4b7YEDhBEE2G1HMPiLZTfV0Fa5TQMiWVRiUPtlF6eF0Zna +IMkoRP8xb/zZMb3HH8wvwQ+YzMvPAUvJbhr/GwDYqBPGxfyMAud/AOf+qDzdcHkO +DqY3AX3mKMgw99rDHNj+bgFnvaS0wEfr8xeZlhuCuk1q3x/P2LgeggkEgq9ezfWT +z9q6XD2tposs0BicucHTDomoQRX3bxwAjmyV3imHMWXS7OyJjMO/CG21ZM5hCswk +rS06p7S/AbIfnruJ73rHKGJDD1M1tRNz9wWr7Mgz5rv8j9Nw6YNE7mw= +=+91n +-----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..c0cfc2c529 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/ambiguous_keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/ambiguous_keyring.gpg new file mode 100644 index 0000000000..c0e9da5ac4 Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/ambiguous_keyring.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----- + +lQOYBGmJ4P8BCADX7NFGPaQCjP+YL2o74CCNHt+Dx0AkvjUp6mEEb+Chte8/82Rd +tH9VlxAxrUL560/UzsHAQYGMzbDFzn041GeWcElMr/n7iH3wQSDlxWXE+gSh+vpt +HKatqzYChSQ8l7u6cmZ3Wuj6ZtcVr0lryZ5x5ja2lPLoK2BnKCoDKwROJLmDR8SY +S2DwH/n6UU/yqyqlFrdDaZQHyJF26z01rx7nCtfIuWSp5ZS4Fr9qf5eZnFwsY7RN +r8X74iAbVRkr2uN7jzvijoH2qW6irqHm9bANmcmdQXJE0fSFU6/rJiuwg8IdzCca +8fJZIzxK5XaMA67rRHXWf5+hFEWyN49oV2UZABEBAAEAB/sEw+zbOJOmht9Ca4k+ +wKdIJSOk9oWiL0rLBPColP5JVgVfSZQlckXreth6EKJPIflfCGBVkLAW8cos+hF2 +BzOU1K31c6ytcF0GgNkEtnyUvsd8VuatW581+X3QS3rhDq7EtCfrCl1WbSv+abdK +vZuuYxwzu78VMkAY1A6e3KrQvWulVI/jbimbdeHTLKLAo2RYU2HtxPCjnnDfX7GL +a7zulsH4BSkbguGNqjZGOuNyDjRbMHKdmuPG9KlMLBKJgayjxYZFzmU9BKpu4g6x +w2fmBq4wGaNentrWH4fsUpc+SsvCDQ7Wchq6gh176KNOF7WRNW0P4L+w3XBUXMBA +mwK1BADlZEmbI+8sZakxWEF/FeVvOLFhNc1Ot6UN6YDLBcPEXgg0vsxQTVxDOgSs +oKn4NLFz51n2HBBcXLTSYkAdobAy0EvyXY0soYh/znaJoTyxxSuvTuoz5LZ3vO3v +kEXEs8NqSfsM/trAMJaOYPWcsp3az5mwHKfy+gzAwxmDchEvNQQA8PikLgc89DIi +lNyQ3mDwLL2uJkWVgw83Woq0daNVpqeHN1yAU88FNr+EXA4pFZ9l5VBgPC0UL9NX +t9eCSif67KubgIG5wVM87TKobyZrvYd7EbTbbPVOALes12o6fwzKwd1+4y5swyRx +PxDegwQ5LDZlCEWTLLakHIlovQXMZtUEAIk20jCFTBzRMuE2KtnC5FW+Ye47QQpV +78adjNaizEDmotC6/EB1t/TjwndV6saIxXq+K78QW3bI+XK/zASjniqPCDk0uTF/ +Xo7mAaso5x6WSWUhL1YfSAyS5V34wNdYydfzWw+7itws33ckZ7YnDGaslovmztnD +bT84ZIel57ZwQ6a0GkJvYiBQcmltYXJ5IDxib2JAd29yay5jb20+iQFSBBMBCAA8 +FiEE6sQimyrITfRWlYfRoGdJuk80sOUFAmmJ4P8CGy8FCwkIBwIDIgIBBhUKCQgL +AgQWAgMBAh4HAheAAAoJEKBnSbpPNLDlEPIH/2F5/cdCz05F4I2UUVkOQxKBMuwe +PqYyTi/njkXv1VfyK+/mHgVvDY3qaVlrCInxnNYXl2I11cLW7s0kKa9JsZNAWN5j +oMPL+edkxf3s1X6e+VPd9C8bowWQcqDsoEHrFGwF4FcfVnaol5LIxTdZeVQSjjLj +ySqGvdLGkQW4CVeAZySLQis15A4Zmb3YdS8ddpTPzPUrc3hUvb84TjjptXHOdjDG +DEJbnaSr/T7YruSs6TNUmiqGbZQ7tV8oP1ToAV+xNU9dQOIeCigu0zCCGMDe8J7g +Wjfvpx31WQs4XbKTuxXTTLAdJ01t1SJrQzQV+Bn2Z26LM51rzsb13+CJqxK0GkJv +YiBQZXJzb25hbCA8Ym9iQGhvbWUuaW8+iQFSBBMBCAA8FiEE6sQimyrITfRWlYfR +oGdJuk80sOUFAmmJ4QcCGy8FCwkIBwIDIgIBBhUKCQgLAgQWAgMBAh4HAheAAAoJ +EKBnSbpPNLDlRz0H/199Bi7sNi34bChTfPsujJ6d0SEKzdjJB/aGbmaIwSFLgOho +B+iC6n6wc6oqx0lMUAbz2LGTwxFo/FMJnkqJnrTWPJHoLKByuXy1MiOx9HO6zfc4 +bo7MBKXblOS/DZz4flJ6QcZWuaea9+8nBasxbKH0C7hPD3tS3CDsFPNKDDVAOfGZ +UGOT2fOoDfERWMfsGORB3uZVT7va4IZ2rIieYAp4sU13WXdTdnDKrCkSj7qzEKgA +0OwDlp92re3+dL9P7dI7bHtoEp8bfxNS7WHNG3iBkTxzZdJSUAEFv6FAhR8dIMI2 +XdQ9mFSxmhUCbtYj5c5vdMAzae3Ja4d8taQbvgSdA5gEaYng/wEIAJsLxaaERwjR +YeYsmqkoVzCfdhl7AlN1F65jEV3Bpet+3/zqoPVaDkB+0oOFs/EN1ac3VtU14cs0 +KtlyxTJIrN8qoOOw4D23gV2pt3jFL11Qf5zHFQKHNpMohhNg0JgV4umXnVwJVcLc +xyDtmu7gjimbWAkWgYcoqIFsATjy6En0VwtgHst2+FbkAcbXljhOfzHy5Nltb4co +le+xFgaNFHR2nYSCpItC4Br7M1z6Y22F+C/uDs4vcuY6KSlBPf53K5gE+YP/xhaD +fqg6Q8YtGeuQK3+a9X1Bdy7zuAHBPQQR9m4SObeCXIjVAWW50TUHp6FMLFup/+C5 +jgm9dqIxzX8AEQEAAQAH/isjht5SXptO+rC6x1t6fGvsakUjqx2CblDYgpv2Bc60 +seiidZ9ea6m5P6RVfp/6y+/nH1NaVxUdUjDHVKOtgd/j8fj4HSQ+2xEu5/wDzS5m +9+Ksp6VY7q/aLhfVL6SpLkX1J9TUShbaK9N3GM0PEK716HQ63VY4U04TOXHZcBUn +JnRfdJToEwNTvtDo/9itVXCJsczWVT1aJRdHKekFDHEQSpTZaDFnWMR1Mluh1Tyt +w4Y25KoOkslM4WxuUmx27u+NOBq6/Z8GOHHuiBOGRlbr9KsuS2Ul6aSwYCOUF/qa +HrnvDNoa+w0Unr1zX71QkVRrSFMNazzTxP+Q7yghlr0EAMV64xrQ7ogDP1TzKA7H +lBFYn91BZgH+AZSysPp0osjjTuhtLI4bWDQk+Sg2tVszE+XESKAlYIIz3eQlD7n1 +PgNhQ2IniAcPyrhGzLQalY+woil+a5jIQHkw3z7So+ooSgfIboHiwRbq+lzaUolf +ZpKoJ3VsGOhxU/2XnQQ2jJQLBADI/cdY7O06asiQiuFddAn7NKQmsMV9uj4SfmKO +SF7taSWUEZZb66FwslwqH2UM92abIr2+OI+G3PJFAxLnsngjjZSJcSHeqBlWemBN +C7/VsFRTO/YNdLTcHwetz6MKVe2/4ZyTOKsLpbwYgFa/I1NWT/mcRwc/2dQniEK8 +IEIA3QP/XDmNeqLAMKl72aKadibcLnuuZDxbNyKFXBbCcduqnVK6xCVYVfRwd0dx +H/KtwiXlhqHokzdXryE26jEq/haEDV0GwXso9rmZWwPAO7wLec2KDfygGFj2pWJg +44LSInrNKMdxxAANCbHMy2pwNJkQAcIVj4QlqsCSI1OXroPJJmA5p4kCbAQYAQgA +IBYhBOrEIpsqyE30VpWH0aBnSbpPNLDlBQJpieD/AhsuAUAJEKBnSbpPNLDlwHQg +BBkBCAAdFiEEAmTl3msGKezJa2F7ijYk4nulVAsFAmmJ4P8ACgkQijYk4nulVAte +6gf7Be1QehFVqh9EbQlCm3iyNZsqTe8WFsnAi+0xCU+N1/ea0X1M64dx+nj2ec1R +GNGRSKmNuuwvgNdqcFCo9FAkGRsIFNhSgBAu3gwAZlRXTdijE7V1oEOS7aYYEVQM +Vscjs+ywJHRDkPGju0ajD7Upt1uc+ZuCdTwzXv95amfjOIKgwoLjItnEmLVFIUBV +hsKRfGzHHuI6yHQcMZiW7ogLguKVdUBQq+ZkBKKC+o+xhLjQdrVl+AUkdPCI8hLQ +kzBSuv5/VUOjYwnsfjIPjrAJY/ZxH6I46tOfFSNOOApebDpUKbCb8Ozvl2OI/Y/o +idKR2YI3wiluEPyJAtfFg0P0Vj4AB/90Q1dIPHXryirzLUtsRXNN9zUlUYWlK8JQ +e7FQLV3lkbqCfs0fij335fy6Z4KVaAOPN/G9Hxh4uTLLBLLMAU7BARAnUH7rap8X +9FpGpg4HmmdG0F7emquun4P9UvDGg3qvGfWmQg9Xc5AdyUN/VHcBYVXh8mWPWnmv +/QHewpu7z9tCakhRjchc1Vka9lbozguzlXgntANxdo/ZVDxOMTk5q3nn+/6NiU6E +SFn2kXlUXMo0kVcU5LROYW10zjmv3oqpInN++FyWyQY31kaGk8Wrx9UKW134biZt +vDO1S0dhI+RhZ2Up8i2xFpym+h4Urku66ew2ha1fGYwirmQUaVfA +=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/dave.asc b/minifi_rust/extensions/minifi_pgp/test_keys/dave.asc new file mode 100644 index 0000000000..e413d1fba2 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/dave.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEarPhDRYJKwYBBAHaRw8BAQdAIutX9mnyeaFBjHNkOMPq9UsWaoZB3An5wfHn +pns7UJ+0F0RhdmUgPGRhdmVAZXhhbXBsZS5jb20+iK8EExYKAFcWIQRTkbiPC69l +G2IDzoApe2qIiH+2TwUCarPhDRsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMC +GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQKXtqiIh/tk8qLgEA7oKJ +95fgapuSqFLqDa2gAemjXHgeox4Jn2LayST3zrYA/ji0PvUzFxuFrgrl5eCGZfmr +FM2rPzkppJ5PAxCA/xQBuDgEarPhDhIKKwYBBAGXVQEFAQEHQBKa7zXTR3mYQ7et +L2oIYWHyRm4kbUmW3E4KGHfyOt4YAwEIB4iUBBgWCgA8FiEEU5G4jwuvZRtiA86A +KXtqiIh/tk8FAmqz4Q4bFIAAAAAABAAObWFudTIsMi41KzEuMTIsMCwzAhsMAAoJ +ECl7aoiIf7ZPnK0A/jt6JWNcf8f8GEQn57O8okYgmCgWFOBQCQ24JTiYfgCNAP9p +Mg/o4PePUr9kkvSNEdK0JKqYAohHzsKj0JszrmWhBg== +=D4Oc +-----END PGP PUBLIC KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/dave_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/dave_private.asc new file mode 100644 index 0000000000..1b99d2f082 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/dave_private.asc @@ -0,0 +1,18 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEarPhDRYJKwYBBAHaRw8BAQdAIutX9mnyeaFBjHNkOMPq9UsWaoZB3An5wfHn +pns7UJ/+BwMC+zyDHE4t43JgXPRBVQSAOrak9PzpECo7QXjcGrqRZBbf1ZTN94Cl +K20K/z2cc9IwBijYGN1/kgCKMVsU2WgRZ9trzOwFJLzS6+uEK2EVCbQXRGF2ZSA8 +ZGF2ZUBleGFtcGxlLmNvbT6IrwQTFgoAVxYhBFORuI8Lr2UbYgPOgCl7aoiIf7ZP +BQJqs+ENGxSAAAAAAAQADm1hbnUyLDIuNSsxLjEyLDAsMwIbAwULCQgHAgIiAgYV +CgkICwIEFgIDAQIeBwIXgAAKCRApe2qIiH+2TyouAQDugon3l+Bqm5KoUuoNraAB +6aNceB6jHgmfYtrJJPfOtgD+OLQ+9TMXG4WuCuXl4IZl+asUzas/OSmknk8DEID/ +FAGciwRqs+EOEgorBgEEAZdVAQUBAQdAEprvNdNHeZhDt60vaghhYfJGbiRtSZbc +TgoYd/I63hgDAQgH/gcDAggY5bjBZ8PLYLukHnvX1OQ6PoL1Phk7rlN9qjPCEX0c ++8Db+U6nJ9ckhjL/c9MsWb/quQLDN5WEyVQCmCiWJTX2gx+vpBTxXAD7+f6LozKI +lAQYFgoAPBYhBFORuI8Lr2UbYgPOgCl7aoiIf7ZPBQJqs+EOGxSAAAAAAAQADm1h +bnUyLDIuNSsxLjEyLDAsMwIbDAAKCRApe2qIiH+2T5ytAP47eiVjXH/H/BhEJ+ez +vKJGIJgoFhTgUAkNuCU4mH4AjQD/aTIP6OD3j1K/ZJL0jRHStCSqmAKIR87Co9Cb +M65loQY= +=EiF6 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/erin.asc b/minifi_rust/extensions/minifi_pgp/test_keys/erin.asc new file mode 100644 index 0000000000..21efeead3d --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/erin.asc @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEarPhDxYJKwYBBAHaRw8BAQdA4/aTB+10d9/3Ey4WHi5iXSlP9i9GWqE9xY8k +c/zzqva0F0VyaW4gPGVyaW5AZXhhbXBsZS5jb20+iK8EExYKAFcWIQTRATOyPqzK +Yn5F1i6MlkQUQKIvdAUCarPhDxsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMC +GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQjJZEFECiL3SlhAD/cCjS +ArPCcbLS8v5YUAJvw2M2/9fKSUsUA8zX/by17PUA/R50qJzXFWEbcG73cPda+PdZ +qiCywhY+HrzMJJ5DE7wC +=xbRI +-----END PGP PUBLIC KEY BLOCK----- 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..fb5c7cc62e --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc @@ -0,0 +1,58 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +wA== +=/AVq +-----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..9bb901d97a 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/mixed_secret_keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/mixed_secret_keyring.gpg new file mode 100644 index 0000000000..e7183c261a Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/mixed_secret_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..12a5227a09 --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc @@ -0,0 +1,99 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=RDvt +-----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..6a5c0f02ab 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/spoofed_bob.asc b/minifi_rust/extensions/minifi_pgp/test_keys/spoofed_bob.asc new file mode 100644 index 0000000000..ea03803a6d --- /dev/null +++ b/minifi_rust/extensions/minifi_pgp/test_keys/spoofed_bob.asc @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGqz4vkBCAC79KLH64ayyRJmSATxXWiaFBhYZIAcYz/AC4159XMMfbAhtaRr +UvCiLjmlPDuA1QZtpR139v8UyTxmlkp5xul+Vgjd7dKc1FRFSdJKZVni2RxnC0Cr +mD+zIzW2qS8kV4tcuxqE2mGVHlpUkivO41HNYnt2Ks5TNcochQMe/3gi+aHko6Pn +8VlInu4aQx7wnDJBaKNNMajpGJgL/zCgw0TgG0v/m4LIgHFE9RE+bXT2YUpMIYVh +o6vG82C0xy8uxCMcyHgbic57JsTgRF0TJlJiLn3eEjenjsjARqFEaniIAPvJ99JR +wVB1v0es8dHfBx00+4XPU67d71l2VoLFHJXHABEBAAG0KEJvYiBQZXJzb25hbCA8 +Ym9iQGhvbWUuaW8uYXR0YWNrZXIudGVzdD6JAW0EEwEIAFcWIQR41anBL61W0dnR +hWt1b8Lr7Lh0fAUCarPi+RsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMCGw0F +CwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQdW/C6+y4dHzgPAf/c9R0Oo7t +pVmfcE1buMa5jopGAugLYsGI4ChhjYILihPw3aCRMW+05wlVYJREAgXQmaigd99a +Sdt0bqLTIYcY7xKlQuYRc0lJilo/xmak2rm2EJzVM13zGH7GK0pEXr9Mt9vLjdpe +j0rbLIup9pZYYPtpYk0kz81T2GLqeSYnR7Wdm99Phdagrn3/RY8tzYt/+F6z8JTd +yF+E955OsLE20xY4dS5j5GShf4xvjNCWhcLfst8Q2ZAyPIGcbpBGP4ww43jFIvjO +q3wSh+VK3eSnB2gfy+sbwO24zL79udsPvT8a1NGUVjZidseYBvqAzs4m8e724l5F +EgkpqCOdeyN8hg== +=nqES +-----END PGP PUBLIC KEY BLOCK----- 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à´?`Ò9ƒŽ˜B«2;²åÂŒ´ƒ�«g]N)‹!r2¥!F–šžnl./ó‚’a˜9._ª}_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 52863ac6c2..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, 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.rs b/minifi_rust/minifi_native/src/api/processor.rs index 8ac4bfb12f..413524e29f 100644 --- a/minifi_rust/minifi_native/src/api/processor.rs +++ b/minifi_rust/minifi_native/src/api/processor.rs @@ -15,12 +15,13 @@ // specific language governing permissions and limitations // under the License. +use crate::GetControllerService; use crate::api::{RawProcessor, ThreadingModel}; use crate::{GetProperty, LogLevel, Logger, MinifiError, ProcessContext}; use std::marker::PhantomData; pub trait Schedule { - fn schedule( + fn schedule( context: &Ctx, logger: &L, ) -> Result 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 f300f99b83..1d1bb6765a 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; #[derive(Debug)] pub struct TransformStreamResult { @@ -72,7 +73,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 89f2c01987..220c8af4e8 100644 --- a/minifi_rust/minifi_rs_behave/Dockerfile.alpine +++ b/minifi_rust/minifi_rs_behave/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM rust:alpine3.22 AS chef +FROM rust:alpine3.24 AS chef RUN apk add --no-cache musl-dev gcc g++ clang-dev lld pkgconfig curl tar && cargo install cargo-chef WORKDIR /app