You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by sz...@apache.org on 2026/09/28 17:36:53 UTC
(nifi-minifi-cpp) branch main updated: MINIFICPP-2749 Add EncryptContentPGP and DecryptContentPGP
This is an automated email from the ASF dual-hosted git repository.
szaszm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 7148ad2a5 MINIFICPP-2749 Add EncryptContentPGP and DecryptContentPGP
7148ad2a5 is described below
commit 7148ad2a50d65af8de5790ec0c6cbc2f5a51774a
Author: Martin Zink <ma...@apache.org>
AuthorDate: Mon Sep 28 17:41:34 2026 +0200
MINIFICPP-2749 Add EncryptContentPGP and DecryptContentPGP
Closes #2225
Signed-off-by: Marton Szasz <sz...@apache.org>
---
.../ubuntu_22_04_clang_arm_manifest.json | 178 ++++++++
CONTROLLERS.md | 36 +-
PROCESSORS.md | 56 +++
README.md | 53 +--
cmake/MiNiFiOptions.cmake | 1 +
minifi_rust/CMakeLists.txt | 10 +
minifi_rust/Cargo.toml | 6 +
.../extensions/minifi_pgp/.cargo/config.toml | 5 +
minifi_rust/extensions/minifi_pgp/.gitignore | 7 +
minifi_rust/extensions/minifi_pgp/Cargo.toml | 14 +
.../minifi_pgp/features/encrypt_decrypt.feature | 67 +++
.../extensions/minifi_pgp/features/environment.py | 41 ++
.../extensions/minifi_pgp/features/steps/steps.py | 80 ++++
minifi_rust/extensions/minifi_pgp/minifi_pgp.md | 112 +++++
.../src/controller_services/encryption_key.rs | 131 ++++++
.../src/controller_services/key_file_property.rs | 118 +++++
.../src/controller_services/key_lookup.rs | 141 ++++++
.../src/controller_services/key_parsing.rs | 90 ++++
.../src/controller_services/key_property.rs | 49 +++
.../minifi_pgp/src/controller_services/mod.rs | 24 ++
.../src/controller_services/private_key_service.rs | 249 +++++++++++
.../src/controller_services/public_key_service.rs | 296 +++++++++++++
minifi_rust/extensions/minifi_pgp/src/lib.rs | 41 ++
.../minifi_pgp/src/processors/decrypt_content.rs | 474 +++++++++++++++++++++
.../minifi_pgp/src/processors/encrypt_content.rs | 392 +++++++++++++++++
.../extensions/minifi_pgp/src/processors/mod.rs | 19 +
.../extensions/minifi_pgp/src/test_utils/mod.rs | 32 ++
minifi_rust/extensions/minifi_pgp/src/utils.rs | 80 ++++
.../extensions/minifi_pgp/test_keys/README.txt | 59 +++
.../extensions/minifi_pgp/test_keys/alice.asc | 18 +
.../extensions/minifi_pgp/test_keys/alice.gpg | Bin 0 -> 640 bytes
.../minifi_pgp/test_keys/alice_private.asc | 32 ++
.../minifi_pgp/test_keys/alice_private.gpg | Bin 0 -> 1337 bytes
.../minifi_pgp/test_keys/ambiguous_keyring.gpg | Bin 0 -> 3227 bytes
.../minifi_pgp/test_keys/bob_private.asc | 71 +++
.../minifi_pgp/test_keys/bob_private.gpg | Bin 0 -> 3207 bytes
.../extensions/minifi_pgp/test_keys/dave.asc | 14 +
.../minifi_pgp/test_keys/dave_private.asc | 18 +
.../extensions/minifi_pgp/test_keys/erin.asc | 10 +
.../extensions/minifi_pgp/test_keys/garbage.gpg | Bin 0 -> 1024 bytes
.../extensions/minifi_pgp/test_keys/keyring.asc | 58 +++
.../extensions/minifi_pgp/test_keys/keyring.gpg | Bin 0 -> 2545 bytes
.../minifi_pgp/test_keys/mixed_secret_keyring.gpg | Bin 0 -> 1966 bytes
.../minifi_pgp/test_keys/secret_keyring.asc | 99 +++++
.../minifi_pgp/test_keys/secret_keyring.gpg | Bin 0 -> 4544 bytes
.../minifi_pgp/test_keys/spoofed_bob.asc | 19 +
.../extensions/minifi_pgp/test_keys/truncated.asc | 10 +
.../minifi_pgp/test_keys/truncated_private.asc | 17 +
.../minifi_pgp/test_messages/foo_for_alice.asc | 12 +
.../minifi_pgp/test_messages/foo_for_alice.gpg | Bin 0 -> 346 bytes
.../test_messages/password_encrypted_foo.asc | 6 +
.../test_messages/password_encrypted_foo.gpg | 1 +
.../minifi_rs_playground/minifi_rs_playground.md | 1 +
.../src/processors/asciify_german/tests.rs | 7 +-
.../src/processors/put_file/unix_permissions.rs | 11 +-
minifi_rust/minifi_native/src/api/processor.rs | 3 +-
.../flow_file_stream_transform.rs | 6 +-
minifi_rust/minifi_native/src/lib.rs | 2 +
minifi_rust/minifi_native/src/test_utils.rs | 16 +
minifi_rust/minifi_rs_behave/Dockerfile.alpine | 2 +-
60 files changed, 3253 insertions(+), 41 deletions(-)
diff --git a/.github/references/ubuntu_22_04_clang_arm_manifest.json b/.github/references/ubuntu_22_04_clang_arm_manifest.json
index 139b3854d..efc67a971 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 28fce43e1..a888c22a5 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<br/>**Sensitive Property: true** |
+| Keyring | | | Secret Key encoded in ASCII Armor<br/>**Sensitive Property: true** |
+| Keyring File | | | File path to PGP Secret Key encoded in binary or ASCII Armor<br/>**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<br/>**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<br/>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 4c7705b2a..a2753b42f 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<br/>**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<br/>BINARY | File Encoding for encryption |
+| Passphrase | | | Passphrase used for encrypting data with Password-Based Encryption<br/>**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<br/>**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 a26a995c5..b7a1b959e 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 [...]
-|----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
-| Archive Extensions | [ApplyTemplate](PROCESSORS.md#applytemplate)<br/>[CompressContent](PROCESSORS.md#compresscontent)<br/>[ManipulateArchive](PROCESSORS.md#manipulatearchive)<br/>[MergeContent](PROCESSORS.md#mergecontent)<br/>[FocusArchiveEntry](PROCESSORS.md#focusarchiveentry)<br/>[UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry) [...]
-| AWS | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice)<br/>[PutS3Object](PROCESSORS.md#puts3object)<br/>[DeleteS3Object](PROCESSORS.md#deletes3object)<br/>[FetchS3Object](PROCESSORS.md#fetchs3object)<br/>[ListS3](PROCESSORS.md#lists3)<br/>[PutKinesisStream](PROCESSORS.md#putkinesisstream) [...]
-| Azure | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice)<br/>[PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage)<br/>[DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage)<br/>[FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage)<br/>[ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage)<br/>[PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage)<br/>[DeleteAzureDataLakeStorage](PROCESSORS.md#del [...]
-| CivetWeb | [ListenHTTP](PROCESSORS.md#listenhttp) [...]
-| Couchbase | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice)<br/>[PutCouchbaseKey](PROCESSORS.md#putcouchbasekey)<br/>[GetCouchbaseKey](PROCESSORS.md#getcouchbasekey) [...]
-| Elasticsearch | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice)<br/>[PostElasticsearch](PROCESSORS.md#postelasticsearch) [...]
-| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess) [...]
-| Google Cloud Platform | [DeleteGCSObject](PROCESSORS.md#deletegcsobject)<br>[FetchGCSObject](PROCESSORS.md#fetchgcsobject)<br>[GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice)<br>[ListGCSBucket](PROCESSORS.md#listgcsbucket)<br>[PutGCSObject](PROCESSORS.md#putgcsobject) [...]
-| Grafana Loki | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest)<br>[PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc) [...]
-| Kafka | [PublishKafka](PROCESSORS.md#publishkafka)<br>[ConsumeKafka](PROCESSORS.md#consumekafka) [...]
-| Kubernetes (Linux) | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice) [...]
-| LlamaCpp | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference) [...]
-| Lua Scripting | [ExecuteScript](PROCESSORS.md#executescript) [...]
-| MQTT | [ConsumeMQTT](PROCESSORS.md#consumemqtt)<br/>[PublishMQTT](PROCESSORS.md#publishmqtt) [...]
-| OPC | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor)<br/>[PutOPCProcessor](PROCESSORS.md#putopcprocessor) [...]
-| PDH (Windows) | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor) [...]
-| ProcFs (Linux) | [ProcFsMonitor](PROCESSORS.md#procfsmonitor) [...]
-| Python Scripting | [ExecuteScript](PROCESSORS.md#executescript)<br/>[**Custom Python Processors**](extensions/python/PYTHON.md) [...]
-| SMB (Windows) | [FetchSmb](PROCESSORS.md#fetchsmb)<br/>[ListSmb](PROCESSORS.md#listsmb)<br/>[PutSmb](PROCESSORS.md#putsmb) [...]
-| SFTP | [FetchSFTP](PROCESSORS.md#fetchsftp)<br/>[ListSFTP](PROCESSORS.md#listsftp)<br/>[PutSFTP](PROCESSORS.md#putsftp) [...]
-| SQL | [ExecuteSQL](PROCESSORS.md#executesql)<br/>[PutSQL](PROCESSORS.md#putsql)<br/>[QueryDatabaseTable](PROCESSORS.md#querydatabasetable)<br/> [...]
-| Splunk | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp)<br/>[QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus) [...]
-| Systemd (Linux) | [ConsumeJournald](PROCESSORS.md#consumejournald) [...]
-| Windows Event Log (Windows) | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog)<br/>[TailEventLog](PROCESSORS.md#taileventlog) [...]
+| Extension Set | Processors and Controller Services [...]
+|----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
+| Archive Extensions | [ApplyTemplate](PROCESSORS.md#applytemplate)<br/>[CompressContent](PROCESSORS.md#compresscontent)<br/>[ManipulateArchive](PROCESSORS.md#manipulatearchive)<br/>[MergeContent](PROCESSORS.md#mergecontent)<br/>[FocusArchiveEntry](PROCESSORS.md#focusarchiveentry)<br/>[UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry) [...]
+| AWS | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice)<br/>[PutS3Object](PROCESSORS.md#puts3object)<br/>[DeleteS3Object](PROCESSORS.md#deletes3object)<br/>[FetchS3Object](PROCESSORS.md#fetchs3object)<br/>[ListS3](PROCESSORS.md#lists3)<br/>[PutKinesisStream](PROCESSORS.md#putkinesisstream) [...]
+| Azure | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice)<br/>[PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage)<br/>[DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage)<br/>[FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage)<br/>[ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage)<br/>[PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage)<br/>[DeleteAzureDataLakeStorage](PROCESSORS.md#del [...]
+| CivetWeb | [ListenHTTP](PROCESSORS.md#listenhttp) [...]
+| Couchbase | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice)<br/>[PutCouchbaseKey](PROCESSORS.md#putcouchbasekey)<br/>[GetCouchbaseKey](PROCESSORS.md#getcouchbasekey) [...]
+| Elasticsearch | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice)<br/>[PostElasticsearch](PROCESSORS.md#postelasticsearch) [...]
+| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess) [...]
+| Google Cloud Platform | [DeleteGCSObject](PROCESSORS.md#deletegcsobject)<br>[FetchGCSObject](PROCESSORS.md#fetchgcsobject)<br>[GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice)<br>[ListGCSBucket](PROCESSORS.md#listgcsbucket)<br>[PutGCSObject](PROCESSORS.md#putgcsobject) [...]
+| Grafana Loki | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest)<br>[PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc) [...]
+| Kafka | [PublishKafka](PROCESSORS.md#publishkafka)<br>[ConsumeKafka](PROCESSORS.md#consumekafka) [...]
+| Kubernetes (Linux) | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice) [...]
+| LlamaCpp | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference) [...]
+| Lua Scripting | [ExecuteScript](PROCESSORS.md#executescript) [...]
+| MQTT | [ConsumeMQTT](PROCESSORS.md#consumemqtt)<br/>[PublishMQTT](PROCESSORS.md#publishmqtt) [...]
+| OPC | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor)<br/>[PutOPCProcessor](PROCESSORS.md#putopcprocessor) [...]
+| PDH (Windows) | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor) [...]
+| PGP | [EncryptContentPGP](PROCESSORS.md#encryptcontentpgp)<br/>[DecryptContentPGP](PROCESSORS.md#decryptcontentpgp)<br/>[PGPPublicKeyService](CONTROLLERS.md#pgppublickeyservice)<br/>[PGPPrivateKeyService](CONTROLLERS.md#pgpprivatekeyservice) [...]
+| ProcFs (Linux) | [ProcFsMonitor](PROCESSORS.md#procfsmonitor) [...]
+| Python Scripting | [ExecuteScript](PROCESSORS.md#executescript)<br/>[**Custom Python Processors**](extensions/python/PYTHON.md) [...]
+| SMB (Windows) | [FetchSmb](PROCESSORS.md#fetchsmb)<br/>[ListSmb](PROCESSORS.md#listsmb)<br/>[PutSmb](PROCESSORS.md#putsmb) [...]
+| SFTP | [FetchSFTP](PROCESSORS.md#fetchsftp)<br/>[ListSFTP](PROCESSORS.md#listsftp)<br/>[PutSFTP](PROCESSORS.md#putsftp) [...]
+| SQL | [ExecuteSQL](PROCESSORS.md#executesql)<br/>[PutSQL](PROCESSORS.md#putsql)<br/>[QueryDatabaseTable](PROCESSORS.md#querydatabasetable)<br/> [...]
+| Splunk | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp)<br/>[QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus) [...]
+| Systemd (Linux) | [ConsumeJournald](PROCESSORS.md#consumejournald) [...]
+| Windows Event Log (Windows) | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog)<br/>[TailEventLog](PROCESSORS.md#taileventlog) [...]
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 7a898a765..dcd3a2b26 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 63b8a407e..a9f604011 100644
--- a/minifi_rust/CMakeLists.txt
+++ b/minifi_rust/CMakeLists.txt
@@ -39,8 +39,18 @@ 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)
+# Build the test binaries during the build
+add_custom_target(cargo_build_tests ALL
+ COMMAND cargo test --no-run
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ COMMENT "Building Rust test binaries")
+
add_test(
NAME cargo_tests
COMMAND cargo test
diff --git a/minifi_rust/Cargo.toml b/minifi_rust/Cargo.toml
index 6dc306f11..8bde74c44 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 000000000..cb8c02ddc
--- /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 000000000..f9f6d205f
--- /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 000000000..6edd4d3b7
--- /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 000000000..202a01307
--- /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 000000000..4652c748a
--- /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 000000000..fd5699553
--- /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 000000000..e72bc0b3f
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md
@@ -0,0 +1,112 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+## 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<br/>**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<br/>BINARY | File Encoding for encryption |
+| Passphrase | | | Passphrase used for encrypting data with Password-Based Encryption<br/>**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<br/>**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<br/>**Sensitive Property: true** |
+| Keyring | | | Secret Key encoded in ASCII Armor<br/>**Sensitive Property: true** |
+| Keyring File | | | File path to PGP Secret Key encoded in binary or ASCII Armor<br/>**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<br/>**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 000000000..173f7d436
--- /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<EncryptionTarget<'_>, 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 000000000..d367d404f
--- /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<PropertyConstraints> = None;
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for SecretKeyFile {
+ type Output = Vec<SignedSecretKey>;
+
+ fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+ parse_key_file(s, KeyKind::Secret)
+ }
+}
+
+pub(crate) struct PublicKeyFile {}
+impl PropertySchema for PublicKeyFile {
+ const CONSTRAINT: Option<PropertyConstraints> = None;
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for PublicKeyFile {
+ type Output = Vec<SignedPublicKey>;
+
+ fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+ 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 000000000..26481bac9
--- /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<String> = 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 000000000..70acdcb27
--- /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<T: Deserializable>(
+ input: &str,
+ kind: KeyKind,
+) -> Result<Vec<T>, MinifiError> {
+ let mut keys: Vec<T> = 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<T: Deserializable>(
+ path: &str,
+ kind: KeyKind,
+) -> Result<Vec<T>, MinifiError> {
+ let mut keys: Vec<T> = 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<T, File, Inline, Ctx>(
+ context: &Ctx,
+ file_property: &Property<Option<File>>,
+ inline_property: &Property<Option<Inline>>,
+) -> Result<Vec<T>, MinifiError>
+where
+ Ctx: GetProperty,
+ File: PropertyType<Output = Vec<T>>,
+ Inline: PropertyType<Output = Vec<T>>,
+{
+ 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<T>(keys: Vec<T>, kind: KeyKind) -> Result<Vec<T>, 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 000000000..fc5c629a2
--- /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<PropertyConstraints> = None;
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for SecretKey {
+ type Output = Vec<SignedSecretKey>;
+
+ fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+ parse_armored_keys(s, KeyKind::Secret)
+ }
+}
+
+pub(crate) struct PublicKey {}
+impl PropertySchema for PublicKey {
+ const CONSTRAINT: Option<PropertyConstraints> = None;
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for PublicKey {
+ type Output = Vec<SignedPublicKey>;
+
+ fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+ 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 000000000..3aa314bc0
--- /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 000000000..56171434e
--- /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<SignedSecretKey>,
+ passphrases: Vec<pgp::types::Password>,
+}
+
+impl EnableControllerService for PGPPrivateKeyService {
+ fn enable<P: GetProperty, L: Logger>(context: &P, _logger: &L) -> Result<Self, MinifiError>
+ 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<Option<SecretKeyFile>> = Property::new(
+ "Keyring File",
+ "File path to PGP Secret Key encoded in binary or ASCII Armor",
+ )
+ .supports_expression_language();
+
+ pub(super) const KEY: Property<Option<SecretKey>> =
+ Property::new("Keyring", "Secret Key encoded in ASCII Armor").sensitive();
+
+ pub(super) const KEY_PASSWORD: Property<Option<utils::Passwords>> = 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<Self>] = &[];
+ }
+}
+
+#[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 <al...@example.com>").is_ok());
+
+ assert!(service.get_secret_key("<Alice>").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 000000000..1fc0987c5
--- /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<SignedPublicKey>,
+}
+
+impl EnableControllerService for PGPPublicKeyService {
+ fn enable<P: GetProperty, L: Logger>(context: &P, _logger: &L) -> Result<Self, MinifiError>
+ 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<Option<PublicKeyFile>> = 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<Option<PublicKey>> = 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<Self>] = &[];
+ }
+}
+
+#[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 <al...@example.com>").is_ok());
+
+ assert!(service.get("<Alice>").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 000000000..eba818524
--- /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 000000000..f80101410
--- /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<pgp::types::Password>,
+}
+
+impl Schedule for DecryptContentPGP {
+ fn schedule<P: GetProperty + GetControllerService, L>(
+ context: &P,
+ _logger: &L,
+ ) -> Result<Self, MinifiError>
+ 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<Message<'a>> {
+ 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<Ctx: GetProperty + GetControllerService, LoggerImpl: Logger>(
+ &self,
+ context: &Ctx,
+ input_stream: &mut dyn InputStream,
+ output_stream: &mut dyn OutputStream,
+ _logger: &LoggerImpl,
+ ) -> Result<TransformStreamResult, ProcessError> {
+ 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<Option<utils::Password>> = Property::new(
+ "Passphrase",
+ "Passphrase used for decrypting data encrypted with Password-Based Encryption",
+ )
+ .sensitive();
+
+ pub(super) const PRIVATE_KEY_SERVICE: Property<Option<PGPPrivateKeyService>> = 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<PrivateKeyData>,
+ 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<u8> = 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<u8> {
+ 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<u8> = 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<u8>) -> Vec<u8> {
+ 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<u8> = 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<u8> = 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<u8> = 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 000000000..14b6ce3ca
--- /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<Password>,
+}
+
+#[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<P: GetProperty + GetControllerService, L: Logger>(
+ context: &P,
+ _logger: &L,
+ ) -> Result<Self, MinifiError>
+ 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<Ctx: GetProperty + GetControllerService>(
+ context: &Ctx,
+ ) -> Result<Option<&SignedPublicKey>, 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<TransformStreamResult, ProcessError> {
+ 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<FileEncoding> =
+ Property::new("File Encoding", "File Encoding for encryption")
+ .with_default(FileEncoding::Binary.into_str());
+
+ pub(crate) const SYMMETRIC_PASSPHRASE: Property<Option<utils::Password>> = Property::new(
+ "Passphrase",
+ "Passphrase used for encrypting data with Password-Based Encryption",
+ )
+ .sensitive();
+
+ pub(crate) const PUBLIC_KEY_SEARCH: Property<Option<String>> = 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<Option<PGPPublicKeyService>> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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 000000000..a22c7b1b8
--- /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 000000000..1ceb14cab
--- /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<u8> {
+ 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 000000000..7528e06b3
--- /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<PropertyConstraints> = Some(PropertyConstraints::Validator(
+ StandardPropertyValidator::NonBlankValidator,
+ ));
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for Password {
+ type Output = pgp::types::Password;
+
+ fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+ 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<PropertyConstraints> = Some(PropertyConstraints::Validator(
+ StandardPropertyValidator::NonBlankValidator,
+ ));
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for Passwords {
+ type Output = Vec<pgp::types::Password>;
+
+ fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+ 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 000000000..86984f128
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/README.txt
@@ -0,0 +1,59 @@
+Testing keys v4
+------------------------
+uid [ultimate] Alice <al...@example.com>
+keyid BCCE3FDFBA019D7E
+passphrase whiterabbit
+RSA, the primary key carries the encrypt capability itself, no subkeys
+
+uid [ultimate] Bob Personal <bo...@home.io>
+uid [ultimate] Bob Primary <bo...@work.com>
+keyid A06749BA4F34B0E5
+no passphrase
+
+uid [ultimate] Dave <da...@example.com>
+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 <da...@example.com>' ed25519 sign never
+ gpg --quick-add-key <fingerprint> cv25519 encr never
+
+uid [ultimate] Erin <er...@example.com>
+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 <er...@example.com>' ed25519 sign never
+
+uid [ultimate] Bob Personal <bo...@home.io.attacker.test>
+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 000000000..ebe21a684
--- /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 000000000..8ef375e68
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 000000000..6b7ca4d44
--- /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 000000000..c0cfc2c52
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 000000000..c0e9da5ac
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 000000000..09fc1bc99
--- /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 000000000..a39370243
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 000000000..e413d1fba
--- /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 000000000..1b99d2f08
--- /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 000000000..21efeead3
--- /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 000000000..9e603b5c3
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 000000000..fb5c7cc62
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc
@@ -0,0 +1,58 @@
+-----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/yP03Dpg0TubJkBDQRpieD/AQgA1+zRRj2kAoz/mC9qO+AgjR7fg8dA
+JL41KephBG/gobXvP/NkXbR/VZcQMa1C+etP1M7BwEGBjM2wxc59ONRnlnBJTK/5
++4h98EEg5cVlxPoEofr6bRymras2AoUkPJe7unJmd1ro+mbXFa9Ja8meceY2tpTy
+6CtgZygqAysETiS5g0fEmEtg8B/5+lFP8qsqpRa3Q2mUB8iRdus9Na8e5wrXyLlk
+qeWUuBa/an+XmZxcLGO0Ta/F++IgG1UZK9rje4874o6B9qluoq6h5vWwDZnJnUFy
+RNH0hVOv6yYrsIPCHcwnGvHyWSM8SuV2jAOu60R11n+foRRFsjePaFdlGQARAQAB
+tBpCb2IgUHJpbWFyeSA8Ym9iQHdvcmsuY29tPokBUgQTAQgAPBYhBOrEIpsqyE30
+VpWH0aBnSbpPNLDlBQJpieD/AhsvBQsJCAcCAyICAQYVCgkICwIEFgIDAQIeBwIX
+gAAKCRCgZ0m6TzSw5RDyB/9hef3HQs9OReCNlFFZDkMSgTLsHj6mMk4v545F79VX
+8ivv5h4Fbw2N6mlZawiJ8ZzWF5diNdXC1u7NJCmvSbGTQFjeY6DDy/nnZMX97NV+
+nvlT3fQvG6MFkHKg7KBB6xRsBeBXH1Z2qJeSyMU3WXlUEo4y48kqhr3SxpEFuAlX
+gGcki0IrNeQOGZm92HUvHXaUz8z1K3N4VL2/OE446bVxznYwxgxCW52kq/0+2K7k
+rOkzVJoqhm2UO7VfKD9U6AFfsTVPXUDiHgooLtMwghjA3vCe4Fo376cd9VkLOF2y
+k7sV00ywHSdNbdUia0M0FfgZ9mduizOda87G9d/giasStBpCb2IgUGVyc29uYWwg
+PGJvYkBob21lLmlvPokBUgQTAQgAPBYhBOrEIpsqyE30VpWH0aBnSbpPNLDlBQJp
+ieEHAhsvBQsJCAcCAyICAQYVCgkICwIEFgIDAQIeBwIXgAAKCRCgZ0m6TzSw5Uc9
+B/9ffQYu7DYt+GwoU3z7LoyendEhCs3YyQf2hm5miMEhS4DoaAfogup+sHOqKsdJ
+TFAG89ixk8MRaPxTCZ5KiZ601jyR6Cygcrl8tTIjsfRzus33OG6OzASl25Tkvw2c
++H5SekHGVrmnmvfvJwWrMWyh9Au4Tw97Utwg7BTzSgw1QDnxmVBjk9nzqA3xEVjH
+7BjkQd7mVU+72uCGdqyInmAKeLFNd1l3U3ZwyqwpEo+6sxCoANDsA5afdq3t/nS/
+T+3SO2x7aBKfG38TUu1hzRt4gZE8c2XSUlABBb+hQIUfHSDCNl3UPZhUsZoVAm7W
+I+XOb3TAM2ntyWuHfLWkG74EuQENBGmJ4P8BCACbC8WmhEcI0WHmLJqpKFcwn3YZ
+ewJTdReuYxFdwaXrft/86qD1Wg5AftKDhbPxDdWnN1bVNeHLNCrZcsUySKzfKqDj
+sOA9t4Fdqbd4xS9dUH+cxxUChzaTKIYTYNCYFeLpl51cCVXC3Mcg7Zru4I4pm1gJ
+FoGHKKiBbAE48uhJ9FcLYB7LdvhW5AHG15Y4Tn8x8uTZbW+HKJXvsRYGjRR0dp2E
+gqSLQuAa+zNc+mNthfgv7g7OL3LmOikpQT3+dyuYBPmD/8YWg36oOkPGLRnrkCt/
+mvV9QXcu87gBwT0EEfZuEjm3glyI1QFludE1B6ehTCxbqf/guY4JvXaiMc1/ABEB
+AAGJAmwEGAEIACAWIQTqxCKbKshN9FaVh9GgZ0m6TzSw5QUCaYng/wIbLgFACRCg
+Z0m6TzSw5cB0IAQZAQgAHRYhBAJk5d5rBinsyWthe4o2JOJ7pVQLBQJpieD/AAoJ
+EIo2JOJ7pVQLXuoH+wXtUHoRVaofRG0JQpt4sjWbKk3vFhbJwIvtMQlPjdf3mtF9
+TOuHcfp49nnNURjRkUipjbrsL4DXanBQqPRQJBkbCBTYUoAQLt4MAGZUV03YoxO1
+daBDku2mGBFUDFbHI7PssCR0Q5Dxo7tGow+1KbdbnPmbgnU8M17/eWpn4ziCoMKC
+4yLZxJi1RSFAVYbCkXxsxx7iOsh0HDGYlu6IC4LilXVAUKvmZASigvqPsYS40Ha1
+ZfgFJHTwiPIS0JMwUrr+f1VDo2MJ7H4yD46wCWP2cR+iOOrTnxUjTjgKXmw6VCmw
+m/Ds75djiP2P6InSkdmCN8IpbhD8iQLXxYND9FY+AAf/dENXSDx168oq8y1LbEVz
+Tfc1JVGFpSvCUHuxUC1d5ZG6gn7NH4o99+X8umeClWgDjzfxvR8YeLkyywSyzAFO
+wQEQJ1B+62qfF/RaRqYOB5pnRtBe3pqrrp+D/VLwxoN6rxn1pkIPV3OQHclDf1R3
+AWFV4fJlj1p5r/0B3sKbu8/bQmpIUY3IXNVZGvZW6M4Ls5V4J7QDcXaP2VQ8TjE5
+Oat55/v+jYlOhEhZ9pF5VFzKNJFXFOS0TmFtdM45r96KqSJzfvhclskGN9ZGhpPF
+q8fVCltd+G4mbbwztUtHYSPkYWdlKfItsRacpvoeFK5LuunsNoWtXxmMIq5kFGlX
+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 000000000..9bb901d97
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 000000000..e7183c261
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 000000000..12a5227a0
--- /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 000000000..6a5c0f02a
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 000000000..ea03803a6
--- /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 000000000..d25ddfb62
--- /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 000000000..9cc58539a
--- /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 000000000..94b8561c9
--- /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 000000000..bd3d9b476
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 000000000..22d639e71
--- /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 000000000..40ab6a354
--- /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 e05adf0e0..74ecd008f 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<br/>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 52863ac6c..619d40d3d 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<u8> = 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/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs b/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs
index 8ded233d4..68a54a7e2 100644
--- a/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs
+++ b/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs
@@ -15,9 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+#[cfg(unix)]
use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType};
-use std::path::Path;
-
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
@@ -48,14 +47,14 @@ pub(super) struct PutFileUnixPermissions {
#[cfg(unix)]
impl PutFileUnixPermissions {
- pub(crate) fn set_directory_permissions(&self, path: &Path) -> std::io::Result<()> {
+ pub(crate) fn set_directory_permissions(&self, path: &std::path::Path) -> std::io::Result<()> {
if let Some(permissions) = self.directory_permissions.clone() {
return std::fs::set_permissions(path, permissions);
}
Ok(())
}
- pub(crate) fn set_file_permissions(&self, file: &Path) -> std::io::Result<()> {
+ pub(crate) fn set_file_permissions(&self, file: &std::path::Path) -> std::io::Result<()> {
if let Some(permissions) = self.file_permissions.clone() {
return std::fs::set_permissions(file, permissions);
}
@@ -69,11 +68,11 @@ pub(crate) struct PutFileUnixPermissions {}
#[cfg(windows)]
impl PutFileUnixPermissions {
- pub(crate) fn set_directory_permissions(&self, _path: &Path) -> std::io::Result<()> {
+ pub(crate) fn set_directory_permissions(&self, _path: &std::path::Path) -> std::io::Result<()> {
Ok(())
}
- pub(crate) fn set_file_permissions(&self, _file: &Path) -> std::io::Result<()> {
+ pub(crate) fn set_file_permissions(&self, _file: &std::path::Path) -> std::io::Result<()> {
Ok(())
}
}
diff --git a/minifi_rust/minifi_native/src/api/processor.rs b/minifi_rust/minifi_native/src/api/processor.rs
index 8ac4bfb12..413524e29 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<Ctx: GetProperty, L: Logger>(
+ fn schedule<Ctx: GetProperty + GetControllerService, L: Logger>(
context: &Ctx,
logger: &L,
) -> Result<Self, MinifiError>
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 f300f99b8..1d1bb6765 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<Ctx: GetProperty + GetControllerService + GetAttribute, LoggerImpl: Logger>(
+ 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 b855291a2..4ae26f9c1 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 000000000..089860498
--- /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<TransformStreamResult, ProcessError>,
+ 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 89f2c0198..220c8af4e 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