This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 82cefaff3d Fixes #8659 : Choose the PGP signing key per row (#8661)
82cefaff3d is described below
commit 82cefaff3d499b9d5009f1eefde6057176d95e29
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Oct 1 20:40:54 2026 +0200
Fixes #8659 : Choose the PGP signing key per row (#8661)
* Issue #8659 : Choose the PGP signing key per row
Add a Signing key column to the PGP encrypt files action, passed to GnuPG
as -u, so Sign and Sign and Encrypt rows can name the key that signs instead of
taking whatever default-key gpg.conf holds.
Sign rows passed the User ID as -r, which GnuPG accepts and ignores for
anything but encryption, so the key named there never had any effect. It is now
left unused and logged, rather than promoted to the signing key: an existing
workflow keeps signing with the same key it always did.
Covered by unit tests on the argument list, end-to-end tests against a real
gpg with two keys in the keyring, and integration test main-0009, which asserts
both the new choice and the unchanged behaviour of rows that name no signing
key.
* Issue #8659 : Address review comments
---
.../pages/workflow/actions/pgpencryptfiles.adoc | 5 +-
.../pgp/main-0009-pgp-choose-the-signing-key.hwf | 446 +++++++++++++++++++++
.../pgpencryptfiles/ActionPGPEncryptFiles.java | 49 ++-
.../ActionPGPEncryptFilesDialog.java | 16 +-
.../hop/workflow/actions/pgpencryptfiles/GPG.java | 121 +++++-
.../messages/messages_en_US.properties | 7 +-
.../ActionPGPEncryptFilesDialogTest.java | 16 +-
.../ActionPGPEncryptFilesSignTest.java | 167 +++++++-
.../pgpencryptfiles/ActionPGPEncryptFilesTest.java | 5 +
.../pgpencryptfiles/GpgArgumentPassingTest.java | 74 +++-
.../test/resources/action-pgp-encrypt-files.xml | 2 +
11 files changed, 857 insertions(+), 51 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
index 0d6ac8479b..876e710fa6 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
@@ -37,12 +37,13 @@ GnuPG package must be installed in the runtime environment
and encryption keys a
|GPG location|The file path of the GnuPG executable (e.g. `/usr/bin/gpg`).
|Use ASCII mode|Whether or not to use ASCII mode when encrypting.
|Include subfolders|Whether or not to include subfolders.
-|Copy previous results to args|
+|Copy previous results to args|Take the rows below from the result of the
previous action instead of from the grid. The fields are read by position:
action, file/folder source, wildcard, user ID, file/folder destination, and
optionally signing key as a sixth field.
|Action|Encrypt, Sign, Sign and Encrypt
|File/Folder source|The file to encrypt, can be added to the Files/Folders
list using Add.
|File/Folder destination|The destination of the encrypted files.
|Wildcard|A regex wildcard.
-// |User ID|User ID of the key in the runtime user keystore
+|User ID|User ID of the key to encrypt to, passed to GnuPG as `-r`. Required
for Encrypt and Sign and Encrypt, and not used by Sign.
+|Signing key|User ID of the key to sign with, passed to GnuPG as `-u`. Used by
Sign and Sign and Encrypt; leave it empty to sign with the default key from the
GnuPG configuration.
|Files/Folders|The files to encrypt.
|===
diff --git a/integration-tests/pgp/main-0009-pgp-choose-the-signing-key.hwf
b/integration-tests/pgp/main-0009-pgp-choose-the-signing-key.hwf
new file mode 100644
index 0000000000..913e4f8444
--- /dev/null
+++ b/integration-tests/pgp/main-0009-pgp-choose-the-signing-key.hwf
@@ -0,0 +1,446 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0009-pgp-choose-the-signing-key</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Signing key per row (gpg -u), and the backwards compatibility
of rows that have none. See
https://github.com/apache/hop/issues/8659</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/09/01 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/09/01 00:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>80</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>create a keyring with two keys</name>
+ <description/>
+ <type>SHELL</type>
+ <attributes/>
+ <filename/>
+ <work_directory>${PROJECT_HOME}</work_directory>
+ <arg_from_previous>N</arg_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <set_append_logfile>N</set_append_logfile>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <insertScript>Y</insertScript>
+ <script>#!/bin/bash
+set -e
+
+# Two usable secret keys, so "which key signed this" has more than one answer
and gpg's own
+# default can only be one of them. A throwaway keyring under output/, so a
test run never
+# touches the keyring of the machine or container it happens to run on.
+GNUPG_HOME=${PROJECT_HOME}/output/gnupg-two-keys
+WRAPPER=${PROJECT_HOME}/output/gpg-two-keys.sh
+
+GPG_BIN=$(command -v gpg || true)
+if [ -z "$GPG_BIN" ]; then
+ echo "gpg is not installed; the PGP actions cannot run without it"
+ exit 1
+fi
+
+# gpg starts an agent bound to this home's socket and leaves it running.
Deleting the home
+# under a live agent, then racing gpg's five-second auto-launch of a fresh
one, makes key
+# generation fail now and then with "No agent running" or "Broken pipe". Stop
the previous
+# test's agent first.
+if [ -d "$GNUPG_HOME" ]; then
+ gpgconf --homedir "$GNUPG_HOME" --kill all >/dev/null 2>&1 || true
+fi
+rm -rf "$GNUPG_HOME"
+mkdir -p "$GNUPG_HOME"
+chmod 700 "$GNUPG_HOME"
+
+# gpg is started by the Hop JVM and inherits its environment, and a Java
process cannot change
+# its own, so GNUPGHOME can never reach it from here. Pin the keyring in a
wrapper instead and
+# point the PGP actions at that.
+printf '#!/bin/sh\nexec "%s" --homedir "%s" "$@"\n' "$GPG_BIN" "$GNUPG_HOME"
> "$WRAPPER"
+chmod 700 "$WRAPPER"
+
+# Start the agent for this home ourselves, and wait for it, before asking it
for keys.
+gpg-connect-agent --homedir "$GNUPG_HOME" /bye >/dev/null 2>&1 ||
true
+
+# The keys carry no passphrase: the PGP encrypt action has no passphrase
field, so signing can
+# only ever use a key gpg is able to unlock on its own.
+for USER_ID in "Hop IT Default <[email protected]>" "Hop IT
Signer <[email protected]>"; do
+ "$GPG_BIN" --homedir "$GNUPG_HOME" --batch --yes --pinentry-mode loopback
--passphrase '' \
+ --quiet --quick-generate-key "$USER_ID" default default never 2>&1
+done
+
+printf 'id,amount\n1,100\n2,200\n' >
${PROJECT_HOME}/output/signing-key-source.csv
+rm -f ${PROJECT_HOME}/output/signed-by-chosen-key.asc \
+ ${PROJECT_HOME}/output/signed-with-user-id-only.asc \
+ ${PROJECT_HOME}/output/sealed-and-signed.asc
+</script>
+ <loglevel>Basic</loglevel>
+ <parallel>N</parallel>
+ <xloc>240</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>sign with the chosen key</name>
+ <description>Signing key filled in: gpg must sign with that key and not
with its default.</description>
+ <type>PGP_ENCRYPT_FILES</type>
+ <attributes/>
+ <gpglocation>${PROJECT_HOME}/output/gpg-two-keys.sh</gpglocation>
+ <arg_from_previous>N</arg_from_previous>
+ <include_subfolders>N</include_subfolders>
+ <add_result_filesname>N</add_result_filesname>
+ <destination_is_a_file>Y</destination_is_a_file>
+ <create_destination_folder>N</create_destination_folder>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <nr_errors_less_than>10</nr_errors_less_than>
+ <success_condition>success_if_no_errors</success_condition>
+ <AddDateBeforeExtension>N</AddDateBeforeExtension>
+ <DoNotKeepFolderStructure>N</DoNotKeepFolderStructure>
+ <iffileexists>overwrite_file</iffileexists>
+ <destinationFolder/>
+ <ifmovedfileexists>do_nothing</ifmovedfileexists>
+ <moved_date_time_format/>
+ <create_move_to_folder>N</create_move_to_folder>
+ <add_moved_date>N</add_moved_date>
+ <add_moved_time>N</add_moved_time>
+ <SpecifyMoveFormat>N</SpecifyMoveFormat>
+ <AddMovedDateBeforeExtension>N</AddMovedDateBeforeExtension>
+ <asciiMode>Y</asciiMode>
+ <fields>
+ <field>
+ <action_type>sign</action_type>
+
<source_filefolder>${PROJECT_HOME}/output/signing-key-source.csv</source_filefolder>
+ <wildcard/>
+ <userid></userid>
+ <local_user>[email protected]</local_user>
+
<destination_filefolder>${PROJECT_HOME}/output/signed-by-chosen-key.asc</destination_filefolder>
+ </field>
+ </fields>
+ <parallel>N</parallel>
+ <xloc>460</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>sign with a user id and no signing key</name>
+ <description>A row as written before the signing key existed. The User
ID is not the key that signs, and never was: it went to gpg as -r, which it
ignores outside encryption. Such a row has to keep signing with the default
key.</description>
+ <type>PGP_ENCRYPT_FILES</type>
+ <attributes/>
+ <gpglocation>${PROJECT_HOME}/output/gpg-two-keys.sh</gpglocation>
+ <arg_from_previous>N</arg_from_previous>
+ <include_subfolders>N</include_subfolders>
+ <add_result_filesname>N</add_result_filesname>
+ <destination_is_a_file>Y</destination_is_a_file>
+ <create_destination_folder>N</create_destination_folder>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <nr_errors_less_than>10</nr_errors_less_than>
+ <success_condition>success_if_no_errors</success_condition>
+ <AddDateBeforeExtension>N</AddDateBeforeExtension>
+ <DoNotKeepFolderStructure>N</DoNotKeepFolderStructure>
+ <iffileexists>overwrite_file</iffileexists>
+ <destinationFolder/>
+ <ifmovedfileexists>do_nothing</ifmovedfileexists>
+ <moved_date_time_format/>
+ <create_move_to_folder>N</create_move_to_folder>
+ <add_moved_date>N</add_moved_date>
+ <add_moved_time>N</add_moved_time>
+ <SpecifyMoveFormat>N</SpecifyMoveFormat>
+ <AddMovedDateBeforeExtension>N</AddMovedDateBeforeExtension>
+ <asciiMode>Y</asciiMode>
+ <fields>
+ <field>
+ <action_type>sign</action_type>
+
<source_filefolder>${PROJECT_HOME}/output/signing-key-source.csv</source_filefolder>
+ <wildcard/>
+ <userid>[email protected]</userid>
+
<destination_filefolder>${PROJECT_HOME}/output/signed-with-user-id-only.asc</destination_filefolder>
+ </field>
+ </fields>
+ <parallel>N</parallel>
+ <xloc>680</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>seal to one key and sign with another</name>
+ <description>Recipient and signer are different keys, which is what
having both options is for.</description>
+ <type>PGP_ENCRYPT_FILES</type>
+ <attributes/>
+ <gpglocation>${PROJECT_HOME}/output/gpg-two-keys.sh</gpglocation>
+ <arg_from_previous>N</arg_from_previous>
+ <include_subfolders>N</include_subfolders>
+ <add_result_filesname>N</add_result_filesname>
+ <destination_is_a_file>Y</destination_is_a_file>
+ <create_destination_folder>N</create_destination_folder>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <nr_errors_less_than>10</nr_errors_less_than>
+ <success_condition>success_if_no_errors</success_condition>
+ <AddDateBeforeExtension>N</AddDateBeforeExtension>
+ <DoNotKeepFolderStructure>N</DoNotKeepFolderStructure>
+ <iffileexists>overwrite_file</iffileexists>
+ <destinationFolder/>
+ <ifmovedfileexists>do_nothing</ifmovedfileexists>
+ <moved_date_time_format/>
+ <create_move_to_folder>N</create_move_to_folder>
+ <add_moved_date>N</add_moved_date>
+ <add_moved_time>N</add_moved_time>
+ <SpecifyMoveFormat>N</SpecifyMoveFormat>
+ <AddMovedDateBeforeExtension>N</AddMovedDateBeforeExtension>
+ <asciiMode>Y</asciiMode>
+ <fields>
+ <field>
+ <action_type>signandencrypt</action_type>
+
<source_filefolder>${PROJECT_HOME}/output/signing-key-source.csv</source_filefolder>
+ <wildcard/>
+ <userid>[email protected]</userid>
+ <local_user>[email protected]</local_user>
+
<destination_filefolder>${PROJECT_HOME}/output/sealed-and-signed.asc</destination_filefolder>
+ </field>
+ </fields>
+ <parallel>N</parallel>
+ <xloc>900</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>check which key signed each file</name>
+ <description/>
+ <type>SHELL</type>
+ <attributes/>
+ <filename/>
+ <work_directory>${PROJECT_HOME}</work_directory>
+ <arg_from_previous>N</arg_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <set_append_logfile>N</set_append_logfile>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <insertScript>Y</insertScript>
+ <script>#!/bin/bash
+
+GPG="${PROJECT_HOME}/output/gpg-two-keys.sh"
[email protected]
[email protected]
+
+# Which key does gpg reach for when nothing names one? Asked rather than
assumed, so the
+# backwards compatibility check below does not depend on the order the keys
were generated in.
+printf 'probe\n' > ${PROJECT_HOME}/output/probe.txt
+"$GPG" --batch --yes --armor --output ${PROJECT_HOME}/output/probe.asc
--clearsign \
+ -- ${PROJECT_HOME}/output/probe.txt
+if "$GPG" --batch --verify ${PROJECT_HOME}/output/probe.asc 2>&1 | grep
-q "$SIGNER_KEY"; then
+ GPG_DEFAULT="$SIGNER_KEY"
+else
+ GPG_DEFAULT="$DEFAULT_KEY"
+fi
+echo "gpg signs with $GPG_DEFAULT when no key is named"
+
+# Every check runs, so one run reports everything that is wrong rather than
only the first
+# thing. FAILURES is what the action's exit code is built from at the end.
+FAILURES=0
+
+fail() {
+ echo "FAIL: $*"
+ FAILURES=$((FAILURES + 1))
+}
+
+signature_report() {
+ "$GPG" --batch --verify "$1" 2>&1 || true
+}
+
+assert_signed_by() {
+ local file="$1" expected="$2" what="$3" report
+ report=$(signature_report "$file")
+ if echo "$report" | grep -q "$expected"; then
+ echo "OK: $what is signed by $expected"
+ else
+ fail "$what: expected a signature from $expected"
+ echo "$report"
+ fi
+}
+
+# The feature: the row named a signing key, so that key has to be the one that
signed.
+assert_signed_by ${PROJECT_HOME}/output/signed-by-chosen-key.asc "$SIGNER_KEY"
\
+ "a Sign row with a signing key"
+
+# Backwards compatibility: a row with only a User ID has always signed with
the default key,
+# and still has to. Only meaningful while the two keys differ.
+if [ "$GPG_DEFAULT" = "$SIGNER_KEY" ]; then
+ fail "gpg's default key is the signer, so the backwards compatibility check
proves nothing"
+else
+ assert_signed_by ${PROJECT_HOME}/output/signed-with-user-id-only.asc
"$GPG_DEFAULT" \
+ "a Sign row with only a User ID"
+ if signature_report ${PROJECT_HOME}/output/signed-with-user-id-only.asc |
grep -q "$SIGNER_KEY"; then
+ fail "a User ID on its own must not become the signing key"
+ else
+ echo "OK: a User ID on its own did not become the signing key"
+ fi
+fi
+
+# Sealed to one key, signed with another: decrypting reports both halves in
one go.
+REPORT=$("$GPG" --batch --decrypt ${PROJECT_HOME}/output/sealed-and-signed.asc
2>&1 || true)
+if echo "$REPORT" | grep -q "$SIGNER_KEY"; then
+ echo "OK: sealed to $DEFAULT_KEY and signed by $SIGNER_KEY"
+else
+ fail "the sealed file must carry a signature from $SIGNER_KEY"
+ echo "$REPORT"
+fi
+if echo "$REPORT" | grep -q '1,100'; then
+ echo "OK: the sealed file decrypts to the original content"
+else
+ fail "the sealed file must decrypt to the original content"
+ echo "$REPORT"
+fi
+
+gpgconf --homedir ${PROJECT_HOME}/output/gnupg-two-keys --kill all
>/dev/null 2>&1 || true
+
+if [ "$FAILURES" -ne 0 ]; then
+ echo "$FAILURES check(s) failed"
+ exit 1
+fi
+echo "all checks passed"
+</script>
+ <loglevel>Basic</loglevel>
+ <parallel>N</parallel>
+ <xloc>1120</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Abort workflow</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <message>Choosing the PGP signing key did not behave as
expected</message>
+ <loglevel>ERROR</loglevel>
+ <always_log_rows>N</always_log_rows>
+ <parallel>N</parallel>
+ <xloc>680</xloc>
+ <yloc>220</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>create a keyring with two keys</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>create a keyring with two keys</from>
+ <to>sign with the chosen key</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>sign with the chosen key</from>
+ <to>sign with a user id and no signing key</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>sign with a user id and no signing key</from>
+ <to>seal to one key and sign with another</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>seal to one key and sign with another</from>
+ <to>check which key signed each file</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>create a keyring with two keys</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>sign with the chosen key</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>sign with a user id and no signing key</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>seal to one key and sign with another</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>check which key signed each file</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
index 3fa261c892..9e4340e8ef 100644
---
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
+++
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
@@ -330,6 +330,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
vPgpFile.getActionType(),
vPgpFile.getSourceFileFolder(),
vPgpFile.getUserId(),
+ vPgpFile.getLocalUser(),
vPgpFile.getDestinationFileFolder(),
vPgpFile.getWildcard(),
parentWorkflow,
@@ -389,6 +390,11 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
previousPgpFile.setWildcard(resolve(resultRow.getString(2, null)));
previousPgpFile.setUserId(resultRow.getString(3, null));
previousPgpFile.setDestinationFileFolder(resultRow.getString(4, null));
+ // The signing key is appended after the columns this action has always
read, so a pipeline
+ // that still feeds five fields keeps working.
+ if (resultRow.size() > 5) {
+ previousPgpFile.setLocalUser(resultRow.getString(5, null));
+ }
if (!Utils.isEmpty(previousPgpFile.getSourceFileFolder())
&& !Utils.isEmpty(previousPgpFile.getDestinationFileFolder())) {
@@ -406,6 +412,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
previousPgpFile.getActionType(),
previousPgpFile.getSourceFileFolder(),
previousPgpFile.getUserId(),
+ previousPgpFile.getLocalUser(),
previousPgpFile.getDestinationFileFolder(),
previousPgpFile.getWildcard(),
parentWorkflow,
@@ -488,6 +495,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
ActionType actionType,
String sourceFileFolderName,
String userId,
+ String localUser,
String destinationFileFolderName,
String wildcard,
IWorkflowEngine<WorkflowMeta> parentWorkflow,
@@ -502,9 +510,22 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
// Get real source, destination file and wildcard
String realSourceFileFolderName = resolve(sourceFileFolderName);
String realUserId = resolve(userId);
+ String realLocalUser = resolve(localUser);
String realDestinationFileFolderName = resolve(destinationFileFolderName);
String realWildcard = resolve(wildcard);
+ // Signing has no recipient, so the user ID is not used here. It never
was: it went to gpg as
+ // -r, which GnuPG ignores for anything but encryption. Saying so out loud
beats both the old
+ // silence and quietly promoting it to the signing key, which would change
what an existing
+ // workflow signs with. Logged once per row, not once per file.
+ if (actionType == ActionType.SIGN
+ && Utils.isEmpty(realLocalUser)
+ && !Utils.isEmpty(realUserId)) {
+ logBasic(
+ BaseMessages.getString(
+ PKG, "ActionPGPEncryptFiles.Log.UserIdIgnoredWhenSigning",
realUserId));
+ }
+
try {
sourceFileFolder = HopVfs.getFileObject(realSourceFileFolderName,
getVariables());
destinationFileFolder =
HopVfs.getFileObject(realDestinationFileFolderName, getVariables());
@@ -564,6 +585,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
shortFileName,
sourceFileFolder,
realUserId,
+ realLocalUser,
destinationFile,
moveToFolderFolder,
parentWorkflow,
@@ -603,6 +625,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
shortFileName,
sourceFileFolder,
realUserId,
+ realLocalUser,
destinationfile,
moveToFolderFolder,
parentWorkflow,
@@ -660,6 +683,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
currentFile,
sourceFileFolder,
realUserId,
+ realLocalUser,
realDestinationFileFolderName,
realWildcard,
parentWorkflow,
@@ -734,6 +758,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
String shortFileName,
FileObject sourceFileName,
String userId,
+ String localUser,
FileObject destinationFileName,
FileObject moveToFolderFolder,
IWorkflowEngine<WorkflowMeta> parentWorkflow,
@@ -744,7 +769,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
try {
if (!destinationFileName.exists()) {
- doJob(actionType, sourceFileName, userId, destinationFileName);
+ doJob(actionType, sourceFileName, userId, localUser,
destinationFileName);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
@@ -771,7 +796,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
}
switch (ifFileExists) {
case "overwrite_file" -> {
- doJob(actionType, sourceFileName, userId, destinationFileName);
+ doJob(actionType, sourceFileName, userId, localUser,
destinationFileName);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
@@ -807,7 +832,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
destinationFileName.getParent().toString() +
Const.FILE_SEPARATOR + shortFilename;
destinationFile = HopVfs.getFileObject(moveToFileNameFull,
getVariables());
- doJob(actionType, sourceFileName, userId, destinationFileName);
+ doJob(actionType, sourceFileName, userId, localUser,
destinationFileName);
if (isDetailed()) {
logDetailed(
toString(),
@@ -959,6 +984,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
FileObject currentFile,
FileObject sourceFileFolder,
String userId,
+ String localUser,
String realDestinationFileFolderName,
String realWildcard,
IWorkflowEngine<WorkflowMeta> parentWorkflow,
@@ -1016,6 +1042,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
shortFileName,
currentFile,
userId,
+ localUser,
filename,
moveToFolderFolder,
parentWorkflow,
@@ -1033,6 +1060,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
shortFileName,
currentFile,
userId,
+ localUser,
filename,
moveToFolderFolder,
parentWorkflow,
@@ -1260,15 +1288,19 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
}
public void doJob(
- ActionType actionType, FileObject sourceFile, String userID, FileObject
destinationFile)
+ ActionType actionType,
+ FileObject sourceFile,
+ String userID,
+ String localUser,
+ FileObject destinationFile)
throws HopException {
switch (actionType) {
case SIGN:
- gpg.signFile(sourceFile, userID, destinationFile, isAsciiMode());
+ gpg.signFile(sourceFile, localUser, destinationFile, isAsciiMode());
break;
case SIGN_AND_ENCRYPT:
- gpg.signAndEncryptFile(sourceFile, userID, destinationFile,
isAsciiMode());
+ gpg.signAndEncryptFile(sourceFile, userID, localUser, destinationFile,
isAsciiMode());
break;
default:
gpg.encryptFile(sourceFile, userID, destinationFile, isAsciiMode());
@@ -1350,6 +1382,10 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
@HopMetadataProperty(key = "userid")
public String userId;
+ /** The key to sign with ({@code -u}). Empty leaves the choice to GnuPG. */
+ @HopMetadataProperty(key = "local_user")
+ public String localUser;
+
@HopMetadataProperty(key = "destination_filefolder")
public String destinationFileFolder;
@@ -1363,6 +1399,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
this.actionType = f.actionType;
this.sourceFileFolder = f.sourceFileFolder;
this.userId = f.userId;
+ this.localUser = f.localUser;
this.destinationFileFolder = f.destinationFileFolder;
this.wildcard = f.wildcard;
}
diff --git
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
index 2d31efd834..33efe7cc1d 100644
---
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
+++
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
@@ -515,6 +515,10 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
BaseMessages.getString(PKG,
"ActionPGPEncryptFiles.Fields.UserID.Label"),
ColumnInfo.COLUMN_TYPE_TEXT,
false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ActionPGPEncryptFiles.Fields.LocalUser.Label"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false),
new ColumnInfo(
BaseMessages.getString(
PKG,
"ActionPGPEncryptFiles.Fields.DestinationFileFolder.Label"),
@@ -529,6 +533,8 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
columnInfos[3].setToolTip(
BaseMessages.getString(PKG,
"ActionPGPEncryptFiles.Fields.UserID.Tooltip"));
columnInfos[4].setToolTip(
+ BaseMessages.getString(PKG,
"ActionPGPEncryptFiles.Fields.LocalUser.Tooltip"));
+ columnInfos[5].setToolTip(
BaseMessages.getString(PKG,
"ActionPGPEncryptFiles.Fields.DestinationFileFolder.Tooltip"));
columnInfos[0].setUsingVariables(true);
@@ -536,6 +542,7 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
columnInfos[2].setUsingVariables(true);
columnInfos[3].setUsingVariables(true);
columnInfos[4].setUsingVariables(true);
+ columnInfos[5].setUsingVariables(true);
wFields =
new TableView(
@@ -566,6 +573,7 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
wSourceFileFolder.getText(),
wWildcard.getText(),
null,
+ null,
wDestinationFileFolder.getText());
wSourceFileFolder.setText("");
wDestinationFileFolder.setText("");
@@ -598,7 +606,7 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
if (idx >= 0) {
String[] string = wFields.getItem(idx);
wSourceFileFolder.setText(string[1]);
- wDestinationFileFolder.setText(string[4]);
+ wDestinationFileFolder.setText(string[5]);
wWildcard.setText(string[2]);
wFields.remove(idx);
}
@@ -1548,7 +1556,8 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
ti.setText(2, Const.NVL(pgpFile.getSourceFileFolder(), ""));
ti.setText(3, Const.NVL(pgpFile.getWildcard(), ""));
ti.setText(4, Const.NVL(pgpFile.getUserId(), ""));
- ti.setText(5, Const.NVL(pgpFile.getDestinationFileFolder(), ""));
+ ti.setText(5, Const.NVL(pgpFile.getLocalUser(), ""));
+ ti.setText(6, Const.NVL(pgpFile.getDestinationFileFolder(), ""));
}
wFields.optimizeTableView();
}
@@ -1642,7 +1651,8 @@ public class ActionPGPEncryptFilesDialog extends
ActionDialog {
pgpFile.setSourceFileFolder(item.getText(2));
pgpFile.setWildcard(item.getText(3));
pgpFile.setUserId(item.getText(4));
- pgpFile.setDestinationFileFolder(item.getText(5));
+ pgpFile.setLocalUser(item.getText(5));
+ pgpFile.setDestinationFileFolder(item.getText(6));
}
dispose();
}
diff --git
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
index db5b2096d5..9e8263759c 100644
---
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
+++
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
@@ -279,10 +279,28 @@ public class GPG {
args.add(userID);
}
+ /**
+ * Adds the key that signs.
+ *
+ * <p>Unlike the recipient, an empty local user is omitted rather than
refused. GnuPG then picks
+ * the key named by {@code default-key} in {@code gpg.conf}, or the first
usable secret key, which
+ * is what every caller got before the option existed.
+ *
+ * @param args argument list to append to
+ * @param localUser the signing key, optional
+ */
+ private static void addLocalUser(List<String> args, String localUser) {
+ if (!Utils.isEmpty(localUser)) {
+ args.add("-u");
+ args.add(localUser);
+ }
+ }
+
/** Arguments for signing the given file with a passphrase supplied over
stdin. */
- private static List<String> signArgs(String filename) {
+ private static List<String> signArgs(String filename, String localUser) {
List<String> args = new ArrayList<>();
addPassPhraseFromStdin(args);
+ addLocalUser(args, localUser);
args.add("--sign");
args.add(END_OF_OPTIONS);
args.add(filename);
@@ -393,21 +411,49 @@ public class GPG {
}
}
+ /**
+ * Sign and encrypt a file, letting GnuPG choose the signing key.
+ *
+ * @deprecated use {@link #signAndEncryptFile(FileObject, String, String,
FileObject, boolean)},
+ * which names the signing key as well as the recipient.
+ */
+ @Deprecated(since = "2.20")
+ public void signAndEncryptFile(
+ FileObject file, String userID, FileObject cryptedFile, boolean
asciiMode)
+ throws HopException {
+ signAndEncryptFile(file, userID, null, cryptedFile, asciiMode);
+ }
+
/**
* Sign and encrypt a file
*
* @param file file to encrypt
* @param userID specific user id key, required: encrypting without one
would let GnuPG fall back
* to the default recipient in gpg.conf
+ * @param localUser the key to sign with, optional: without one GnuPG signs
with the default key
+ * from gpg.conf
* @param cryptedFile crypted filename
* @param asciiMode output ASCII file
* @throws HopException
*/
public void signAndEncryptFile(
- FileObject file, String userID, FileObject cryptedFile, boolean
asciiMode)
+ FileObject file, String userID, String localUser, FileObject
cryptedFile, boolean asciiMode)
throws HopException {
signAndEncryptFile(
- HopVfs.getFilename(file), userID, HopVfs.getFilename(cryptedFile),
asciiMode);
+ HopVfs.getFilename(file), userID, localUser,
HopVfs.getFilename(cryptedFile), asciiMode);
+ }
+
+ /**
+ * Sign and encrypt a file, letting GnuPG choose the signing key.
+ *
+ * @deprecated use {@link #signAndEncryptFile(String, String, String,
String, boolean)}, which
+ * names the signing key as well as the recipient.
+ */
+ @Deprecated(since = "2.20")
+ public void signAndEncryptFile(
+ String filename, String userID, String cryptedFilename, boolean
asciiMode)
+ throws HopException {
+ signAndEncryptFile(filename, userID, null, cryptedFilename, asciiMode);
}
/**
@@ -416,12 +462,14 @@ public class GPG {
* @param filename file to encrypt
* @param userID specific user id key, required: encrypting without one
would let GnuPG fall back
* to the default recipient in gpg.conf
+ * @param localUser the key to sign with, optional: without one GnuPG signs
with the default key
+ * from gpg.conf
* @param cryptedFilename crypted filename
* @param asciiMode output ASCII file
* @throws HopException
*/
public void signAndEncryptFile(
- String filename, String userID, String cryptedFilename, boolean
asciiMode)
+ String filename, String userID, String localUser, String
cryptedFilename, boolean asciiMode)
throws HopException {
try {
@@ -430,6 +478,7 @@ public class GPG {
args.add("-a");
}
addRecipient(args, userID);
+ addLocalUser(args, localUser);
args.add("--output");
args.add(cryptedFilename);
args.add("--encrypt");
@@ -444,25 +493,27 @@ public class GPG {
}
/**
- * Sign a file
+ * Sign a file.
*
- * @param filename file to encrypt
- * @param userID specific user id key
- * @param signedFilename crypted filename
+ * <p>The user ID is the key that signs. Signing has no recipient, so the
value is passed as
+ * {@code -u}: until Hop 2.20 it was passed as {@code -r}, which GnuPG
accepts and ignores for
+ * anything but encryption, so the key named here had no effect at all.
+ *
+ * @param filename file to sign
+ * @param localUser the key to sign with, optional: without one GnuPG signs
with the default key
+ * from gpg.conf
+ * @param signedFilename signed filename
* @param asciiMode output ASCII file
* @throws HopException
*/
- public void signFile(String filename, String userID, String signedFilename,
boolean asciiMode)
+ public void signFile(String filename, String localUser, String
signedFilename, boolean asciiMode)
throws HopException {
try {
List<String> args = new ArrayList<>(BATCH_YES);
if (asciiMode) {
args.add("-a");
}
- if (!Utils.isEmpty(userID)) {
- args.add("-r");
- args.add(userID);
- }
+ addLocalUser(args, localUser);
args.add("--output");
args.add(signedFilename);
args.add(asciiMode ? "--clearsign" : "--sign");
@@ -479,16 +530,17 @@ public class GPG {
/**
* Sign a file
*
- * @param file file to encrypt
- * @param userID specific user id key
- * @param signedFile crypted filename
+ * @param file file to sign
+ * @param localUser the key to sign with, optional: without one GnuPG signs
with the default key
+ * from gpg.conf
+ * @param signedFile signed filename
* @param asciiMode output ASCII file
* @throws HopException
*/
- public void signFile(FileObject file, String userID, FileObject signedFile,
boolean asciiMode)
+ public void signFile(FileObject file, String localUser, FileObject
signedFile, boolean asciiMode)
throws HopException {
try {
- signFile(HopVfs.getFilename(file), userID,
HopVfs.getFilename(signedFile), asciiMode);
+ signFile(HopVfs.getFilename(file), localUser,
HopVfs.getFilename(signedFile), asciiMode);
} catch (Exception e) {
throw new HopException(e);
@@ -560,23 +612,38 @@ public class GPG {
return execGnuPG(args, plainText, false);
}
+ /**
+ * Signs and encrypts a string, letting GnuPG choose the signing key.
+ *
+ * @deprecated use {@link #signAndEncrypt(String, String, String, String)},
which names the
+ * signing key as well as the recipient.
+ */
+ @Deprecated(since = "2.20")
+ public String signAndEncrypt(String plainText, String userID, String
passPhrase)
+ throws HopException {
+ return signAndEncrypt(plainText, userID, null, passPhrase);
+ }
+
/**
* Signs and encrypts a string
*
* @param plainText input string to encrypt
* @param userID key ID of the key in GnuPG's key database to encrypt with,
required: encrypting
* without one would let GnuPG fall back to the default recipient in
gpg.conf
+ * @param localUser the key to sign with, optional: without one GnuPG signs
with the default key
+ * from gpg.conf
* @param passPhrase passphrase for the personal private key to sign with
* @return encrypted string
* @throws HopException
*/
- public String signAndEncrypt(String plainText, String userID, String
passPhrase)
+ public String signAndEncrypt(String plainText, String userID, String
localUser, String passPhrase)
throws HopException {
try {
createTempFile(plainText);
List<String> args = new ArrayList<>();
addRecipient(args, userID);
+ addLocalUser(args, localUser);
addPassPhraseFromStdin(args);
args.add("-se");
args.add(END_OF_OPTIONS);
@@ -589,20 +656,32 @@ public class GPG {
}
}
+ /**
+ * Signs a string, letting GnuPG choose the signing key.
+ *
+ * @deprecated use {@link #sign(String, String, String)}, which names the
signing key.
+ */
+ @Deprecated(since = "2.20")
+ public String sign(String stringToSign, String passPhrase) throws
HopException {
+ return sign(stringToSign, null, passPhrase);
+ }
+
/**
* Sign
*
* @param stringToSign input string to sign
+ * @param localUser the key to sign with, optional: without one GnuPG signs
with the default key
+ * from gpg.conf
* @param passPhrase passphrase for the personal private key to sign with
* @throws HopException
*/
- public String sign(String stringToSign, String passPhrase) throws
HopException {
+ public String sign(String stringToSign, String localUser, String passPhrase)
throws HopException {
String retval;
try {
createTempFile(stringToSign);
- retval = execGnuPG(signArgs(getTempFileName()), passPhrase, false);
+ retval = execGnuPG(signArgs(getTempFileName(), localUser), passPhrase,
false);
} finally {
deleteTempFile();
diff --git
a/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
b/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
index 56c797d481..7cd439fed6 100644
---
a/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
+++
b/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
@@ -70,10 +70,12 @@ ActionPGPEncryptFiles.Fields.Action.Label=Action
ActionPGPEncryptFiles.Fields.DestinationFileFolder.Label=File/Folder
destination
ActionPGPEncryptFiles.Fields.DestinationFileFolder.Tooltip=Enter here the
destination folder to hit.\n If you selected file as source,you can define a
file as destination.
ActionPGPEncryptFiles.Fields.Label=Files/Folders\:
+ActionPGPEncryptFiles.Fields.LocalUser.Label=Signing key
+ActionPGPEncryptFiles.Fields.LocalUser.Tooltip=User ID of the key to sign with
(gpg ''-u'').\n Leave empty to let GnuPG use the default key from its
configuration.\n Ignored when the action only encrypts.
ActionPGPEncryptFiles.Fields.SourceFileFolder.Label=File/Folder source
ActionPGPEncryptFiles.Fields.SourceFileFolder.Tooltip=Enter here the file or
folder to move\n If it's a folder, Apache Hop will fetch only if ''Include
subfolders'' is checked\!
ActionPGPEncryptFiles.Fields.UserID.Label=User ID
-ActionPGPEncryptFiles.Fields.UserID.Tooltip=Name of the key
+ActionPGPEncryptFiles.Fields.UserID.Tooltip=User ID of the key to encrypt to
(gpg ''-r'').\n Required when the action encrypts.
ActionPGPEncryptFiles.Fields.Wildcard.Label=Wildcard
ActionPGPEncryptFiles.Fields.Wildcard.Tooltip=Specify here the wildcard to
match.\n Only files that match the wildcard will be moved.
ActionPGPEncryptFiles.FilenameAdd.Button=&Add
@@ -114,6 +116,7 @@ ActionPGPEncryptFiles.Log.IncludeSubFoldersOn=Sub folders
will be fetched ...
ActionPGPEncryptFiles.Log.Info.FilesInError=Total files in error \: {0}
ActionPGPEncryptFiles.Log.Info.FilesInSuccess=Total files successfully
processed \: {0}
ActionPGPEncryptFiles.Log.ProcessingRow=Processing row source File/folder
source \: [{0}] ... destination file/folder \: [{1}]... wildcard \: [{2}]
+ActionPGPEncryptFiles.Log.UserIdIgnoredWhenSigning=The User ID ''{0}'' is not
used by the Sign action\: it is the key to encrypt to. Fill in the Signing key
to choose the key to sign with.
ActionPGPEncryptFiles.Move_To_Folder_IfFileExists.Label=Move source file to
folder
ActionPGPEncryptFiles.MovedDateTimeFormat.Label=Date format
ActionPGPEncryptFiles.Name=Encrypt files with PGP
@@ -124,7 +127,7 @@ ActionPGPEncryptFiles.NrErrorsLessThan.Tooltip=Success when
Number of errors is
ActionPGPEncryptFiles.Overwrite_File_IfFileExists.Label=Overwrite destination
file
ActionPGPEncryptFiles.Overwrite_Filename_IffMovedFileExists.Label=Overwrite
file
ActionPGPEncryptFiles.Previous.Label=Copy previous results to args
-ActionPGPEncryptFiles.Previous.Tooltip=Check this to pass the results of the
previous entry to the arguments of this entry.\nBe careful, arguments must be
in the same order that arguments\!\n ie \:\n(1) action type (encrypt, sign,
sign and encrypt)\n(2) source folder/file\n(3) wildcard\n(4) User ID\n(5)
destination folder/file
+ActionPGPEncryptFiles.Previous.Tooltip=Check this to pass the results of the
previous entry to the arguments of this entry.\nBe careful, arguments must be
in the same order that arguments\!\n ie \:\n(1) action type (encrypt, sign,
sign and encrypt)\n(2) source folder/file\n(3) wildcard\n(4) User ID\n(5)
destination folder/file\n(6) signing key (optional)\nThe signing key comes
after the destination, not where the Signing key column sits in the grid.
ActionPGPEncryptFiles.Settings.Label=Settings
ActionPGPEncryptFiles.SourceFileFolder.Label=File/Folder source
ActionPGPEncryptFiles.SourceFileFolder.Tooltip=Enter here the file or folder
to move\n If it's a folder, check ''Include subfolders'' option if necessary\!
diff --git
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
index 3ac63d6083..77c0baa5c8 100644
---
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
+++
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
@@ -52,12 +52,14 @@ class ActionPGPEncryptFilesDialogTest extends
SwtBotTestBase {
private static final String SIGN_SOURCE = "/data/outbox/invoices.csv";
private static final String SIGN_WILDCARD = ".*\\.csv$";
- private static final String SIGN_USER_ID = "[email protected]";
+ private static final String SIGN_USER_ID = "[email protected]";
+ private static final String SIGN_LOCAL_USER = "[email protected]";
private static final String SIGN_DESTINATION =
"/data/signed/invoices.csv.asc";
private static final String SEAL_SOURCE = "/data/outbox/payments.xml";
private static final String SEAL_WILDCARD = ".*\\.xml$";
private static final String SEAL_USER_ID = "[email protected]";
+ private static final String SEAL_LOCAL_USER = "[email protected]";
private static final String SEAL_DESTINATION =
"/data/sealed/payments.xml.gpg";
@Test
@@ -72,7 +74,8 @@ class ActionPGPEncryptFilesDialogTest extends SwtBotTestBase {
assertEquals(ActionPGPEncryptFiles.ActionType.SIGN,
signRow.getActionType(), "action type");
assertEquals(SIGN_SOURCE, signRow.getSourceFileFolder(), "source
file/folder");
assertEquals(SIGN_WILDCARD, signRow.getWildcard(), "wildcard");
- assertEquals(SIGN_USER_ID, signRow.getUserId(), "user id (the key to sign
with)");
+ assertEquals(SIGN_USER_ID, signRow.getUserId(), "user id (the key to
encrypt to)");
+ assertEquals(SIGN_LOCAL_USER, signRow.getLocalUser(), "local user (the key
to sign with)");
assertEquals(SIGN_DESTINATION, signRow.getDestinationFileFolder(),
"destination file/folder");
ActionPGPEncryptFiles.PgpFile sealRow = action.getPgpFiles().get(1);
@@ -80,7 +83,8 @@ class ActionPGPEncryptFilesDialogTest extends SwtBotTestBase {
ActionPGPEncryptFiles.ActionType.SIGN_AND_ENCRYPT,
sealRow.getActionType(), "action type");
assertEquals(SEAL_SOURCE, sealRow.getSourceFileFolder(), "source
file/folder");
assertEquals(SEAL_WILDCARD, sealRow.getWildcard(), "wildcard");
- assertEquals(SEAL_USER_ID, sealRow.getUserId(), "user id (the key to sign
with)");
+ assertEquals(SEAL_USER_ID, sealRow.getUserId(), "user id (the key to
encrypt to)");
+ assertEquals(SEAL_LOCAL_USER, sealRow.getLocalUser(), "local user (the key
to sign with)");
assertEquals(SEAL_DESTINATION, sealRow.getDestinationFileFolder(),
"destination file/folder");
}
@@ -99,6 +103,7 @@ class ActionPGPEncryptFilesDialogTest extends SwtBotTestBase
{
typeInto(grid, 0, headers.indexOf(label("SourceFileFolder")),
"typed-under-source");
typeInto(grid, 0, headers.indexOf(label("Wildcard")),
"typed-under-wildcard");
typeInto(grid, 0, headers.indexOf(label("UserID")),
"typed-under-user-id");
+ typeInto(grid, 0, headers.indexOf(label("LocalUser")),
"typed-under-local-user");
typeInto(grid, 0, headers.indexOf(label("DestinationFileFolder")),
"typed-under-dest");
});
@@ -106,6 +111,7 @@ class ActionPGPEncryptFilesDialogTest extends
SwtBotTestBase {
assertEquals("typed-under-source", row.getSourceFileFolder(),
label("SourceFileFolder"));
assertEquals("typed-under-wildcard", row.getWildcard(), label("Wildcard"));
assertEquals("typed-under-user-id", row.getUserId(), label("UserID"));
+ assertEquals("typed-under-local-user", row.getLocalUser(),
label("LocalUser"));
assertEquals(
"typed-under-dest", row.getDestinationFileFolder(),
label("DestinationFileFolder"));
}
@@ -146,6 +152,7 @@ class ActionPGPEncryptFilesDialogTest extends
SwtBotTestBase {
SIGN_SOURCE,
SIGN_WILDCARD,
SIGN_USER_ID,
+ SIGN_LOCAL_USER,
SIGN_DESTINATION));
action
.getPgpFiles()
@@ -155,6 +162,7 @@ class ActionPGPEncryptFilesDialogTest extends
SwtBotTestBase {
SEAL_SOURCE,
SEAL_WILDCARD,
SEAL_USER_ID,
+ SEAL_LOCAL_USER,
SEAL_DESTINATION));
return action;
}
@@ -164,12 +172,14 @@ class ActionPGPEncryptFilesDialogTest extends
SwtBotTestBase {
String source,
String wildcard,
String userId,
+ String localUser,
String destination) {
ActionPGPEncryptFiles.PgpFile file = new ActionPGPEncryptFiles.PgpFile();
file.setActionType(actionType);
file.setSourceFileFolder(source);
file.setWildcard(wildcard);
file.setUserId(userId);
+ file.setLocalUser(localUser);
file.setDestinationFileFolder(destination);
return file;
}
diff --git
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
index cb9e212779..3929ca6bd3 100644
---
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
+++
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
@@ -18,6 +18,8 @@
package org.apache.hop.workflow.actions.pgpencryptfiles;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.mock;
@@ -64,6 +66,10 @@ import org.junit.jupiter.api.condition.OS;
class ActionPGPEncryptFilesSignTest {
private static final String KEY_USER_ID = "[email protected]";
+
+ /** A second key in the same keyring, so "which key signed this" has more
than one answer. */
+ private static final String SIGNER_USER_ID = "[email protected]";
+
private static final String PLAIN_TEXT = "Apache Hop signs this file.\n";
private static Path gpgBinary;
@@ -101,8 +107,13 @@ class ActionPGPEncryptFilesSignTest {
StandardCharsets.UTF_8);
Files.setPosixFilePermissions(gpgWrapper,
PosixFilePermissions.fromString("rwx------"));
- // A passphrase-less key: the encrypt action has no passphrase field, so
signing can only ever
+ // Passphrase-less keys: the encrypt action has no passphrase field, so
signing can only ever
// use a key gpg can unlock on its own.
+ generateKey("Hop PGP Test <" + KEY_USER_ID + ">");
+ generateKey("Hop PGP Signer <" + SIGNER_USER_ID + ">");
+ }
+
+ private void generateKey(String userId) throws Exception {
run(
gpgBinary.toString(),
"--homedir",
@@ -114,7 +125,7 @@ class ActionPGPEncryptFilesSignTest {
"--passphrase",
"",
"--quick-generate-key",
- "Hop PGP Test <" + KEY_USER_ID + ">",
+ userId,
"default",
"default",
"never");
@@ -142,8 +153,7 @@ class ActionPGPEncryptFilesSignTest {
ActionPGPEncryptFiles.ActionType.SIGN,
source,
signed,
- // The key to sign with cannot be chosen: the User ID goes to
gpg as -r, which
- // --clearsign ignores. See
https://github.com/apache/hop/issues/8206.
+ // No key named: gpg signs with its default key.
""));
Result result = sign.execute(new Result(), 0);
@@ -204,6 +214,130 @@ class ActionPGPEncryptFilesSignTest {
assertEquals(PLAIN_TEXT, Files.readString(opened), "the round trip must
preserve the content");
}
+ /**
+ * With two usable secret keys in the keyring, gpg's own default can only be
one of them. Naming
+ * either one on the row has to produce a signature from that key, which is
what
+ * https://github.com/apache/hop/issues/8659 asked for.
+ */
+ @Test
+ void theSigningKeyIsChosenPerRow() throws Exception {
+ assertSignedBy(SIGNER_USER_ID, sign(SIGNER_USER_ID,
"signed-by-signer.csv"));
+ assertSignedBy(KEY_USER_ID, sign(KEY_USER_ID, "signed-by-test.csv"));
+ }
+
+ /**
+ * A Sign row written before the signing key existed could only name a User
ID, and that went to
+ * gpg as {@code -r}, which it ignores outside encryption: such a row has
always signed with the
+ * default key. It still has to, or upgrading Hop would silently change what
those workflows sign
+ * with, and fail outright where the named key has no secret half in the
keyring.
+ */
+ @Test
+ void aUserIdOnItsOwnStillSignsWithTheDefaultKey() throws Exception {
+ Path source = Files.writeString(work.resolve("legacy.csv"), PLAIN_TEXT);
+ Path signed = work.resolve("legacy.csv.asc");
+
+ ActionPGPEncryptFiles action = encryptAction();
+ action.setAsciiMode(true);
+ action
+ .getPgpFiles()
+ .add(
+ pgpFile(
+ ActionPGPEncryptFiles.ActionType.SIGN,
+ source,
+ signed,
+ // The only field such a row has, and never the key that signs.
+ SIGNER_USER_ID,
+ null));
+
+ Result result = action.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors(), "signing must not report errors");
+ assertTrue(result.getResult(), "signing must succeed");
+
+ // Only meaningful while the two differ: were gpg's default the key this
row names, the
+ // assertion below would hold whether or not the User ID had been promoted
to the signing key.
+ String defaultKey = defaultSigningKey();
+ assertNotEquals(
+ SIGNER_USER_ID,
+ defaultKey,
+ "gpg's default key is the one this row names, so this test would prove
nothing");
+ assertSignedBy(defaultKey, signed);
+ }
+
+ /** The key gpg signs with when nothing names one: whichever of the two it
picks on its own. */
+ private String defaultSigningKey() throws Exception {
+ Path probe = Files.writeString(work.resolve("probe.csv"), PLAIN_TEXT);
+ Path signed = work.resolve("probe.csv.asc");
+
+ ActionPGPEncryptFiles action = encryptAction();
+ action.setAsciiMode(true);
+ action
+ .getPgpFiles()
+ .add(pgpFile(ActionPGPEncryptFiles.ActionType.SIGN, probe, signed, "",
null));
+ assertEquals(0, action.execute(new Result(), 0).getNrErrors(), "the probe
must sign");
+
+ String report = gpg("--verify", signed.toString());
+ return report.contains(SIGNER_USER_ID) ? SIGNER_USER_ID : KEY_USER_ID;
+ }
+
+ /** Sealing to one key while signing with another is what the two options
are for. */
+ @Test
+ void signAndEncryptSealsToTheUserIdAndSignsWithTheLocalUser() throws
Exception {
+ Path source = Files.writeString(work.resolve("statement.xml"), PLAIN_TEXT);
+ Path sealed = work.resolve("statement.xml.asc");
+
+ ActionPGPEncryptFiles seal = encryptAction();
+ seal.setAsciiMode(true);
+ seal.getPgpFiles()
+ .add(
+ pgpFile(
+ ActionPGPEncryptFiles.ActionType.SIGN_AND_ENCRYPT,
+ source,
+ sealed,
+ KEY_USER_ID,
+ SIGNER_USER_ID));
+
+ Result result = seal.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors(), "sign and encrypt must not report
errors");
+ assertTrue(result.getResult(), "sign and encrypt must succeed");
+
+ // Decrypting reports the signature as well, so one run shows both halves
of the choice.
+ String report = gpg("--decrypt", sealed.toString());
+ assertTrue(
+ report.contains(SIGNER_USER_ID),
+ "the signature must come from the key named as the signing key:\n" +
report);
+ assertTrue(report.contains(PLAIN_TEXT), "decrypting must return the
content:\n" + report);
+ }
+
+ /** Signs a file with the given key and returns the signature. */
+ private Path sign(String localUser, String name) throws Exception {
+ Path source = Files.writeString(work.resolve(name), PLAIN_TEXT);
+ Path signed = work.resolve(name + ".asc");
+
+ ActionPGPEncryptFiles action = encryptAction();
+ action.setAsciiMode(true);
+ action
+ .getPgpFiles()
+ .add(pgpFile(ActionPGPEncryptFiles.ActionType.SIGN, source, signed,
"", localUser));
+
+ Result result = action.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors(), "signing with " + localUser + " must
not report errors");
+ assertTrue(result.getResult(), "signing with " + localUser + " must
succeed");
+ return signed;
+ }
+
+ private void assertSignedBy(String expectedUserId, Path signed) throws
Exception {
+ String other = expectedUserId.equals(SIGNER_USER_ID) ? KEY_USER_ID :
SIGNER_USER_ID;
+ String report = gpg("--verify", signed.toString());
+ assertTrue(
+ report.contains(expectedUserId),
+ "the signature must come from " + expectedUserId + ":\n" + report);
+ assertFalse(
+ report.contains(other), "the signature must not come from " + other +
":\n" + report);
+ }
+
/**
* Filenames are discovered by scanning a folder, so their content is chosen
by whoever can write
* to it. They must reach gpg as literal arguments and never be interpreted.
@@ -252,11 +386,21 @@ class ActionPGPEncryptFilesSignTest {
private static ActionPGPEncryptFiles.PgpFile pgpFile(
ActionPGPEncryptFiles.ActionType actionType, Path source, Path
destination, String userId) {
+ return pgpFile(actionType, source, destination, userId, null);
+ }
+
+ private static ActionPGPEncryptFiles.PgpFile pgpFile(
+ ActionPGPEncryptFiles.ActionType actionType,
+ Path source,
+ Path destination,
+ String userId,
+ String localUser) {
ActionPGPEncryptFiles.PgpFile file = new ActionPGPEncryptFiles.PgpFile();
file.setActionType(actionType);
file.setSourceFileFolder(source.toString());
file.setDestinationFileFolder(destination.toString());
file.setUserId(userId);
+ file.setLocalUser(localUser);
return file;
}
@@ -295,6 +439,21 @@ class ActionPGPEncryptFilesSignTest {
return null;
}
+ /** Runs gpg against the throwaway keyring and returns what it reported,
stderr included. */
+ private String gpg(String... arguments) throws Exception {
+ List<String> command = new java.util.ArrayList<>();
+ command.add(gpgBinary.toString());
+ command.add("--homedir");
+ command.add(gnupgHome.toString());
+ command.addAll(List.of(arguments));
+
+ Process process = new
ProcessBuilder(command).redirectErrorStream(true).start();
+ String output = new String(process.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
+ assertTrue(process.waitFor(60, TimeUnit.SECONDS), "timed out: " +
String.join(" ", command));
+ assertEquals(0, process.exitValue(), String.join(" ", command) + "
failed:\n" + output);
+ return output;
+ }
+
private static void run(String... command) throws Exception {
Process process = new
ProcessBuilder(command).redirectErrorStream(true).start();
String output = new String(process.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
diff --git
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
index 5da97d70b1..dc055fc07d 100644
---
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
+++
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
@@ -19,6 +19,7 @@
package org.apache.hop.workflow.actions.pgpencryptfiles;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.hop.core.encryption.Encr;
@@ -74,18 +75,22 @@ class ActionPGPEncryptFilesTest {
assertEquals(ActionPGPEncryptFiles.ActionType.ENCRYPT, f.getActionType());
assertEquals("folder1", f.getSourceFileFolder());
assertEquals("user1", f.getUserId());
+ // Written before the signing key had a field of its own, so it has none.
+ assertNull(f.getLocalUser());
assertEquals("target1", f.getDestinationFileFolder());
assertEquals("wildcard1", f.getWildcard());
f = action.getPgpFiles().get(1);
assertEquals(ActionPGPEncryptFiles.ActionType.SIGN, f.getActionType());
assertEquals("folder2", f.getSourceFileFolder());
assertEquals("user2", f.getUserId());
+ assertEquals("signer2", f.getLocalUser());
assertEquals("target2", f.getDestinationFileFolder());
assertEquals("wildcard2", f.getWildcard());
f = action.getPgpFiles().getLast();
assertEquals(ActionPGPEncryptFiles.ActionType.SIGN_AND_ENCRYPT,
f.getActionType());
assertEquals("folder3", f.getSourceFileFolder());
assertEquals("user3", f.getUserId());
+ assertEquals("signer3", f.getLocalUser());
assertEquals("target3", f.getDestinationFileFolder());
assertEquals("wildcard3", f.getWildcard());
}
diff --git
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
index 7ec302f877..8857ae9940 100644
---
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
+++
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
@@ -141,7 +141,7 @@ class GpgArgumentPassingTest {
@Test
void signAndEncryptFilePassesFilenamesLiterally() throws Exception {
for (String name : HOSTILE_NAMES) {
- gpg().signAndEncryptFile(name, "[email protected]", "sealed-" + name,
true);
+ gpg().signAndEncryptFile(name, "[email protected]", null, "sealed-" +
name, true);
assertPassedLiterally(name, "signAndEncryptFile source");
assertPassedLiterally("sealed-" + name, "signAndEncryptFile
destination");
assertRecipient("[email protected]", "signAndEncryptFile");
@@ -199,7 +199,7 @@ class GpgArgumentPassingTest {
@Test
void signAndEncryptStringSendsThePassphraseOverStdin() throws Exception {
- gpg().signAndEncrypt("some data", "[email protected]", PASSPHRASE);
+ gpg().signAndEncrypt("some data", "[email protected]", null, PASSPHRASE);
assertPassphraseOnStdinOnly("signAndEncrypt");
}
@@ -220,15 +220,60 @@ class GpgArgumentPassingTest {
assertEquals("", recordedStdin(), "nothing must be written to stdin
without a passphrase");
}
+ /**
+ * Signing has no recipient. Until Hop 2.20 the key named on a SIGN row was
passed as {@code -r},
+ * which GnuPG accepts and ignores for anything but encryption, so the
choice had no effect.
+ */
@Test
- void anEmptyUserIdOmitsTheRecipientFlagWhenSigning() throws Exception {
- gpg().signFile("plain.txt", "", "plain.txt.asc", true);
+ void signingNamesTheKeyWithTheLocalUserFlag() throws Exception {
+ gpg().signFile("plain.txt", "[email protected]", "plain.txt.asc", true);
+ assertLocalUser("[email protected]", "signFile");
assertFalse(
recordedArguments().contains("-r"),
- "an empty user id must not be passed to GnuPG as an empty recipient");
+ "signing has no recipient, so the key must not be passed as one: " +
recordedArguments());
+ }
- gpg().signFile("plain.txt", "[email protected]", "plain.txt.asc", true);
- assertRecipient("[email protected]", "signFile");
+ @Test
+ void anEmptySigningKeyIsOmittedRatherThanPassedEmpty() throws Exception {
+ gpg().signFile("plain.txt", "", "plain.txt.asc", true);
+ List<String> args = recordedArguments();
+ assertFalse(args.contains("-u"), "an empty signing key must not reach
GnuPG: " + args);
+ assertFalse(args.contains("-r"), "signing must never pass a recipient: " +
args);
+ }
+
+ /**
+ * Sealing to one key and signing with another is the whole point of having
both options: the two
+ * user IDs have to land on their own flags and stay distinct.
+ */
+ @Test
+ void signAndEncryptCarriesTheRecipientAndTheSigningKeySeparately() throws
Exception {
+ gpg()
+ .signAndEncryptFile(
+ "plain.txt", "[email protected]", "[email protected]",
"sealed.asc", true);
+ assertRecipient("[email protected]", "signAndEncryptFile");
+ assertLocalUser("[email protected]", "signAndEncryptFile");
+ }
+
+ @Test
+ void signAndEncryptWithoutASigningKeyLeavesTheChoiceToGnuPg() throws
Exception {
+ gpg().signAndEncryptFile("plain.txt", "[email protected]", null,
"sealed.asc", true);
+ assertRecipient("[email protected]", "signAndEncryptFile");
+ assertFalse(
+ recordedArguments().contains("-u"),
+ "no signing key must mean no -u, so GnuPG falls back to its default
key");
+ }
+
+ @Test
+ void signAndEncryptStringCarriesTheSigningKey() throws Exception {
+ gpg().signAndEncrypt("some data", "[email protected]",
"[email protected]", PASSPHRASE);
+ assertRecipient("[email protected]", "signAndEncrypt");
+ assertLocalUser("[email protected]", "signAndEncrypt");
+ }
+
+ @Test
+ void signStringCarriesTheSigningKey() throws Exception {
+ gpg().sign("some data", "[email protected]", PASSPHRASE);
+ assertLocalUser("[email protected]", "sign");
}
/**
@@ -244,7 +289,7 @@ class GpgArgumentPassingTest {
"encryptFile must refuse an empty recipient");
assertThrows(
HopException.class,
- () -> gpg.signAndEncryptFile("plain.txt", "", "sealed.asc", false),
+ () -> gpg.signAndEncryptFile("plain.txt", "", null, "sealed.asc",
false),
"signAndEncryptFile must refuse an empty recipient");
assertThrows(
HopException.class,
@@ -252,7 +297,7 @@ class GpgArgumentPassingTest {
"encrypt must refuse an empty recipient");
assertThrows(
HopException.class,
- () -> gpg.signAndEncrypt("some data", "", PASSPHRASE),
+ () -> gpg.signAndEncrypt("some data", "", null, PASSPHRASE),
"signAndEncrypt must refuse an empty recipient");
assertFalse(Files.exists(record), "GnuPG must not be started without a
recipient");
@@ -322,7 +367,7 @@ class GpgArgumentPassingTest {
gpg().encrypt("some data", payload);
assertFalse(Files.exists(marker), "encrypt executed a command from a key
id");
- gpg().signAndEncrypt("some data", payload, PASSPHRASE);
+ gpg().signAndEncrypt("some data", payload, null, PASSPHRASE);
assertFalse(Files.exists(marker), "signAndEncrypt executed a command from
a key id");
assertPassedLiterally(payload, "the payload");
@@ -374,6 +419,15 @@ class GpgArgumentPassingTest {
assertEquals(PASSPHRASE, recordedStdin(), what + " must write the
passphrase to stdin");
}
+ private void assertLocalUser(String expected, String what) throws
IOException {
+ List<String> args = recordedArguments();
+ assertTrue(args.contains("-u"), what + " must pass a signing key: " +
args);
+ assertEquals(
+ expected,
+ args.get(args.indexOf("-u") + 1),
+ what + " must pass the signing key as its own argument following -u: "
+ args);
+ }
+
private void assertRecipient(String expected, String what) throws
IOException {
List<String> args = recordedArguments();
assertTrue(args.contains("-r"), what + " must pass a recipient: " + args);
diff --git
a/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
b/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
index 70b2071cce..d9a3a4f0eb 100644
--- a/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
+++ b/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
@@ -53,6 +53,7 @@
<action_type>sign</action_type>
<source_filefolder>folder2</source_filefolder>
<userid>user2</userid>
+ <local_user>signer2</local_user>
<destination_filefolder>target2</destination_filefolder>
<wildcard>wildcard2</wildcard>
</field>
@@ -60,6 +61,7 @@
<action_type>signandencrypt</action_type>
<source_filefolder>folder3</source_filefolder>
<userid>user3</userid>
+ <local_user>signer3</local_user>
<destination_filefolder>target3</destination_filefolder>
<wildcard>wildcard3</wildcard>
</field>