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 021d31072c issue #7176 : per-project AES/AES2 password encoder and Set
password encoder action (#7497)
021d31072c is described below
commit 021d31072cffdd29c0962d24c5308fa04410947e
Author: Matt Casters <[email protected]>
AuthorDate: Sun Jul 12 16:19:59 2026 +0200
issue #7176 : per-project AES/AES2 password encoder and Set password
encoder action (#7497)
* issue #7176 : per-project AES/AES2 password encoder and Set password
encoder action
Re-initialize the process-global two-way password encoder from project and
environment variables on project enable, support HOP_AES_ENCODER_KEY_FILE
for
Kubernetes-style key mounts, and add a workflow action for an explicit
encoder
switch on the canvas.
* issue #7176 : integration test for Set password encoder action
Add main-0019-set-password-encoder.hwf under integration-tests/actions:
Simple Evaluation of a Hop-decoded password, switch to AES2 via Set
password encoder, then Simple Evaluation of an AES2-decoded password.
* issue #7176 : stop Encr.init from requiring HopLogStore
Unconditional LogChannel.GENERAL.logBasic in Encr.init crashed unit tests
that call Encr.init before HopLogStore is initialized (common on CI
Surefire forks). Remove success logging from Encr, guard missing-plugin
logging, report encoder re-bind from ProjectsUtil instead, and isolate
AES encoder tests with AfterEach restore to Hop.
---
SECURITY.md | 9 +-
THREAT_MODEL.md | 10 +-
assemblies/debug/pom.xml | 6 +
assemblies/plugins/pom.xml | 6 +
assemblies/static/src/main/resources/hop-conf.bat | 3 +
assemblies/static/src/main/resources/hop-conf.sh | 3 +
.../static/src/main/resources/hop-encrypt.bat | 3 +
.../static/src/main/resources/hop-encrypt.sh | 3 +
assemblies/static/src/main/resources/hop-gui.bat | 3 +
assemblies/static/src/main/resources/hop-gui.sh | 3 +
.../static/src/main/resources/hop-import.bat | 3 +
assemblies/static/src/main/resources/hop-import.sh | 3 +
assemblies/static/src/main/resources/hop-run.bat | 3 +
assemblies/static/src/main/resources/hop-run.sh | 3 +
.../static/src/main/resources/hop-search.bat | 3 +
assemblies/static/src/main/resources/hop-search.sh | 3 +
.../static/src/main/resources/hop-server.bat | 3 +
assemblies/static/src/main/resources/hop-server.sh | 3 +
assemblies/static/src/main/resources/hop.bat | 3 +
core/src/main/java/org/apache/hop/core/Const.java | 20 ++
.../java/org/apache/hop/core/encryption/Encr.java | 83 ++++++-
.../core/encryption/ITwoWayPasswordEncoder.java | 12 +
docker/rest.Dockerfile | 2 +
docker/unified.Dockerfile | 4 +
docker/web.Dockerfile | 2 +
docs/hop-user-manual/modules/ROOT/nav.adoc | 1 +
.../pages/password/passwords/aespasswords.adoc | 42 +++-
.../modules/ROOT/pages/variables.adoc | 4 +-
.../pages/workflow/actions/setpasswordencoder.adoc | 52 ++++
.../actions/main-0019-set-password-encoder.hwf | 274 +++++++++++++++++++++
plugins/actions/pom.xml | 1 +
plugins/actions/setpasswordencoder/pom.xml | 32 +++
.../setpasswordencoder/src/assembly/assembly.xml | 46 ++++
.../ActionSetPasswordEncoder.java | 212 ++++++++++++++++
.../ActionSetPasswordEncoderDialog.java | 174 +++++++++++++
.../src/main/resources/SetPasswordEncoder.svg | 24 ++
.../messages/messages_en_US.properties | 32 +++
.../src/main/resources/version.xml | 20 ++
.../passwords/aes/Aes2TwoWayPasswordEncoder.java | 27 +-
.../hop/passwords/aes/AesEncoderKeyUtil.java | 91 +++++++
.../passwords/aes/AesTwoWayPasswordEncoder.java | 25 +-
.../aes/AesTwoWayPasswordEncoderTest.java | 81 +++++-
.../org/apache/hop/projects/util/ProjectsUtil.java | 24 ++
43 files changed, 1309 insertions(+), 52 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 62b250b81d..ed9982f952 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -40,9 +40,12 @@ it is not a sandbox. Operators exposing Hop on a network
should, at minimum:
- **Enable TLS** for the Hop Server and sensitive backends (off by default),
and
set `javax.net.ssl.keyStore` so outbound TLS verification is not relaxed.
- **Protect credentials at rest** with the AES2 password encoder
- (`HOP_PASSWORD_ENCODER_PLUGIN=AES2` + `HOP_AES_ENCODER_KEY`) or a secrets
- resolver (Vault / Azure Key Vault / Google Secret Manager); the default
- metadata password protection is reversible obfuscation, not encryption.
+ (`HOP_PASSWORD_ENCODER_PLUGIN=AES2` + `HOP_AES_ENCODER_KEY` or
+ `HOP_AES_ENCODER_KEY_FILE`) or a secrets resolver (Vault / Azure Key Vault /
+ Google Secret Manager); the default metadata password protection is
reversible
+ obfuscation, not encryption. Encoder settings can be supplied per project or
+ environment (re-initialized on project enable) or via the Set password
encoder
+ workflow action.
- **Only run pipelines/workflows from trusted authors**, and parameterize any
variable that may carry untrusted data.
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 3c04bd210f..7de2dd274f 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -164,8 +164,11 @@ where strict outbound TLS verification is required (see
§9).
states it is *"not really encryption … obfuscation."* Anyone with the file
recovers the plaintext. Real confidentiality is **opt-in**: set
`HOP_PASSWORD_ENCODER_PLUGIN=AES2` with an operator-held
`HOP_AES_ENCODER_KEY`
- (AES-GCM), and/or keep secrets out of metadata via a variable resolver
- (HashiCorp Vault, Azure Key Vault, Google Secret Manager). See §9, §10, §11a.
+ or `HOP_AES_ENCODER_KEY_FILE` (AES-GCM), and/or keep secrets out of metadata
+ via a variable resolver (HashiCorp Vault, Azure Key Vault, Google Secret
+ Manager). Encoder plugin and key can be re-bound per project/environment on
+ enable (and via the Set password encoder action); the active encoder remains
+ process-global. See §9, §10, §11a.
- **Credential leakage in logs.** No plaintext password is logged by default,
and decrypted DB passwords travel in a JDBC `Properties` object, not the URL.
However, resolved JDBC/REST/HTTP URLs are logged at debug/detailed level
@@ -229,7 +232,8 @@ where strict outbound TLS verification is required (see §9).
it has none of its own. Be aware the Docker image binds `0.0.0.0:8080` with
the default credential.
- **Protect credentials at rest:** enable the AES2 encoder
- (`HOP_PASSWORD_ENCODER_PLUGIN=AES2` + `HOP_AES_ENCODER_KEY`) and/or a secrets
+ (`HOP_PASSWORD_ENCODER_PLUGIN=AES2` + `HOP_AES_ENCODER_KEY` or
+ `HOP_AES_ENCODER_KEY_FILE`) and/or a secrets
resolver (Vault / Azure Key Vault / Google Secret Manager); never commit
default-obfuscated credentials to VCS; avoid embedding credentials in URLs.
- **Manage connection credentials**; don't source connection config or
diff --git a/assemblies/debug/pom.xml b/assemblies/debug/pom.xml
index 7b90c9bd17..8036f69593 100644
--- a/assemblies/debug/pom.xml
+++ b/assemblies/debug/pom.xml
@@ -217,6 +217,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-action-setpasswordencoder</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-action-setvariables</artifactId>
diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index 3943d9fdf0..d53fd7e6c5 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -222,6 +222,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-action-setpasswordencoder</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-action-setvariables</artifactId>
diff --git a/assemblies/static/src/main/resources/hop-conf.bat
b/assemblies/static/src/main/resources/hop-conf.bat
index 933708aa5a..b9268d7103 100644
--- a/assemblies/static/src/main/resources/hop-conf.bat
+++ b/assemblies/static/src/main/resources/hop-conf.bat
@@ -66,6 +66,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Conf
diff --git a/assemblies/static/src/main/resources/hop-conf.sh
b/assemblies/static/src/main/resources/hop-conf.sh
index b762787e65..d6e9cd0f72 100755
--- a/assemblies/static/src/main/resources/hop-conf.sh
+++ b/assemblies/static/src/main/resources/hop-conf.sh
@@ -63,6 +63,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=Conf
-DHOP_AUTO_CREATE_CONFIG=Y -DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop-encrypt.bat
b/assemblies/static/src/main/resources/hop-encrypt.bat
index 8eecda5f78..d57cfc86fe 100644
--- a/assemblies/static/src/main/resources/hop-encrypt.bat
+++ b/assemblies/static/src/main/resources/hop-encrypt.bat
@@ -67,6 +67,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=GUI
diff --git a/assemblies/static/src/main/resources/hop-encrypt.sh
b/assemblies/static/src/main/resources/hop-encrypt.sh
index c2b112d09a..c97b43da30 100755
--- a/assemblies/static/src/main/resources/hop-encrypt.sh
+++ b/assemblies/static/src/main/resources/hop-encrypt.sh
@@ -63,6 +63,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=Encrypt
-DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop-gui.bat
b/assemblies/static/src/main/resources/hop-gui.bat
index 59bf3473db..e5e8fba4b2 100755
--- a/assemblies/static/src/main/resources/hop-gui.bat
+++ b/assemblies/static/src/main/resources/hop-gui.bat
@@ -87,6 +87,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=GUI
diff --git a/assemblies/static/src/main/resources/hop-gui.sh
b/assemblies/static/src/main/resources/hop-gui.sh
index 05461f7e81..ef3d194ae0 100755
--- a/assemblies/static/src/main/resources/hop-gui.sh
+++ b/assemblies/static/src/main/resources/hop-gui.sh
@@ -65,6 +65,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=GUI
-DHOP_AUTO_CREATE_CONFIG=Y -DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop-import.bat
b/assemblies/static/src/main/resources/hop-import.bat
index 9ba9464885..55c5917e22 100644
--- a/assemblies/static/src/main/resources/hop-import.bat
+++ b/assemblies/static/src/main/resources/hop-import.bat
@@ -67,6 +67,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Import
diff --git a/assemblies/static/src/main/resources/hop-import.sh
b/assemblies/static/src/main/resources/hop-import.sh
index f2a5a1e49a..dd47db757f 100755
--- a/assemblies/static/src/main/resources/hop-import.sh
+++ b/assemblies/static/src/main/resources/hop-import.sh
@@ -63,6 +63,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=Import
-DHOP_AUTO_CREATE_CONFIG=Y -DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop-run.bat
b/assemblies/static/src/main/resources/hop-run.bat
index 3cfb680c2d..1dd10edc50 100755
--- a/assemblies/static/src/main/resources/hop-run.bat
+++ b/assemblies/static/src/main/resources/hop-run.bat
@@ -86,6 +86,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run
diff --git a/assemblies/static/src/main/resources/hop-run.sh
b/assemblies/static/src/main/resources/hop-run.sh
index 635e6bc53f..8959684f67 100755
--- a/assemblies/static/src/main/resources/hop-run.sh
+++ b/assemblies/static/src/main/resources/hop-run.sh
@@ -63,6 +63,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=Run
-DHOP_AUTO_CREATE_CONFIG=Y -DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop-search.bat
b/assemblies/static/src/main/resources/hop-search.bat
index 9f0f98a47c..360a3df0ee 100644
--- a/assemblies/static/src/main/resources/hop-search.bat
+++ b/assemblies/static/src/main/resources/hop-search.bat
@@ -66,6 +66,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Search
diff --git a/assemblies/static/src/main/resources/hop-search.sh
b/assemblies/static/src/main/resources/hop-search.sh
index 0046658907..00282a0a70 100755
--- a/assemblies/static/src/main/resources/hop-search.sh
+++ b/assemblies/static/src/main/resources/hop-search.sh
@@ -63,6 +63,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=Search
-DHOP_AUTO_CREATE_CONFIG=Y -DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop-server.bat
b/assemblies/static/src/main/resources/hop-server.bat
index 13244b5a67..1d39aa1527 100644
--- a/assemblies/static/src/main/resources/hop-server.bat
+++ b/assemblies/static/src/main/resources/hop-server.bat
@@ -69,6 +69,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Server
diff --git a/assemblies/static/src/main/resources/hop-server.sh
b/assemblies/static/src/main/resources/hop-server.sh
index 2883148f3d..02604432c2 100755
--- a/assemblies/static/src/main/resources/hop-server.sh
+++ b/assemblies/static/src/main/resources/hop-server.sh
@@ -63,6 +63,9 @@ fi
if [ -n "${HOP_AES_ENCODER_KEY}" ]; then
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_AES_ENCODER_KEY=${HOP_AES_ENCODER_KEY}"
fi
+if [ -n "${HOP_AES_ENCODER_KEY_FILE}" ]; then
+ HOP_OPTIONS="${HOP_OPTIONS}
-DHOP_AES_ENCODER_KEY_FILE=${HOP_AES_ENCODER_KEY_FILE}"
+fi
HOP_OPTIONS="${HOP_OPTIONS} -DHOP_PLATFORM_RUNTIME=Server
-DHOP_AUTO_CREATE_CONFIG=Y -DHOP_PLATFORM_OS="$(uname -s)
HOP_OPTIONS="${HOP_OPTIONS} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens
java.base/java.lang.invoke=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens
java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens
java.base/java.util.concurre [...]
diff --git a/assemblies/static/src/main/resources/hop.bat
b/assemblies/static/src/main/resources/hop.bat
index 04be8a57d6..af3dd79b3a 100755
--- a/assemblies/static/src/main/resources/hop.bat
+++ b/assemblies/static/src/main/resources/hop.bat
@@ -77,6 +77,9 @@ if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
if not "%HOP_AES_ENCODER_KEY%"=="" (
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+ set HOP_OPTIONS=%HOP_OPTIONS%
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run
diff --git a/core/src/main/java/org/apache/hop/core/Const.java
b/core/src/main/java/org/apache/hop/core/Const.java
index a1d5db35df..7394cd4b33 100644
--- a/core/src/main/java/org/apache/hop/core/Const.java
+++ b/core/src/main/java/org/apache/hop/core/Const.java
@@ -724,6 +724,26 @@ public class Const {
description = "Specifies the password encoder plugin to use by ID (Hop
is the default).")
public static final String HOP_PASSWORD_ENCODER_PLUGIN =
"HOP_PASSWORD_ENCODER_PLUGIN";
+ /**
+ * Key string used by the AES / AES2 two-way password encoders. Prefer
supplying this via
+ * environment or project variables rather than embedding it in metadata.
+ */
+ @Variable(
+ value = "",
+ description =
+ "Key used by the AES/AES2 password encoder plugins. Can also be set
as a system property.")
+ public static final String HOP_AES_ENCODER_KEY = "HOP_AES_ENCODER_KEY";
+
+ /**
+ * Path to a file whose contents are the AES / AES2 encoder key (for example
a Kubernetes secret
+ * mount). Used when {@link #HOP_AES_ENCODER_KEY} is empty.
+ */
+ @Variable(
+ value = "",
+ description =
+ "Path to a file containing the AES/AES2 password encoder key. Used
when HOP_AES_ENCODER_KEY is empty.")
+ public static final String HOP_AES_ENCODER_KEY_FILE =
"HOP_AES_ENCODER_KEY_FILE";
+
/**
* The name of the Hop encryption seed environment variable for the
HopTwoWayPasswordEncoder class
*/
diff --git a/core/src/main/java/org/apache/hop/core/encryption/Encr.java
b/core/src/main/java/org/apache/hop/core/encryption/Encr.java
index 878569be44..93eab02ec1 100644
--- a/core/src/main/java/org/apache/hop/core/encryption/Encr.java
+++ b/core/src/main/java/org/apache/hop/core/encryption/Encr.java
@@ -20,15 +20,22 @@ package org.apache.hop.core.encryption;
import org.apache.hop.core.Const;
import org.apache.hop.core.HopClientEnvironment;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.HopLogStore;
import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.plugins.IPlugin;
import org.apache.hop.core.plugins.PluginRegistry;
import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
import org.eclipse.jetty.util.security.Password;
/**
* This class handles basic encryption of passwords in Hop. Note that it's not
really encryption,
* it's more obfuscation. Passwords are <b>difficult</b> to read, not
impossible.
+ *
+ * <p>The active encoder is process-global. Call {@link #init(String,
IVariables)} or {@link
+ * #initFromVariables(IVariables)} to re-bind it (for example when enabling a
project or running a
+ * Set password encoder action).
*/
public class Encr {
@@ -38,24 +45,90 @@ public class Encr {
// Do nothing
}
+ /**
+ * Initialize the global two-way password encoder from the given plugin ID
using the default
+ * variable space.
+ *
+ * @param encoderPluginId plugin ID (for example {@code Hop} or {@code AES2})
+ * @throws HopException when the encoder cannot be loaded or initialized
+ */
public static void init(String encoderPluginId) throws HopException {
+ init(encoderPluginId, Variables.getADefaultVariableSpace());
+ }
+
+ /**
+ * Initialize the global two-way password encoder from the given plugin ID
and variable space.
+ * Thread-safe: only one re-init runs at a time.
+ *
+ * @param encoderPluginId plugin ID (for example {@code Hop} or {@code AES2})
+ * @param variables variables used to resolve key material and related
options
+ * @throws HopException when the encoder cannot be loaded or initialized
+ */
+ public static synchronized void init(String encoderPluginId, IVariables
variables)
+ throws HopException {
if (Utils.isEmpty(encoderPluginId)) {
throw new HopException(
"Unable to initialize the two way password encoder: No encoder
plugin type specified.");
}
+ if (variables == null) {
+ variables = Variables.getADefaultVariableSpace();
+ }
PluginRegistry registry = PluginRegistry.getInstance();
IPlugin plugin =
registry.findPluginWithId(TwoWayPasswordEncoderPluginType.class,
encoderPluginId);
+ ITwoWayPasswordEncoder newEncoder;
if (plugin == null) {
- LogChannel.GENERAL.logError("Unable to find plugin with ID '" +
encoderPluginId + "'");
- encoder = new HopTwoWayPasswordEncoder();
+ // Do not require HopLogStore: many unit tests call Encr.init without
full environment init.
+ logSafely(
+ true, "Unable to find two-way password encoder plugin with ID '" +
encoderPluginId + "'");
+ newEncoder = new HopTwoWayPasswordEncoder();
} else {
- encoder = (ITwoWayPasswordEncoder) registry.loadClass(plugin);
+ newEncoder = (ITwoWayPasswordEncoder) registry.loadClass(plugin);
}
- // Load encoder specific options...
+ // Load encoder specific options from the provided variable space...
//
- encoder.init();
+ newEncoder.init(variables);
+ encoder = newEncoder;
+ // Intentionally no success log here: callers with a live log channel
(project enable, Set
+ // password encoder action) report the active plugin ID. Logging from Encr
itself would break
+ // unit tests that initialize the encoder before HopLogStore is ready.
+ }
+
+ /**
+ * Log only when the central log store is available. Avoids {@code Central
Log Store is not
+ * initialized} crashes during lightweight unit-test bootstrap.
+ */
+ private static void logSafely(boolean error, String message) {
+ if (!HopLogStore.isInitialized()) {
+ return;
+ }
+ if (error) {
+ LogChannel.GENERAL.logError(message);
+ } else {
+ LogChannel.GENERAL.logBasic(message);
+ }
+ }
+
+ /**
+ * Resolve the password encoder plugin ID from variables (then system
properties, then {@code
+ * Hop}) and re-initialize the global encoder.
+ *
+ * @param variables project/environment or workflow variables
+ * @throws HopException when initialization fails
+ */
+ public static synchronized void initFromVariables(IVariables variables)
throws HopException {
+ if (variables == null) {
+ variables = Variables.getADefaultVariableSpace();
+ }
+ String pluginId = variables.getVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN);
+ if (Utils.isEmpty(pluginId)) {
+ pluginId = System.getProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN);
+ }
+ if (Utils.isEmpty(pluginId)) {
+ pluginId = "Hop";
+ }
+ init(pluginId, variables);
}
public static final String encryptPassword(String password) {
diff --git
a/core/src/main/java/org/apache/hop/core/encryption/ITwoWayPasswordEncoder.java
b/core/src/main/java/org/apache/hop/core/encryption/ITwoWayPasswordEncoder.java
index e0a7a0d492..fdf9cc21cd 100644
---
a/core/src/main/java/org/apache/hop/core/encryption/ITwoWayPasswordEncoder.java
+++
b/core/src/main/java/org/apache/hop/core/encryption/ITwoWayPasswordEncoder.java
@@ -18,6 +18,7 @@
package org.apache.hop.core.encryption;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
public interface ITwoWayPasswordEncoder {
@@ -29,6 +30,17 @@ public interface ITwoWayPasswordEncoder {
*/
void init() throws HopException;
+ /**
+ * Initialize the password encoder using the given variable space (for
example project or
+ * environment variables). Default implementation delegates to {@link
#init()}.
+ *
+ * @param variables variables to resolve key material and related options
from
+ * @throws HopException when initialization fails
+ */
+ default void init(IVariables variables) throws HopException {
+ init();
+ }
+
/**
* Encode the raw password, include a prefix indicating the type of
encryption used.
*
diff --git a/docker/rest.Dockerfile b/docker/rest.Dockerfile
index 7c083b5c36..d2f541acb3 100644
--- a/docker/rest.Dockerfile
+++ b/docker/rest.Dockerfile
@@ -20,6 +20,7 @@ FROM tomcat:10-jdk21-openjdk
LABEL maintainer="Apache Hop Team"
ENV HOP_CONFIG_FOLDER=""
ENV HOP_AES_ENCODER_KEY=""
+ENV HOP_AES_ENCODER_KEY_FILE=""
ENV HOP_AUDIT_FOLDER="${CATALINA_HOME}/webapps/ROOT/audit"
ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
# specify the hop log level
@@ -37,6 +38,7 @@ ENV HOP_REST_CONFIG_FOLDER="/config"
# Set TOMCAT start variables
ENV CATALINA_OPTS='${HOP_OPTIONS} \
-DHOP_AES_ENCODER_KEY="${HOP_AES_ENCODER_KEY}" \
+ -DHOP_AES_ENCODER_KEY_FILE="${HOP_AES_ENCODER_KEY_FILE}" \
-DHOP_AUDIT_FOLDER="${HOP_AUDIT_FOLDER}" \
-DHOP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}" \
-DHOP_LOG_LEVEL="${HOP_LOG_LEVEL}" \
diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index 8b59182842..e239035101 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -320,6 +320,7 @@ ARG HOP_GID=501
# Environment variables
ENV DEPLOYMENT_PATH=/usr/local/tomcat/webapps/ROOT
ENV HOP_AES_ENCODER_KEY=""
+ENV HOP_AES_ENCODER_KEY_FILE=""
ENV HOP_AUDIT_FOLDER="${CATALINA_HOME}/webapps/ROOT/audit"
ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
ENV HOP_LOG_LEVEL="Basic"
@@ -339,6 +340,7 @@ ENV HOP_ENVIRONMENT_CONFIG_FILE_NAME_PATHS=
# Set TOMCAT start variables
ENV CATALINA_OPTS='${HOP_OPTIONS} \
-DHOP_AES_ENCODER_KEY="${HOP_AES_ENCODER_KEY}" \
+ -DHOP_AES_ENCODER_KEY_FILE="${HOP_AES_ENCODER_KEY_FILE}" \
-DHOP_AUDIT_FOLDER="${HOP_AUDIT_FOLDER}" \
-DHOP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}" \
-DHOP_LOG_LEVEL="${HOP_LOG_LEVEL}" \
@@ -371,6 +373,7 @@ FROM tomcat:10-jdk21 AS rest
# Environment variables
ENV HOP_CONFIG_FOLDER=""
ENV HOP_AES_ENCODER_KEY=""
+ENV HOP_AES_ENCODER_KEY_FILE=""
ENV HOP_AUDIT_FOLDER="${CATALINA_HOME}/webapps/ROOT/audit"
ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
ENV HOP_LOG_LEVEL="Basic"
@@ -383,6 +386,7 @@ ENV HOP_REST_CONFIG_FOLDER="/config"
# Set TOMCAT start variables
ENV CATALINA_OPTS='${HOP_OPTIONS} \
-DHOP_AES_ENCODER_KEY="${HOP_AES_ENCODER_KEY}" \
+ -DHOP_AES_ENCODER_KEY_FILE="${HOP_AES_ENCODER_KEY_FILE}" \
-DHOP_AUDIT_FOLDER="${HOP_AUDIT_FOLDER}" \
-DHOP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}" \
-DHOP_LOG_LEVEL="${HOP_LOG_LEVEL}" \
diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile
index 3fbf4dd062..7dfd19c72b 100644
--- a/docker/web.Dockerfile
+++ b/docker/web.Dockerfile
@@ -25,6 +25,7 @@ ARG HOP_GID=501
# path to where the artifacts should be deployed to
ENV DEPLOYMENT_PATH=/usr/local/tomcat/webapps/ROOT
ENV HOP_AES_ENCODER_KEY=""
+ENV HOP_AES_ENCODER_KEY_FILE=""
ENV HOP_AUDIT_FOLDER="${CATALINA_HOME}/webapps/ROOT/audit"
ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
# specify the hop log level
@@ -52,6 +53,7 @@ ENV HOP_ENVIRONMENT_CONFIG_FILE_NAME_PATHS=
# Set TOMCAT start variables
ENV CATALINA_OPTS='${HOP_OPTIONS} \
-DHOP_AES_ENCODER_KEY="${HOP_AES_ENCODER_KEY}" \
+ -DHOP_AES_ENCODER_KEY_FILE="${HOP_AES_ENCODER_KEY_FILE}" \
-DHOP_AUDIT_FOLDER="${HOP_AUDIT_FOLDER}" \
-DHOP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}" \
-DHOP_LOG_LEVEL="${HOP_LOG_LEVEL}" \
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index b81ffb068b..422e970bae 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -356,6 +356,7 @@ under the License.
*** xref:workflow/actions/runpipelinetests.adoc[Run pipeline unit tests]
*** xref:workflow/actions/sendnagiospassivecheck.adoc[Send Nagios passive
check]
*** xref:workflow/actions/snmptrap.adoc[Send SNMP trap]
+*** xref:workflow/actions/setpasswordencoder.adoc[Set password encoder]
*** xref:workflow/actions/setvariables.adoc[Set variables]
*** xref:workflow/actions/shell.adoc[Shell]
*** xref:workflow/actions/simpleeval.adoc[Simple evaluation]
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/password/passwords/aespasswords.adoc
b/docs/hop-user-manual/modules/ROOT/pages/password/passwords/aespasswords.adoc
index 84fca333ee..5f1932eb8a 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/password/passwords/aespasswords.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/password/passwords/aespasswords.adoc
@@ -26,12 +26,48 @@ WARNING: The AES prefix is deprecated since version 2.11,
use the AES2 prefix th
The AES two-way password encoder plugin allows you to encrypt and decrypt
passwords using a provided key string.
+The active encoder is *process-global* for the Hop JVM. It can be selected at
startup (system properties), re-initialized when a project or environment is
enabled, or switched explicitly with the
xref:workflow/actions/setpasswordencoder.adoc[Set password encoder] workflow
action.
+
== Configuration
-The configuration of this encoder is for the whole Hop environment and can as
such only be done using system properties. These properties are listed below
and can also be specified as system properties with hop-run:
+=== Variables and system properties
+
+These properties can be set as system properties (`-D` / environment for hop
scripts), as project or environment described variables, or a combination:
+
+* *`HOP_PASSWORD_ENCODER_PLUGIN`*: set this to the ID of this plugin: `AES2`
(or `AES` for the deprecated plugin). Use `Hop` to fall back to obfuscation.
+* *`HOP_AES_ENCODER_KEY`*: the key string used to encode or decode passwords
+* *`HOP_AES_ENCODER_KEY_FILE`*: path to a file whose contents are the key
(used when `HOP_AES_ENCODER_KEY` is empty). Useful for Kubernetes secret
mounts, for example `${PROJECT_HOME}/.aes.key` or `/secrets/aes.key`
+
+Key material resolution order for AES/AES2:
+
+. Non-empty `HOP_AES_ENCODER_KEY` from the current variable space (or system
property)
+. Non-empty `HOP_AES_ENCODER_KEY_FILE` (variable or system property) → read
file contents
+
+=== Per project / environment
+
+When a project (and optional lifecycle environment) is enabled, Hop
re-initializes the password encoder from the resulting variable space. That
means:
+
+* Different environments can use different AES2 keys
+* Switching projects resets the encoder so a previous project's key does not
stick
+* Omitting AES settings (or setting `HOP_PASSWORD_ENCODER_PLUGIN=Hop`) returns
to obfuscation
+
+Example environment config variables:
+
+[source,json]
+----
+{
+ "variables": [
+ { "name": "HOP_PASSWORD_ENCODER_PLUGIN", "value": "AES2", "description":
"Use AES2 for this environment" },
+ { "name": "HOP_AES_ENCODER_KEY_FILE", "value": "${PROJECT_HOME}/.aes.key",
"description": "Per-project key file" }
+ ]
+}
+----
+
+Do not commit key files into source control.
+
+=== Workflow action
-* *`HOP_PASSWORD_ENCODER_PLUGIN`*: set this to the ID of this plugin: AES2
-* *`HOP_AES_ENCODER_KEY`*: Specify the key to use to encode or decode passwords
+For an explicit, reviewable decision on the canvas, use
xref:workflow/actions/setpasswordencoder.adoc[Set password encoder]. It
re-binds the same process-global encoder and must not store the raw key in the
workflow.
== Noteworthy
diff --git a/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
b/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
index 763424ee72..a14cd93af1 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
@@ -293,7 +293,9 @@ Set to 0 to keep all rows (default)
Set to 0 to keep all rows indefinitely (default)
|HOP_MAX_TAB_LENGTH|-|A variable to configure Tab size
|HOP_MAX_WORKFLOW_TRACKER_SIZE|5000|The maximum age (in minutes) of a log line
while being kept internally by Hop. Set to 0 to keep all rows indefinitely
(default)
-|HOP_PASSWORD_ENCODER_PLUGIN|Hop|Specifies the password encoder plugin to use
by ID (Hop is the default).
+|HOP_PASSWORD_ENCODER_PLUGIN|Hop|Specifies the password encoder plugin to use
by ID (Hop is the default). Re-applied when a project/environment is enabled.
+|HOP_AES_ENCODER_KEY||Key string for the AES/AES2 password encoder plugins.
Prefer environment variables or a secrets mount over embedding in metadata.
+|HOP_AES_ENCODER_KEY_FILE||Path to a file containing the AES/AES2 encoder key.
Used when `HOP_AES_ENCODER_KEY` is empty.
|HOP_PIPELINE_ROWSET_SIZE|-|Name of the environment variable that contains the
size of the pipeline rowset size. This overwrites values that you set pipeline
settings
|HOP_QUERY_PREVIEW_TIMEOUT|0|Default JDBC statement query timeout in
**seconds** for database query preview (for example the initial value in the
preview settings dialog for Table Input / Get table names, and related preview
flows). `0` means no default from this variable. Does not apply to normal
pipeline execution for those transforms' JDBC timeouts in the same way; use the
preview dialog to set the timeout for a given preview run.
|HOP_PLUGIN_CLASSES||A comma delimited list of classes to scan for plugin
annotations
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/setpasswordencoder.adoc
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/setpasswordencoder.adoc
new file mode 100644
index 0000000000..87da6bc3b6
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/setpasswordencoder.adoc
@@ -0,0 +1,52 @@
+////
+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.
+////
+:documentationPath: /workflow/actions/
+:language: en_US
+:description: The Set password encoder action enables or switches the
process-global two-way password encoder from a point in a workflow.
+
+= Set password encoder
+
+== Description
+
+The `Set password encoder` action enables or switches the *process-global*
two-way password encoder from a specific point in a workflow.
+
+Use this when the choice of encoder (Hop obfuscation, AES2, and so on) should
be a *visible decision on the canvas*, not only a setting hidden in environment
configuration.
+
+Default configuration for a project or environment should still use
project/environment variables (see
xref:password/passwords/aespasswords.adoc[AES two-way password encoder]). This
action is for explicit, auditable control in a workflow.
+
+[WARNING]
+====
+The encoder is **process-global**. Changing it affects every concurrent
pipeline and workflow in the same JVM after this action runs. Place it before
any step that needs the new encoder to decrypt secrets.
+====
+
+== Options
+
+[options="header", width="90%", cols="1,3"]
+|===
+|Option|Description
+|Action name|The name of the workflow action.
+|Encoder plugin ID|Plugin ID to activate: `Hop` (obfuscation), `AES2`
(recommended), or `AES` (deprecated). Other registered two-way password encoder
IDs can be typed as well.
+|Key variable name|Name of the variable that holds the AES/AES2 key (default
`HOP_AES_ENCODER_KEY`). Do **not** put the raw key in the workflow definition.
+|Key file path|Optional path to a file containing the AES/AES2 key (supports
variables). Used when the key variable is empty. Suitable for Kubernetes secret
mounts.
+|===
+
+== Best practices
+
+* Prefer environment config variables for day-to-day keys; use this action
when the switch must be visible in the workflow.
+* Inject the key via the environment, a secrets manager, or a key file — never
commit keys into HWF or git.
+* Set plugin ID to `Hop` to disable AES for subsequent steps (return to
obfuscation).
+* Mixing ciphertext prefixes (`Encrypted`, `AES`, `AES2`) is not supported for
a single active encoder.
diff --git a/integration-tests/actions/main-0019-set-password-encoder.hwf
b/integration-tests/actions/main-0019-set-password-encoder.hwf
new file mode 100644
index 0000000000..38d458287d
--- /dev/null
+++ b/integration-tests/actions/main-0019-set-password-encoder.hwf
@@ -0,0 +1,274 @@
+<?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-0019-set-password-encoder</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Evaluate a Hop-encoded password, switch to AES2 with Set
password encoder, then evaluate an AES2-encoded password.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 12: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>64</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Set test variables</name>
+ <description>Known Hop and AES2 ciphertexts plus the AES2 key used to
encrypt the AES2 sample.</description>
+ <type>SET_VARIABLES</type>
+ <attributes/>
+ <replacevars>Y</replacevars>
+ <filename/>
+ <file_variable_type>JVM</file_variable_type>
+ <fields>
+ <field>
+ <variable_name>HOP_ENC</variable_name>
+ <variable_value>Encrypted
2be98afc86aa7a181a80bab64f69dbf8b</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ <field>
+ <variable_name>HOP_PLAIN</variable_name>
+ <variable_value>SecretHop1</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ <field>
+ <variable_name>AES2_ENC</variable_name>
+ <variable_value>AES2
5SPGXV3SEPhoDRiYIxBv+wtnSC7WQYoYU6zyJx1Y75sEzHAM</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ <field>
+ <variable_name>AES2_PLAIN</variable_name>
+ <variable_value>abcdefgh</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ <field>
+ <variable_name>HOP_AES_ENCODER_KEY</variable_name>
+ <variable_value>abcdef</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ </fields>
+ <parallel>N</parallel>
+ <xloc>224</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Decrypt Hop encoded</name>
+ <description>Decrypt the Hop-encoded password with the default Hop
encoder into DECRYPTED.</description>
+ <type>EVAL</type>
+ <attributes/>
+ <script>// Decrypt Hop-obfuscated password using the process-global
encoder (Hop by default)
+var enc = parent_workflow.getVariable("HOP_ENC");
+var plain =
org.apache.hop.core.encryption.Encr.decryptPasswordOptionallyEncrypted(enc);
+parent_workflow.setVariable("DECRYPTED", plain);
+true;</script>
+ <parallel>N</parallel>
+ <xloc>400</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Simple evaluation Hop</name>
+ <description>Assert Hop decrypt of Encrypted … equals
SecretHop1</description>
+ <type>SIMPLE_EVAL</type>
+ <attributes/>
+ <comparevalue>${HOP_PLAIN}</comparevalue>
+ <fieldtype>string</fieldtype>
+ <successbooleancondition>true</successbooleancondition>
+ <successcondition>equal</successcondition>
+ <successnumbercondition>equal</successnumbercondition>
+ <successwhenvarset>N</successwhenvarset>
+ <valuetype>variable</valuetype>
+ <variablename>DECRYPTED</variablename>
+ <parallel>N</parallel>
+ <xloc>576</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Set password encoder AES2</name>
+ <description>Switch the process-global encoder to AES2 using
HOP_AES_ENCODER_KEY.</description>
+ <type>SET_PASSWORD_ENCODER</type>
+ <attributes/>
+ <encoder_plugin_id>AES2</encoder_plugin_id>
+ <key_variable>HOP_AES_ENCODER_KEY</key_variable>
+ <key_file/>
+ <parallel>N</parallel>
+ <xloc>768</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Decrypt AES2 encoded</name>
+ <description>Decrypt the AES2-encoded password with the newly activated
AES2 encoder into DECRYPTED.</description>
+ <type>EVAL</type>
+ <attributes/>
+ <script>// Decrypt AES2 ciphertext now that Set password encoder
activated AES2
+var enc = parent_workflow.getVariable("AES2_ENC");
+var plain =
org.apache.hop.core.encryption.Encr.decryptPasswordOptionallyEncrypted(enc);
+parent_workflow.setVariable("DECRYPTED", plain);
+true;</script>
+ <parallel>N</parallel>
+ <xloc>960</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Simple evaluation AES2</name>
+ <description>Assert AES2 decrypt equals abcdefgh</description>
+ <type>SIMPLE_EVAL</type>
+ <attributes/>
+ <comparevalue>${AES2_PLAIN}</comparevalue>
+ <fieldtype>string</fieldtype>
+ <successbooleancondition>true</successbooleancondition>
+ <successcondition>equal</successcondition>
+ <successnumbercondition>equal</successnumbercondition>
+ <successwhenvarset>N</successwhenvarset>
+ <valuetype>variable</valuetype>
+ <variablename>DECRYPTED</variablename>
+ <parallel>N</parallel>
+ <xloc>1152</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Success</name>
+ <description/>
+ <type>SUCCESS</type>
+ <attributes/>
+ <parallel>N</parallel>
+ <xloc>1344</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Abort Hop eval failed</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <always_log_rows>N</always_log_rows>
+ <message>Hop-encoded password did not decrypt to the expected
plaintext</message>
+ <parallel>N</parallel>
+ <xloc>576</xloc>
+ <yloc>272</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Abort AES2 eval failed</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <always_log_rows>N</always_log_rows>
+ <message>AES2-encoded password did not decrypt to the expected plaintext
after Set password encoder</message>
+ <parallel>N</parallel>
+ <xloc>1152</xloc>
+ <yloc>272</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Set test variables</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>Set test variables</from>
+ <to>Decrypt Hop encoded</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>Decrypt Hop encoded</from>
+ <to>Simple evaluation Hop</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Simple evaluation Hop</from>
+ <to>Set password encoder AES2</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Simple evaluation Hop</from>
+ <to>Abort Hop eval failed</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Set password encoder AES2</from>
+ <to>Decrypt AES2 encoded</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Decrypt AES2 encoded</from>
+ <to>Simple evaluation AES2</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Simple evaluation AES2</from>
+ <to>Success</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Simple evaluation AES2</from>
+ <to>Abort AES2 eval failed</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/plugins/actions/pom.xml b/plugins/actions/pom.xml
index a6535b4dbb..931e2c380d 100644
--- a/plugins/actions/pom.xml
+++ b/plugins/actions/pom.xml
@@ -62,6 +62,7 @@
<module>pipeline</module>
<module>repeat</module>
<module>sendnagiospassivecheck</module>
+ <module>setpasswordencoder</module>
<module>setvariables</module>
<module>shell</module>
<module>simpleeval</module>
diff --git a/plugins/actions/setpasswordencoder/pom.xml
b/plugins/actions/setpasswordencoder/pom.xml
new file mode 100644
index 0000000000..d8e1f480bf
--- /dev/null
+++ b/plugins/actions/setpasswordencoder/pom.xml
@@ -0,0 +1,32 @@
+<?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.
+ ~
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-plugins-actions</artifactId>
+ <version>2.19.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>hop-action-setpasswordencoder</artifactId>
+ <packaging>jar</packaging>
+ <name>Hop Plugins Actions Set Password Encoder</name>
+
+</project>
diff --git a/plugins/actions/setpasswordencoder/src/assembly/assembly.xml
b/plugins/actions/setpasswordencoder/src/assembly/assembly.xml
new file mode 100644
index 0000000000..51c10618e8
--- /dev/null
+++ b/plugins/actions/setpasswordencoder/src/assembly/assembly.xml
@@ -0,0 +1,46 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+ <id>hop-action-setpasswordencoder</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>.</baseDirectory>
+ <files>
+ <file>
+ <source>${project.basedir}/src/main/resources/version.xml</source>
+ <outputDirectory>${hop.plugin.libdir}</outputDirectory>
+ <filtered>true</filtered>
+ </file>
+ </files>
+
+ <fileSets>
+ <fileSet>
+ <directory>${project.basedir}/src/main/samples</directory>
+ <outputDirectory>config/projects/samples/</outputDirectory>
+ </fileSet>
+ </fileSets>
+
+
+ <componentDescriptors>
+
<componentDescriptor>${maven.multiModuleProjectDirectory}/assemblies/shared/hop-plugin-libs.xml</componentDescriptor>
+ </componentDescriptors>
+</assembly>
diff --git
a/plugins/actions/setpasswordencoder/src/main/java/org/apache/hop/workflow/actions/setpasswordencoder/ActionSetPasswordEncoder.java
b/plugins/actions/setpasswordencoder/src/main/java/org/apache/hop/workflow/actions/setpasswordencoder/ActionSetPasswordEncoder.java
new file mode 100644
index 0000000000..4fc185f293
--- /dev/null
+++
b/plugins/actions/setpasswordencoder/src/main/java/org/apache/hop/workflow/actions/setpasswordencoder/ActionSetPasswordEncoder.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.workflow.actions.setpasswordencoder;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.annotations.Action;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.encryption.ITwoWayPasswordEncoder;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.json.JsonMetadataProvider;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider;
+import org.apache.hop.workflow.action.ActionBase;
+import org.apache.hop.workflow.action.IAction;
+
+/**
+ * Explicitly enable (or switch) the process-global two-way password encoder
from a point in a
+ * workflow. Prefer project/environment variables for default configuration;
use this action when
+ * the decision should be visible on the canvas.
+ *
+ * <p>Does not store raw key material in the workflow. Supply a key via
variable (default {@link
+ * Const#HOP_AES_ENCODER_KEY}) and/or a key file path ({@link
Const#HOP_AES_ENCODER_KEY_FILE}).
+ */
+@Action(
+ id = "SET_PASSWORD_ENCODER",
+ name = "i18n::ActionSetPasswordEncoder.Name",
+ description = "i18n::ActionSetPasswordEncoder.Description",
+ image = "SetPasswordEncoder.svg",
+ categoryDescription =
"i18n:org.apache.hop.workflow:ActionCategory.Category.Utility",
+ keywords = "i18n::ActionSetPasswordEncoder.keyword",
+ documentationUrl = "/workflow/actions/setpasswordencoder.html")
+public class ActionSetPasswordEncoder extends ActionBase implements Cloneable,
IAction {
+ private static final Class<?> PKG = ActionSetPasswordEncoder.class;
+
+ /** Plugin ID: Hop, AES2, AES, or another registered two-way password
encoder. */
+ @HopMetadataProperty(key = "encoder_plugin_id")
+ private String encoderPluginId;
+
+ /**
+ * Optional variable name that holds the AES key. When empty, {@link
Const#HOP_AES_ENCODER_KEY} is
+ * used as-is from the variable space / system properties.
+ */
+ @HopMetadataProperty(key = "key_variable")
+ private String keyVariable;
+
+ /** Optional path to a file containing the AES key (supports variables). */
+ @HopMetadataProperty(key = "key_file")
+ private String keyFile;
+
+ public ActionSetPasswordEncoder(String name) {
+ super(name, "");
+ encoderPluginId = "AES2";
+ keyVariable = Const.HOP_AES_ENCODER_KEY;
+ keyFile = null;
+ }
+
+ public ActionSetPasswordEncoder() {
+ this("");
+ }
+
+ public ActionSetPasswordEncoder(ActionSetPasswordEncoder other) {
+ super(other.getName(), other.getDescription(), other.getPluginId());
+ this.encoderPluginId = other.encoderPluginId;
+ this.keyVariable = other.keyVariable;
+ this.keyFile = other.keyFile;
+ }
+
+ @Override
+ public Object clone() {
+ return new ActionSetPasswordEncoder(this);
+ }
+
+ @Override
+ public Result execute(Result result, int nr) throws HopException {
+ result.setResult(false);
+ result.setNrErrors(1);
+
+ try {
+ String pluginId = resolve(Const.NVL(encoderPluginId, "Hop"));
+ if (Utils.isEmpty(pluginId)) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.Error.PluginIdMissing"));
+ }
+
+ // Optionally promote a named variable's value into HOP_AES_ENCODER_KEY
for the encoder.
+ // Only set workflow variables — do not write key material into JVM
system properties so
+ // project switches do not inherit a previous key via
Variables.initializeFrom(System).
+ String keyVarName = resolve(Const.NVL(keyVariable,
Const.HOP_AES_ENCODER_KEY));
+ if (StringUtils.isNotEmpty(keyVarName) &&
!Const.HOP_AES_ENCODER_KEY.equals(keyVarName)) {
+ String keyValue = getVariable(keyVarName);
+ if (StringUtils.isNotEmpty(keyValue)) {
+ setVariable(Const.HOP_AES_ENCODER_KEY, keyValue);
+ if (parentWorkflow != null) {
+ parentWorkflow.setVariable(Const.HOP_AES_ENCODER_KEY, keyValue);
+ }
+ }
+ }
+
+ String resolvedKeyFile = resolve(keyFile);
+ if (StringUtils.isNotEmpty(resolvedKeyFile)) {
+ setVariable(Const.HOP_AES_ENCODER_KEY_FILE, resolvedKeyFile);
+ if (parentWorkflow != null) {
+ parentWorkflow.setVariable(Const.HOP_AES_ENCODER_KEY_FILE,
resolvedKeyFile);
+ }
+ }
+
+ setVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN, pluginId);
+ if (parentWorkflow != null) {
+ parentWorkflow.setVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN,
pluginId);
+ }
+
+ Encr.init(pluginId, this);
+ updateMetadataProviderEncoder(Encr.getEncoder());
+
+ logBasic(
+ BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.Log.EncoderInitialized", pluginId));
+ result.setResult(true);
+ result.setNrErrors(0);
+ } catch (Exception e) {
+ logError(BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.Error.Failed"), e);
+ result.setResult(false);
+ result.setNrErrors(1);
+ }
+
+ return result;
+ }
+
+ private void updateMetadataProviderEncoder(ITwoWayPasswordEncoder encoder) {
+ if (encoder == null) {
+ return;
+ }
+ IHopMetadataProvider provider = getMetadataProvider();
+ applyEncoder(provider, encoder);
+
+ if (getParentWorkflow() != null) {
+ applyEncoder(getParentWorkflow().getMetadataProvider(), encoder);
+ }
+ }
+
+ private void applyEncoder(IHopMetadataProvider provider,
ITwoWayPasswordEncoder encoder) {
+ if (provider == null) {
+ return;
+ }
+ if (provider instanceof MultiMetadataProvider multi) {
+ multi.setTwoWayPasswordEncoder(encoder);
+ if (multi.getProviders() != null) {
+ for (IHopMetadataProvider nested : multi.getProviders()) {
+ applyEncoder(nested, encoder);
+ }
+ }
+ } else if (provider instanceof JsonMetadataProvider json) {
+ json.setTwoWayPasswordEncoder(encoder);
+ } else if (provider instanceof MemoryMetadataProvider memory) {
+ memory.setTwoWayPasswordEncoder(encoder);
+ }
+ }
+
+ @Override
+ public boolean isEvaluation() {
+ return true;
+ }
+
+ @Override
+ public boolean isUnconditional() {
+ return false;
+ }
+
+ public String getEncoderPluginId() {
+ return encoderPluginId;
+ }
+
+ public void setEncoderPluginId(String encoderPluginId) {
+ this.encoderPluginId = encoderPluginId;
+ }
+
+ public String getKeyVariable() {
+ return keyVariable;
+ }
+
+ public void setKeyVariable(String keyVariable) {
+ this.keyVariable = keyVariable;
+ }
+
+ public String getKeyFile() {
+ return keyFile;
+ }
+
+ public void setKeyFile(String keyFile) {
+ this.keyFile = keyFile;
+ }
+}
diff --git
a/plugins/actions/setpasswordencoder/src/main/java/org/apache/hop/workflow/actions/setpasswordencoder/ActionSetPasswordEncoderDialog.java
b/plugins/actions/setpasswordencoder/src/main/java/org/apache/hop/workflow/actions/setpasswordencoder/ActionSetPasswordEncoderDialog.java
new file mode 100644
index 0000000000..9460bb6139
--- /dev/null
+++
b/plugins/actions/setpasswordencoder/src/main/java/org/apache/hop/workflow/actions/setpasswordencoder/ActionSetPasswordEncoderDialog.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.workflow.actions.setpasswordencoder;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.workflow.action.ActionDialog;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.IAction;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Dialog for {@link ActionSetPasswordEncoder}. */
+public class ActionSetPasswordEncoderDialog extends ActionDialog {
+ private static final Class<?> PKG = ActionSetPasswordEncoder.class;
+
+ private ActionSetPasswordEncoder action;
+ private boolean changed;
+
+ private Combo wEncoderPluginId;
+ private TextVar wKeyVariable;
+ private TextVar wKeyFile;
+
+ public ActionSetPasswordEncoderDialog(
+ Shell parent,
+ ActionSetPasswordEncoder action,
+ WorkflowMeta workflowMeta,
+ IVariables variables) {
+ super(parent, workflowMeta, variables);
+ this.action = action;
+ if (this.action.getName() == null) {
+ this.action.setName(BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.Name.Default"));
+ }
+ }
+
+ @Override
+ public IAction open() {
+ createShell(BaseMessages.getString(PKG, "ActionSetPasswordEncoder.Title"),
action);
+ buildButtonBar().ok(e -> ok()).cancel(e -> cancel()).build();
+
+ changed = action.hasChanged();
+ ModifyListener lsMod = e -> action.setChanged();
+
+ // Encoder plugin ID
+ Label wlEncoderPluginId = new Label(shell, SWT.RIGHT);
+ wlEncoderPluginId.setText(
+ BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.EncoderPluginId.Label"));
+ PropsUi.setLook(wlEncoderPluginId);
+ FormData fdlEncoderPluginId = new FormData();
+ fdlEncoderPluginId.left = new FormAttachment(0, 0);
+ fdlEncoderPluginId.right = new FormAttachment(middle, -margin);
+ fdlEncoderPluginId.top = new FormAttachment(wSpacer, margin);
+ wlEncoderPluginId.setLayoutData(fdlEncoderPluginId);
+
+ wEncoderPluginId = new Combo(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ wEncoderPluginId.setItems(new String[] {"Hop", "AES2", "AES"});
+ PropsUi.setLook(wEncoderPluginId);
+ wEncoderPluginId.addModifyListener(lsMod);
+ FormData fdEncoderPluginId = new FormData();
+ fdEncoderPluginId.left = new FormAttachment(middle, 0);
+ fdEncoderPluginId.top = new FormAttachment(wlEncoderPluginId, 0,
SWT.CENTER);
+ fdEncoderPluginId.right = new FormAttachment(100, 0);
+ wEncoderPluginId.setLayoutData(fdEncoderPluginId);
+
+ // Key variable name
+ Label wlKeyVariable = new Label(shell, SWT.RIGHT);
+ wlKeyVariable.setText(
+ BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.KeyVariable.Label"));
+ PropsUi.setLook(wlKeyVariable);
+ FormData fdlKeyVariable = new FormData();
+ fdlKeyVariable.left = new FormAttachment(0, 0);
+ fdlKeyVariable.top = new FormAttachment(wEncoderPluginId, margin);
+ fdlKeyVariable.right = new FormAttachment(middle, -margin);
+ wlKeyVariable.setLayoutData(fdlKeyVariable);
+
+ wKeyVariable = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wKeyVariable);
+ wKeyVariable.addModifyListener(lsMod);
+ FormData fdKeyVariable = new FormData();
+ fdKeyVariable.left = new FormAttachment(middle, 0);
+ fdKeyVariable.top = new FormAttachment(wEncoderPluginId, margin);
+ fdKeyVariable.right = new FormAttachment(100, 0);
+ wKeyVariable.setLayoutData(fdKeyVariable);
+ wKeyVariable.setToolTipText(
+ BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.KeyVariable.Tooltip"));
+
+ // Key file
+ Label wlKeyFile = new Label(shell, SWT.RIGHT);
+ wlKeyFile.setText(BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.KeyFile.Label"));
+ PropsUi.setLook(wlKeyFile);
+ FormData fdlKeyFile = new FormData();
+ fdlKeyFile.left = new FormAttachment(0, 0);
+ fdlKeyFile.top = new FormAttachment(wKeyVariable, margin);
+ fdlKeyFile.right = new FormAttachment(middle, -margin);
+ wlKeyFile.setLayoutData(fdlKeyFile);
+
+ wKeyFile = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wKeyFile);
+ wKeyFile.addModifyListener(lsMod);
+ FormData fdKeyFile = new FormData();
+ fdKeyFile.left = new FormAttachment(middle, 0);
+ fdKeyFile.top = new FormAttachment(wKeyVariable, margin);
+ fdKeyFile.right = new FormAttachment(100, 0);
+ wKeyFile.setLayoutData(fdKeyFile);
+ wKeyFile.setToolTipText(
+ BaseMessages.getString(PKG,
"ActionSetPasswordEncoder.KeyFile.Tooltip"));
+
+ getData();
+ focusActionName();
+
+ BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+
+ return action;
+ }
+
+ public void getData() {
+ wName.setText(Const.nullToEmpty(action.getName()));
+ wEncoderPluginId.setText(Const.NVL(action.getEncoderPluginId(), "AES2"));
+ wKeyVariable.setText(Const.NVL(action.getKeyVariable(),
Const.HOP_AES_ENCODER_KEY));
+ wKeyFile.setText(Const.nullToEmpty(action.getKeyFile()));
+ }
+
+ @Override
+ protected void onActionNameModified() {
+ action.setChanged();
+ }
+
+ private void cancel() {
+ action.setChanged(changed);
+ action = null;
+ dispose();
+ }
+
+ private void ok() {
+ if (Utils.isEmpty(wName.getText())) {
+ MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+ mb.setText(BaseMessages.getString(PKG,
"System.TransformActionNameMissing.Title"));
+ mb.setMessage(BaseMessages.getString(PKG,
"System.ActionNameMissing.Msg"));
+ mb.open();
+ return;
+ }
+ action.setName(wName.getText());
+ action.setEncoderPluginId(wEncoderPluginId.getText());
+ action.setKeyVariable(wKeyVariable.getText());
+ action.setKeyFile(wKeyFile.getText());
+ dispose();
+ }
+}
diff --git
a/plugins/actions/setpasswordencoder/src/main/resources/SetPasswordEncoder.svg
b/plugins/actions/setpasswordencoder/src/main/resources/SetPasswordEncoder.svg
new file mode 100644
index 0000000000..53d9dd50bc
--- /dev/null
+++
b/plugins/actions/setpasswordencoder/src/main/resources/SetPasswordEncoder.svg
@@ -0,0 +1,24 @@
+<?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.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ width="42px" height="42px" viewBox="0 0 42 42" enable-background="new 0 0
42 42">
+ <rect x="10" y="18" width="22" height="16" rx="2" ry="2" fill="#3D6480"/>
+ <path d="M14,18 v-4 c0-3.3,2.7-6,6-6 s6,2.7,6,6 v4" fill="none"
stroke="#3D6480" stroke-width="2.5" stroke-linecap="round"/>
+ <circle cx="21" cy="26" r="2.5" fill="#EFF9FE"/>
+ <rect x="20" y="27.5" width="2" height="4" fill="#EFF9FE"/>
+</svg>
diff --git
a/plugins/actions/setpasswordencoder/src/main/resources/org/apache/hop/workflow/actions/setpasswordencoder/messages/messages_en_US.properties
b/plugins/actions/setpasswordencoder/src/main/resources/org/apache/hop/workflow/actions/setpasswordencoder/messages/messages_en_US.properties
new file mode 100644
index 0000000000..33e527601d
--- /dev/null
+++
b/plugins/actions/setpasswordencoder/src/main/resources/org/apache/hop/workflow/actions/setpasswordencoder/messages/messages_en_US.properties
@@ -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
+#
+# 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.
+#
+#
+
+ActionSetPasswordEncoder.Name=Set password encoder
+ActionSetPasswordEncoder.Name.Default=Set password encoder
+ActionSetPasswordEncoder.Description=Enable or switch the process-global
two-way password encoder from this point in the workflow
+ActionSetPasswordEncoder.keyword=password,encoder,AES,AES2,encrypt,security
+ActionSetPasswordEncoder.Title=Set password encoder
+ActionSetPasswordEncoder.EncoderPluginId.Label=Encoder plugin ID
+ActionSetPasswordEncoder.KeyVariable.Label=Key variable name
+ActionSetPasswordEncoder.KeyVariable.Tooltip=Name of the variable that holds
the AES/AES2 key. Default is HOP_AES_ENCODER_KEY. Do not put the raw key in the
workflow.
+ActionSetPasswordEncoder.KeyFile.Label=Key file path
+ActionSetPasswordEncoder.KeyFile.Tooltip=Optional path to a file containing
the AES/AES2 key (supports variables). Used when the key variable is empty.
+ActionSetPasswordEncoder.Log.EncoderInitialized=Two-way password encoder
initialized with plugin ID ''{0}'' (process-global)
+ActionSetPasswordEncoder.Error.PluginIdMissing=Encoder plugin ID is required
+ActionSetPasswordEncoder.Error.Failed=Error initializing the two-way password
encoder
diff --git a/plugins/actions/setpasswordencoder/src/main/resources/version.xml
b/plugins/actions/setpasswordencoder/src/main/resources/version.xml
new file mode 100644
index 0000000000..36ab20e22e
--- /dev/null
+++ b/plugins/actions/setpasswordencoder/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?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.
+ ~
+ -->
+
+<version>${project.version}</version>
diff --git
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
index 28421486a4..bcfd713469 100644
---
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
+++
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
@@ -34,12 +34,13 @@ import
org.apache.hop.core.encryption.TwoWayPasswordEncoderPlugin;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.core.util.StringUtil;
+import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.variables.Variables;
/**
* We expect a few variables to be set for this plugin to be picked up: 1.
- * HOP_PASSWORD_ENCODER_PLUGIN set to the ID of this plugin:"AES" 2.
HOP_AES_ENCODER_KEY set to the
- * key of your choice.
+ * HOP_PASSWORD_ENCODER_PLUGIN set to the ID of this plugin: "AES2" 2.
HOP_AES_ENCODER_KEY set to
+ * the key of your choice, or HOP_AES_ENCODER_KEY_FILE pointing at a key file.
*/
@TwoWayPasswordEncoderPlugin(
id = "AES2",
@@ -55,33 +56,25 @@ public class Aes2TwoWayPasswordEncoder implements
ITwoWayPasswordEncoder {
@Override
public void init() throws HopException {
+ init(Variables.getADefaultVariableSpace());
+ }
+ @Override
+ public void init(IVariables variables) throws HopException {
try {
- String aesKey = System.getProperty(VARIABLE_HOP_AES_ENCODER_KEY, null);
- if (StringUtils.isEmpty(aesKey)) {
- noKeySpecified();
- }
- String realAesKey = Variables.getADefaultVariableSpace().resolve(aesKey);
- if (StringUtils.isEmpty(realAesKey)) {
- noKeySpecified();
- }
+ String realAesKey = AesEncoderKeyUtil.resolveKey(variables);
byte[] key = realAesKey.getBytes(StandardCharsets.UTF_8);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
byte[] digestKey = messageDigest.digest(key);
byte[] copiedKey = Arrays.copyOf(digestKey, 16);
secretKeySpec = new SecretKeySpec(copiedKey, "AES");
+ } catch (HopException e) {
+ throw e;
} catch (Exception e) {
throw new HopException("Error initializing AES password encoder plugin",
e);
}
}
- private void noKeySpecified() throws HopException {
- throw new HopException(
- "Please specify a key to encrypt/decrypt with by setting variable "
- + VARIABLE_HOP_AES_ENCODER_KEY
- + " in the system properties");
- }
-
@Override
public String encode(String password) {
return encode(password, true);
diff --git
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesEncoderKeyUtil.java
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesEncoderKeyUtil.java
new file mode 100644
index 0000000000..0d8360270f
--- /dev/null
+++
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesEncoderKeyUtil.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.hop.passwords.aes;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.vfs.HopVfs;
+
+/**
+ * Shared key material resolution for AES / AES2 password encoders.
+ *
+ * <p>Order: {@code HOP_AES_ENCODER_KEY} (variables then system property),
then contents of {@code
+ * HOP_AES_ENCODER_KEY_FILE}.
+ */
+final class AesEncoderKeyUtil {
+
+ private AesEncoderKeyUtil() {
+ // utility
+ }
+
+ static String resolveKey(IVariables variables) throws HopException {
+ if (variables == null) {
+ variables = Variables.getADefaultVariableSpace();
+ }
+
+ String key =
firstNonEmpty(variables.getVariable(Const.HOP_AES_ENCODER_KEY));
+ if (StringUtils.isEmpty(key)) {
+ key = firstNonEmpty(System.getProperty(Const.HOP_AES_ENCODER_KEY));
+ }
+ if (StringUtils.isNotEmpty(key)) {
+ String resolved = variables.resolve(key);
+ if (StringUtils.isNotEmpty(resolved)) {
+ return resolved;
+ }
+ }
+
+ String keyFile =
firstNonEmpty(variables.getVariable(Const.HOP_AES_ENCODER_KEY_FILE));
+ if (StringUtils.isEmpty(keyFile)) {
+ keyFile =
firstNonEmpty(System.getProperty(Const.HOP_AES_ENCODER_KEY_FILE));
+ }
+ if (StringUtils.isNotEmpty(keyFile)) {
+ String resolvedPath = variables.resolve(keyFile);
+ if (StringUtils.isNotEmpty(resolvedPath)) {
+ return readKeyFile(resolvedPath);
+ }
+ }
+
+ throw new HopException(
+ "Please specify a key to encrypt/decrypt with by setting variable "
+ + Const.HOP_AES_ENCODER_KEY
+ + " or "
+ + Const.HOP_AES_ENCODER_KEY_FILE
+ + " (system properties or project/environment variables)");
+ }
+
+ private static String readKeyFile(String path) throws HopException {
+ try (InputStream inputStream = HopVfs.getInputStream(path)) {
+ byte[] bytes = inputStream.readAllBytes();
+ String content = new String(bytes, StandardCharsets.UTF_8);
+ // Trim trailing newlines common in secret files; do not trim
leading/internal spaces.
+ return content.replaceAll("[\\r\\n]+$", "");
+ } catch (Exception e) {
+ throw new HopException("Error reading AES encoder key from file '" +
path + "'", e);
+ }
+ }
+
+ private static String firstNonEmpty(String value) {
+ return StringUtils.isEmpty(value) ? null : value;
+ }
+}
diff --git
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoder.java
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoder.java
index e360637eee..c9921c1087 100644
---
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoder.java
+++
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoder.java
@@ -32,12 +32,13 @@ import
org.apache.hop.core.encryption.TwoWayPasswordEncoderPlugin;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.core.util.StringUtil;
+import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.variables.Variables;
/**
* @deprecated We expect a few variables to be set for this plugin to be
picked up: 1.
* HOP_PASSWORD_ENCODER_PLUGIN set to the ID of this plugin:"AES" 2.
HOP_AES_ENCODER_KEY set to
- * the key of your choice.
+ * the key of your choice, or HOP_AES_ENCODER_KEY_FILE pointing at a key
file.
*/
@TwoWayPasswordEncoderPlugin(
id = "AES",
@@ -56,16 +57,13 @@ public class AesTwoWayPasswordEncoder implements
ITwoWayPasswordEncoder {
@Override
public void init() throws HopException {
+ init(Variables.getADefaultVariableSpace());
+ }
+ @Override
+ public void init(IVariables variables) throws HopException {
try {
- String aesKey = System.getProperty(VARIABLE_HOP_AES_ENCODER_KEY, null);
- if (StringUtils.isEmpty(aesKey)) {
- noKeySpecified();
- }
- String realAesKey = Variables.getADefaultVariableSpace().resolve(aesKey);
- if (StringUtils.isEmpty(realAesKey)) {
- noKeySpecified();
- }
+ String realAesKey = AesEncoderKeyUtil.resolveKey(variables);
byte[] key = realAesKey.getBytes(StandardCharsets.UTF_8);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
byte[] digestKey = messageDigest.digest(key);
@@ -79,18 +77,13 @@ public class AesTwoWayPasswordEncoder implements
ITwoWayPasswordEncoder {
decryptCipher = Cipher.getInstance(AES_ALGORITHM);
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
+ } catch (HopException e) {
+ throw e;
} catch (Exception e) {
throw new HopException("Error initializing AES password encoder plugin",
e);
}
}
- private void noKeySpecified() throws HopException {
- throw new HopException(
- "Please specify a key to encrypt/decrypt with by setting variable "
- + VARIABLE_HOP_AES_ENCODER_KEY
- + " in the system properties");
- }
-
@Override
public String encode(String password) {
return encode(password, true);
diff --git
a/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoderTest.java
b/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoderTest.java
index 226fbf36f9..35e1dca924 100644
---
a/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoderTest.java
+++
b/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/aes/AesTwoWayPasswordEncoderTest.java
@@ -23,16 +23,22 @@ import static
org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Const;
import org.apache.hop.core.HopEnvironment;
import org.apache.hop.core.database.DatabaseMeta;
import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.encryption.HopTwoWayPasswordEncoder;
import org.apache.hop.core.encryption.ITwoWayPasswordEncoder;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.metadata.SerializableMetadataProvider;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
import org.apache.hop.metadata.api.IHopMetadataSerializer;
import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -40,6 +46,8 @@ class AesTwoWayPasswordEncoderTest {
private ITwoWayPasswordEncoder encoder;
+ private static final String TEST_AES_KEY = "<TheKeyForTheseTestsHere!!>";
+
private static final String[] TEST_PASSWORDS = {
"MySillyButGoodPassword!", "", null, "abcd", "${DB_PASSWORD}"
};
@@ -47,12 +55,23 @@ class AesTwoWayPasswordEncoderTest {
@BeforeEach
void setup() throws Exception {
System.setProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN, "AES");
- System.setProperty(
- AesTwoWayPasswordEncoder.VARIABLE_HOP_AES_ENCODER_KEY,
"<TheKeyForTheseTestsHere!!>");
+ System.setProperty(AesTwoWayPasswordEncoder.VARIABLE_HOP_AES_ENCODER_KEY,
TEST_AES_KEY);
+ System.clearProperty(Const.HOP_AES_ENCODER_KEY_FILE);
HopEnvironment.init();
+ // HopEnvironment.init() only runs once; re-bind the encoder for each test.
+ Encr.init("AES");
encoder = Encr.getEncoder();
}
+ @AfterEach
+ void tearDown() throws Exception {
+ // Restore a default Hop encoder so other tests in the same Surefire fork
are not polluted.
+ System.setProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN, "Hop");
+ System.clearProperty(Const.HOP_AES_ENCODER_KEY);
+ System.clearProperty(Const.HOP_AES_ENCODER_KEY_FILE);
+ Encr.init("Hop");
+ }
+
@Test
void testInit() {
// See if we've picked up the plugin
@@ -129,7 +148,7 @@ class AesTwoWayPasswordEncoderTest {
String password = "My Password";
String encoded1 = Encr.encryptPasswordIfNotUsingVariables(password);
- String decoded1 = Encr.decryptPasswordOptionallyEncrypted(password);
+ String decoded1 = Encr.decryptPasswordOptionallyEncrypted(encoded1);
assertEquals(password, decoded1);
@@ -140,10 +159,64 @@ class AesTwoWayPasswordEncoderTest {
Encr.init("AES");
String encoded2 = Encr.encryptPasswordIfNotUsingVariables(password);
- String decoded2 = Encr.decryptPasswordOptionallyEncrypted(password);
+ String decoded2 = Encr.decryptPasswordOptionallyEncrypted(encoded2);
assertEquals(password, decoded2);
assertNotEquals(encoded1, encoded2);
}
+
+ @Test
+ void testInitFromVariablesWithKey() throws Exception {
+ IVariables variables = new Variables();
+ variables.setVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN, "AES");
+ variables.setVariable(Const.HOP_AES_ENCODER_KEY,
"VariableSpaceKeyForAesTests!!");
+ // Ensure system property key is different so we prove variables win
+ System.setProperty(Const.HOP_AES_ENCODER_KEY,
"system-property-key-should-not-win");
+
+ Encr.initFromVariables(variables);
+ assertEquals(AesTwoWayPasswordEncoder.class, Encr.getEncoder().getClass());
+
+ String password = "VariableKeyPassword";
+ String encoded = Encr.encryptPasswordIfNotUsingVariables(password);
+ assertTrue(encoded.startsWith(AesTwoWayPasswordEncoder.AES_PREFIX));
+ assertEquals(password, Encr.decryptPasswordOptionallyEncrypted(encoded));
+ }
+
+ @Test
+ void testInitFromKeyFile() throws Exception {
+ Path keyFile = Files.createTempFile("hop-aes-key-", ".txt");
+ try {
+ Files.writeString(keyFile, "KeyFromFileForAesTests!!\n");
+ IVariables variables = new Variables();
+ variables.setVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN, "AES");
+ // Prefer key file when key var empty: clear variable/system key for
this test
+ System.clearProperty(Const.HOP_AES_ENCODER_KEY);
+ variables.setVariable(Const.HOP_AES_ENCODER_KEY_FILE,
keyFile.toAbsolutePath().toString());
+
+ Encr.initFromVariables(variables);
+ assertEquals(AesTwoWayPasswordEncoder.class,
Encr.getEncoder().getClass());
+
+ String password = "FileKeyPassword";
+ String encoded = Encr.encryptPasswordIfNotUsingVariables(password);
+ assertEquals(password, Encr.decryptPasswordOptionallyEncrypted(encoded));
+ } finally {
+ Files.deleteIfExists(keyFile);
+ }
+ }
+
+ @Test
+ void testReinitToHopObfuscation() throws Exception {
+ String aesEncoded =
Encr.encryptPasswordIfNotUsingVariables("OnlyValidWithAes");
+ assertTrue(aesEncoded.startsWith(AesTwoWayPasswordEncoder.AES_PREFIX));
+
+ IVariables variables = new Variables();
+ variables.setVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN, "Hop");
+ Encr.initFromVariables(variables);
+
+ assertEquals(HopTwoWayPasswordEncoder.class, Encr.getEncoder().getClass());
+ String hopEncoded = Encr.encryptPasswordIfNotUsingVariables("hop-pass");
+ assertTrue(hopEncoded.startsWith(Encr.PASSWORD_ENCRYPTED_PREFIX));
+ assertEquals("hop-pass",
Encr.decryptPasswordOptionallyEncrypted(hopEncoded));
+ }
}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
index 651d572e22..d6a6e70971 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
@@ -25,6 +25,7 @@ import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.hop.core.Const;
import org.apache.hop.core.DbCache;
+import org.apache.hop.core.encryption.Encr;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.extension.ExtensionPointHandler;
import org.apache.hop.core.logging.ILogChannel;
@@ -94,6 +95,29 @@ public class ProjectsUtil {
//
project.modifyVariables(variables, projectConfig, configurationFiles,
environmentName);
+ // Re-bind the process-global two-way password encoder from
project/environment variables
+ // (HOP_PASSWORD_ENCODER_PLUGIN, HOP_AES_ENCODER_KEY /
HOP_AES_ENCODER_KEY_FILE). This resets
+ // AES keys between projects and allows falling back to Hop obfuscation
when unset.
+ //
+ try {
+ Encr.initFromVariables(variables);
+ String encoderPluginId =
+ Const.NVL(
+ variables.getVariable(Const.HOP_PASSWORD_ENCODER_PLUGIN),
+ Const.NVL(System.getProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN),
"Hop"));
+ if (log.isBasic()) {
+ log.logBasic(
+ "Two-way password encoder initialized with plugin ID '"
+ + encoderPluginId
+ + "' for project '"
+ + projectName
+ + "'");
+ }
+ } catch (HopException e) {
+ throw new HopException(
+ "Error initializing the two-way password encoder for project '" +
projectName + "'", e);
+ }
+
// Change the metadata provider in the GUI
//
if (hasHopMetadataProvider != null) {