Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b1611dc
Adding support of Secret Manager framework support in Apache OFBiz. F…
ashishvijaywargiya Jun 7, 2026
348d346
Adding the support of AES Encryption based password. Also added the g…
ashishvijaywargiya Jun 8, 2026
dc09410
Testing with aws secret manager has been completed. I tested it with …
ashishvijaywargiya Jun 9, 2026
653ee97
Changing -Pvalue to -PdbPassword for clarity.
ashishvijaywargiya Jun 9, 2026
2d6aa95
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 10, 2026
fd38a3c
Added the fallback support in the code base. If remote services of se…
ashishvijaywargiya Jun 10, 2026
b9d4683
Adding the fallback option for all the plugins. And also improved con…
ashishvijaywargiya Jun 11, 2026
77e264e
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 11, 2026
3a75ccf
Improved: Use Shiro's AesCipherService for ConfigCryptoUtil encryption
ashishvijaywargiya Jun 11, 2026
f9520a5
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 11, 2026
4de4287
Adding a very important feature which I envisioned before I started t…
ashishvijaywargiya Jun 12, 2026
bdc41a1
Adding the two files SecretValueResolver.java and SecretValueResolver…
ashishvijaywargiya Jun 12, 2026
a490fd7
Committing the missed build.gradle file in which I added a new gradle…
ashishvijaywargiya Jun 12, 2026
229d62b
I renamed the gradle task and also renamed the master key to OFBIZ_MA…
ashishvijaywargiya Jun 13, 2026
63d4f7d
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 14, 2026
400b9e1
Add masked stdin prompts for generateDBPassword and generateEncrypted…
ashishvijaywargiya Jun 14, 2026
b4d6060
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 16, 2026
723ca7f
Adding a support of Secret Manager screen to add encrypted secrets an…
ashishvijaywargiya Jun 16, 2026
50ab873
The secret-value marker (default: LOOKUP, was SECRET) is now configur…
ashishvijaywargiya Jun 16, 2026
7fcf87a
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 16, 2026
dfcf653
Fixing the following points:
ashishvijaywargiya Jun 17, 2026
4949fca
Fix EncryptValue screen 500 error and usage-stats rendering
ashishvijaywargiya Jun 17, 2026
5477136
Fixing formatting issues.
ashishvijaywargiya Jun 17, 2026
9d21423
Fixing formatting issues.
ashishvijaywargiya Jun 17, 2026
5717a3f
Fixing console errors for FTL file.
ashishvijaywargiya Jun 17, 2026
a6962d6
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 17, 2026
abc1cb0
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 22, 2026
6da2979
Add automatic secret-rotation sync for SystemProperty-backed secrets
ashishvijaywargiya Jun 22, 2026
faea278
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 25, 2026
d9152f0
Add per-key alias overrides and shared helpers for secret-provider pl…
ashishvijaywargiya Jun 25, 2026
c6af8db
Add SecretAuditLog entity and async audit trail for secret access events
ashishvijaywargiya Jun 27, 2026
86ee418
Fixing the build failure related to code formatting.
ashishvijaywargiya Jun 27, 2026
4f48017
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 27, 2026
4f69f62
Add DENIED outcome audit events for all SECRET_MAINT and SECRET_AUDIT…
ashishvijaywargiya Jun 27, 2026
2a919c7
Add ROTATION_POLL summary audit event at start/end of each scheduled …
ashishvijaywargiya Jun 27, 2026
e6622ad
Wire SecretAuditContainer to pass the configured delegator name to Se…
ashishvijaywargiya Jun 27, 2026
73a58ba
Add provider health-status indicator and Secret Usage Statistics screen
ashishvijaywargiya Jun 27, 2026
d8cb9e5
Following the best practices in Groovy file.
ashishvijaywargiya Jun 27, 2026
42cfc85
Add reEncryptAllSecrets Gradle task for master-key rotation
ashishvijaywargiya Jun 27, 2026
efcfd18
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 28, 2026
285dc92
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jun 30, 2026
1642e1d
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jul 5, 2026
b5decc1
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jul 14, 2026
c0e82ad
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jul 20, 2026
4b54144
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jul 20, 2026
ef38c78
Removing the mysql driver that I mistakenly committed.
ashishvijaywargiya Jul 20, 2026
eeb9df1
Merge branch 'apache:trunk' into various-secret-manager
ashishvijaywargiya Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ tasks.withType(JavaCompile) {
// Enables Zip larger than 4 GB and more than 65535 entries
tasks.withType(Zip) { zip64 = true }

// Multiple SecretProvider plugins each contribute the same META-INF/services/ filename.
// Only one provider should be enabled at a time (via ofbiz-component.xml enabled="true/false").
// FIRST keeps the alphabetically first file during transitional states where two are temporarily enabled.
processResources {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

// Only used for release branches
def getCurrentGitBranch() {
return "git branch --show-current".execute().text.trim()
Expand Down Expand Up @@ -742,6 +749,113 @@ task gitInfoFooter(group: sysadminGroup, description: 'Update the Git Branch-rev
}
}

// System.console() is unreliable under Gradle (it is almost always null, even with
// --console=plain), so read directly from stdin instead. This still requires --no-daemon,
// since the Gradle daemon is detached from the terminal and stdin is not forwarded to it.
def stdinReader = new BufferedReader(new InputStreamReader(System.in))

// Reads a non-sensitive value (e.g. a lookup key) from stdin.
def promptValue = { String prompt ->
print prompt
System.out.flush()
def line = stdinReader.readLine()
if (!line) {
throw new GradleException("No value entered. Re-run with --no-daemon from an interactive "
+ "terminal, or pass the value via -P.")
}
line
}

// Reads a sensitive value (password/master key) from stdin, masking the terminal echo via
// `stty` (when a tty is available) so it never appears in shell history, `ps`, or CI logs.
def promptSecret = { String prompt ->
print prompt
System.out.flush()
def ttyAvailable = new File('/dev/tty').exists()
if (ttyAvailable) {
['sh', '-c', 'stty -echo < /dev/tty'].execute().waitFor()
}
try {
def line = stdinReader.readLine()
if (!line) {
throw new GradleException("No value entered. Re-run with --no-daemon from an interactive "
+ "terminal, or pass the value via -P.")
}
return line
} finally {
if (ttyAvailable) {
['sh', '-c', 'stty echo < /dev/tty'].execute().waitFor()
}
println()
}
}

task generateDBPassword(group: sysadminGroup,
description: 'Write a plain or encrypted (ENC(...)) database password into passwords.properties. '
+ 'Usage: ./gradlew generateDBPassword -PdbPassword=<password> -PlookupKey=<e.g. mysql-ofbiz> [-PmasterKey=<key>]. '
+ 'If -PdbPassword is omitted, you will be prompted for it via masked console input '
+ '(run with --no-daemon --console=plain). The master key is taken from -PmasterKey, '
+ 'falling back to the OFBIZ_MASTER_KEY environment variable.') {
doLast {
def secret = project.hasProperty('dbPassword') ? project.property('dbPassword')
: promptSecret('Enter database password: ')
def masterKey = project.hasProperty('masterKey') ? project.property('masterKey') : System.getenv('OFBIZ_MASTER_KEY')
if (masterKey) {
def output = new java.io.ByteArrayOutputStream()
javaexec {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'org.apache.ofbiz.base.crypto.ConfigCryptoUtil'
args masterKey, secret
standardOutput = output
}
secret = output.toString().trim()
}
def propertyName = "jdbc-password.${project.property('lookupKey')}"
def newLine = "${propertyName}=${secret}"
def passwordsFile = file('framework/base/config/passwords.properties')
def lines = passwordsFile.readLines()
def index = lines.findIndexOf { it.startsWith("${propertyName}=") }
if (index >= 0) lines[index] = newLine else lines << newLine
passwordsFile.text = lines.join(System.lineSeparator()) + System.lineSeparator()
println "Stored ${newLine} in passwords.properties"
}
}

task generateEncryptedSecret(group: sysadminGroup,
description: 'Generate an encrypted secret (ENC(...)) and its systemPropertyLookup marker, for pasting '
+ 'into a SystemProperty record (e.g. payment/SMS/shipment gateway credentials). '
+ 'Usage: ./gradlew generateEncryptedSecret -PsecretPassword=<value> -PlookupKey=<key> -PmasterKey=<key>. '
+ 'Any of -PsecretPassword/-PlookupKey/-PmasterKey may be omitted to be prompted for '
+ 'interactively (run with --no-daemon --console=plain); the master key also falls back '
+ 'to the OFBIZ_MASTER_KEY environment variable.') {
doLast {
def secret = project.hasProperty('secretPassword') ? project.property('secretPassword')
: promptSecret('Enter secret value: ')
def lookupKey = project.hasProperty('lookupKey') ? project.property('lookupKey')
: promptValue('Enter lookup key: ')
def masterKey = project.hasProperty('masterKey') ? project.property('masterKey')
: (System.getenv('OFBIZ_MASTER_KEY') ?: promptSecret('Enter master key: '))

def markerName = 'SECRET'
def generalPropsFile = file('framework/common/config/general.properties')
if (generalPropsFile.exists()) {
def generalProps = new Properties()
generalPropsFile.withInputStream { generalProps.load(it) }
markerName = generalProps.getProperty('secret.value.marker', markerName)
}

def output = new java.io.ByteArrayOutputStream()
javaexec {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'org.apache.ofbiz.base.crypto.ConfigCryptoUtil'
args masterKey, secret
standardOutput = output
}
println "systemPropertyLookup=${markerName}(${lookupKey})"
println "systemPropertyValue=${output.toString().trim()}"
}
}

task generateSecretKeys(group: sysadminGroup,
description: 'Generate cryptographically secure 512-bit (64-char) secret keys for JWT token signing and password encryption, and write them to security.properties') {
doLast {
Expand Down
1 change: 1 addition & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ dependencies {
runtimeOnly 'org.apache.axis2:axis2-transport-http:1.8.2'
runtimeOnly 'org.apache.axis2:axis2-transport-local:1.8.2'
runtimeOnly 'com.h2database:h2:2.4.240'
runtimeOnly 'com.mysql:mysql-connector-j:8.4.0'
Comment thread
ashishvijaywargiya marked this conversation as resolved.
Outdated
runtimeOnly 'org.apache.geronimo.specs:geronimo-jaxrpc_1.1_spec:2.1'
runtimeOnly 'org.apache.logging.log4j:log4j-1.2-api:2.25.4' // for external jars using the old log4j1.2: routes logging to log4j 2
runtimeOnly 'org.apache.logging.log4j:log4j-jul:2.25.4' // for external jars using the java.util.logging: routes logging to log4j 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*******************************************************************************
* 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.ofbiz.base.crypto;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Base64;
import java.util.Properties;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import org.apache.ofbiz.base.util.GeneralException;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.shiro.crypto.CryptoException;
import org.apache.shiro.crypto.cipher.AesCipherService;
import org.apache.shiro.lang.util.ByteSource;

/**
* AES-256-GCM encryption/decryption for configuration values such as the
* database passwords stored in {@code passwords.properties} as {@code ENC(...)}.
*
* <p>The AES key is derived from a master key (e.g. the {@code OFBIZ_MASTER_KEY}
* environment variable) via PBKDF2WithHmacSHA256, so the same master key always
* yields the same AES key. The actual cipher operations are delegated to Shiro's
* {@link AesCipherService} (the same library used by
* {@link org.apache.ofbiz.entity.util.EntityCrypto}), which defaults to GCM mode
* and prepends a random IV to the ciphertext.</p>
*/
public final class ConfigCryptoUtil {

private static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA256";
// Static salt: the master key is the actual secret; the salt only needs to defeat
// precomputed rainbow tables, not provide per-installation uniqueness.
private static final byte[] SALT = "OFBizConfigCryptoUtilSalt".getBytes(StandardCharsets.UTF_8);
private static final int KEY_LENGTH_BITS = 256;

// Note: AesCipherService derives the GCM tag length from getKeySize(), which must stay at
// its default of 128 (a valid GCM tag length). The 256-bit AES key below is passed directly
// to encrypt/decrypt and does not depend on this setting.
private static final AesCipherService CIPHER_SERVICE = new AesCipherService();

// Read config once at class-load time using raw Properties to avoid re-entrancy via
// UtilProperties.getPropertyValue() → SecretValueResolver → ConfigCryptoUtil.
private static final Properties SECURITY_PROPERTIES = UtilProperties.getProperties("security");

/** Name of the environment variable holding the AES master key; configurable via secret.master.key.env.var. */
public static final String MASTER_KEY_ENV_VAR = SECURITY_PROPERTIES != null
? SECURITY_PROPERTIES.getProperty("secret.master.key.env.var", "OFBIZ_MASTER_KEY").trim()
: "OFBIZ_MASTER_KEY";

// PBKDF2 iteration count. Raising this requires re-encrypting all existing ENC(...) values
// because the derived AES key changes when the iteration count changes.
static final int ITERATIONS = readIterations();

private static int readIterations() {
if (SECURITY_PROPERTIES == null) {
return 310000;
}
try {
int v = Integer.parseInt(
SECURITY_PROPERTIES.getProperty("secret.pbkdf2.iterations", "310000").trim());
return v > 0 ? v : 310000;
} catch (NumberFormatException e) {
return 310000;
}
}

private static byte[] deriveKey(String masterKey) throws GeneralSecurityException {
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_ALGORITHM);
PBEKeySpec spec = new PBEKeySpec(masterKey.toCharArray(), SALT, ITERATIONS, KEY_LENGTH_BITS);
try {
return factory.generateSecret(spec).getEncoded();
} finally {
spec.clearPassword();
}
}

/**
* Encrypts {@code plainText} with a key derived from {@code masterKey}.
* @return Base64 string containing the random IV followed by the ciphertext (with GCM auth tag)
*/
public static String encrypt(String plainText, String masterKey) throws GeneralException {
if (masterKey == null || masterKey.isEmpty()) {
throw new GeneralException("masterKey must not be null or empty");
}
try {
byte[] key = deriveKey(masterKey);
ByteSource encrypted = CIPHER_SERVICE.encrypt(plainText.getBytes(StandardCharsets.UTF_8), key);
return encrypted.toBase64();
} catch (GeneralSecurityException | CryptoException e) {
throw new GeneralException("Unable to encrypt value", e);
}
}

/**
* Reverses {@link #encrypt(String, String)}: derives the AES key from {@code masterKey}
* and lets {@link AesCipherService} split the IV from the ciphertext and decrypt.
*/
public static String decrypt(String encryptedBase64, String masterKey) throws GeneralException {
if (masterKey == null || masterKey.isEmpty()) {
throw new GeneralException("masterKey must not be null or empty");
}
try {
byte[] key = deriveKey(masterKey);
byte[] payload = Base64.getDecoder().decode(encryptedBase64);
byte[] decrypted = CIPHER_SERVICE.decrypt(payload, key).getClonedBytes();
return new String(decrypted, StandardCharsets.UTF_8);
} catch (GeneralSecurityException | CryptoException | IllegalArgumentException e) {
throw new GeneralException("Unable to decrypt value: verify the master key is correct", e);
}
}

/**
* Returns the plaintext of {@code rawValue} if it is wrapped in {@code ENC(...)},
* otherwise returns it unchanged.
*
* <p>All {@link org.apache.ofbiz.base.secret.SecretProvider} implementations should
* call this after fetching a value from their remote backend so that operators can
* optionally add a client-side AES-256-GCM layer on top of whatever the vault already
* provides. The master key is read from the {@code OFBIZ_MASTER_KEY} environment variable.</p>
*
* @param rawValue the value as returned by the secret backend (plaintext or {@code ENC(...)})
* @param secretName used only in error messages to identify which secret failed
* @return the plaintext value — either the original string or the decrypted one
* @throws GeneralException if the value is encrypted but {@code OFBIZ_MASTER_KEY} is missing,
* or if decryption fails (e.g. wrong key)
*/
public static String decryptIfEncrypted(String rawValue, String secretName) throws GeneralException {
if (!rawValue.startsWith("ENC(") || !rawValue.endsWith(")")) {
return rawValue;
}
String masterKey = System.getenv(MASTER_KEY_ENV_VAR);
if (masterKey == null || masterKey.isEmpty()) {
throw new GeneralException("Secret '" + secretName + "' is encrypted (ENC(...)) but the "
+ MASTER_KEY_ENV_VAR + " environment variable is not set");
}
String base64 = rawValue.substring("ENC(".length(), rawValue.length() - 1);
try {
return decrypt(base64, masterKey);
} catch (GeneralException e) {
throw new GeneralException("Failed to decrypt secret '" + secretName
+ "': verify OFBIZ_MASTER_KEY matches the key used to encrypt", e);
}
}

private ConfigCryptoUtil() { }

/**
* Command-line helper that prints {@code ENC(<base64>)} for a given master key and plaintext
* value. Invoked via the {@code generateDBPassword} and {@code generateEncryptedSecret}
* Gradle tasks to produce values for {@code passwords.properties}
* ({@code jdbc-password.<lookup-key>=ENC(...)}) and {@code SystemProperty.systemPropertyValue}
* respectively.
*/
public static void main(String[] args) throws GeneralException {
if (args.length != 2) {
System.err.println("Usage: ConfigCryptoUtil <masterKey> <plainTextValue>");
System.exit(1);
return;
}
System.out.println("ENC(" + encrypt(args[1], args[0]) + ")");
}
}
Loading
Loading