Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1c10ff2
Add CryptoEnvironmentProvider with AES-256-GCM encryption
chhil Feb 21, 2026
f7b46c2
fix(crypto): use explicit UTF-8 charset for string/bytes conversion
chhil Mar 19, 2026
e695206
feat(core): implement SystemKeyManager for named AES-256-GCM encrypti…
chhil Mar 19, 2026
21c38ae
refactor(crypto): CRYPTO CLI delegates to CryptoEnvironmentProvider.e…
chhil Mar 19, 2026
fadcd6a
refactor(crypto): remove unused clearKeys() no-op stub
chhil Mar 19, 2026
c558681
fix(test): use @TempDir instead of relative paths in DeployWithEncryp…
chhil Mar 19, 2026
d2927f1
refactor(crypto): change encrypted value format from enc::db:: to enc…
chhil Mar 19, 2026
9cce165
refactor(crypto): simplify get() parsing logic in CryptoEnvironmentPr…
chhil Mar 19, 2026
550c617
fix(sonar): address SonarQube issues in crypto classes
chhil Mar 20, 2026
535dc82
style: add trailing newlines to crypto-related files
chhil Mar 20, 2026
0eebe3b
fix(test): add key setup/teardown to SimpleConfigurationWithCryptoTest
chhil Mar 20, 2026
b3a772f
fix(core): remove dead prefix parsing in CryptoEnvironmentProvider
chhil Mar 21, 2026
16d4bd7
fix(core): sanitize env keynames
chhil Mar 21, 2026
e0923ae
fix(core): remove dead crypto prefix parsing and sanitize env keys
chhil Mar 21, 2026
7fa2ac8
fix(cli): tighten argument arity check in CRYPTO command
chhil Mar 21, 2026
f22b334
fix(core): remove static SecureRandom usage to ensure thread safety
chhil Mar 21, 2026
39bfada
fix(core): remove duplicate AES encryption logic from SystemKeyManager
chhil Mar 21, 2026
0d16f68
fix(core): enforce strong key name parsing and explicitly log decode …
chhil Mar 21, 2026
de7cb01
fix(core): demote unused public methods and remove trailing whitespace
chhil Mar 21, 2026
43cedc7
fix(core): improve logging and diagnostic feedback in crypto environm…
chhil Mar 23, 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
174 changes: 174 additions & 0 deletions jpos/src/main/java/org/jpos/core/CryptoEnvironmentProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2026 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.jpos.core;

import org.jpos.util.Log;
import org.jpos.q2.Q2;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

/**
* EnvironmentProvider that encrypts/decrypts values using AES256.
*
* <p>
* Format: {@code enc::<base64-encoded-ciphertext>}
* <ul>
* <li>Algorithm: AES-256-GCM with authenticated encryption</li>
* <li>Key: 256-bit AES key loaded from environment variable via SystemKeyManager</li>
* <li>IV/Nonce: 12 bytes (generated per encryption)</li>
* <li>Authentication: 16-byte GCM tag included in ciphertext</li>
* </ul>
*/
public class CryptoEnvironmentProvider implements EnvironmentProvider {
private static final Log log = Log.getLog(Q2.LOGGER_NAME, "crypto-env-provider");

private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final String ENC_PREFIX = "enc::";
private static final int IV_SIZE_BYTES = 12;
private static final int TAG_LENGTH_BITS = 128;

@Override
public String prefix() {
return ENC_PREFIX;
}

@Override
public String get(String config) {
try {
String keyName = null;
String encoded;

int colonIdx = config.indexOf(':');
if (colonIdx >= 0) {
String possibleKeyName = config.substring(0, colonIdx);
// Valid key names are alphanumeric with optional hyphens/underscores.
// If it doesn't match this pattern, it's probably just part of the Base64 payload.
if (possibleKeyName.matches("^[a-zA-Z0-9_\\-]+$")) {
keyName = possibleKeyName;
encoded = config.substring(colonIdx + 1);
} else {
encoded = config;
}
} else {
encoded = config;
}

byte[] decoded = Base64.getDecoder().decode(encoded);
ByteBuffer buf = ByteBuffer.wrap(decoded);

// First 12 bytes are the IV/nonce
byte[] iv = new byte[IV_SIZE_BYTES];
buf.get(iv);

// Rest is ciphertext with GCM authentication tag
byte[] ciphertext = new byte[buf.remaining()];
buf.get(ciphertext);

// Use SystemKeyManager to get the derived key
SecretKey key = SystemKeyManager.getInstance().getKey(keyName);
if (key == null) {
String envVarName = SystemKeyManager.getInstance().getEnvVarName(keyName);
log.warn("Key not found in environment for name: " +
(keyName != null && !keyName.isEmpty() ? keyName : "default") +
". Please set " + envVarName);
throw new RuntimeException("Key not found in environment for name: " +
(keyName != null && !keyName.isEmpty() ? keyName : "default") +
". Please set " + envVarName);
}
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), ALGORITHM);

// Decrypt with GCM authentication
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
byte[] plaintext = cipher.doFinal(ciphertext);

return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt value", e);
}
}

/**
* Helper method to encrypt a value (for generating encrypted config).
*
* @param value the plaintext value to encrypt
* @return base64-encoded ciphertext with enc:: prefix
*/
public static String encrypt(String value) {
return encrypt(value, null);
}

/**
* Helper method to encrypt a value with a named key.
*
* @param value the plaintext value to encrypt
* @param keyName the name of the key to use (null for default)
* @return base64-encoded ciphertext with enc::keyname: prefix
*/
public static String encrypt(String value, String keyName) {
try {
SecretKey key = SystemKeyManager.getInstance().getKey(keyName);
if (key == null) {
String envVarName = SystemKeyManager.getInstance().getEnvVarName(keyName);
log.warn("Key not found in environment for name: " +
(keyName != null && !keyName.isEmpty() ? keyName : "default") +
". Please set " + envVarName);
throw new IllegalArgumentException("Key not found in environment for name: " +
(keyName != null && !keyName.isEmpty() ? keyName : "default") +
". Please set " + envVarName);
}
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), ALGORITHM);

// Generate secure random 12-byte IV
byte[] iv = new byte[IV_SIZE_BYTES];
new SecureRandom().nextBytes(iv);

// Encrypt with GCM authentication
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
byte[] ciphertext = cipher.doFinal(value.getBytes(StandardCharsets.UTF_8));

// Combine IV and ciphertext
ByteBuffer buf = ByteBuffer.allocate(iv.length + ciphertext.length);
buf.put(iv);
buf.put(ciphertext);

String base64 = Base64.getEncoder().encodeToString(buf.array());

// If keyName is provided, include it in the prefix
if (keyName != null && !keyName.isEmpty()) {
return ENC_PREFIX + keyName + ":" + base64;
} else {
return ENC_PREFIX + base64;
}
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt value", e);
}
}
}
165 changes: 165 additions & 0 deletions jpos/src/main/java/org/jpos/core/SystemKeyManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2026 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.jpos.core;

import org.jpos.util.Log;
import org.jpos.q2.Q2;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

/**
* Manages AES-256 encryption keys loaded from environment variables.
*
* <p>
* This class:
* <ul>
* <li>Loads keys strictly from environment variables (no internal caching)</li>
* <li>Generates new keys using OS-provided SecureRandom (truly random)</li>
* </ul>
*/
public class SystemKeyManager {
private static final Log log = Log.getLog(Q2.LOGGER_NAME, "crypto-env-provider");
private static final String DEFAULT_KEY_NAME = "default";
private static final String DEFAULT_ENV_VAR = "JPOS_ENCRYPTION_KEY";
private static final int KEY_SIZE_BITS = 256;

private static final SystemKeyManager instance;

static {
try {
instance = new SystemKeyManager();
} catch (Exception e) {
throw new RuntimeException("Failed to initialize SystemKeyManager", e);
}
}

private SystemKeyManager() {
}

/**
* Returns the singleton SystemKeyManager instance.
*
* @return the SystemKeyManager instance
*/
public static SystemKeyManager getInstance() {
return instance;
}

/**
* Gets a key by name. Returns null if key doesn't exist in environment.
* Never caches the key.
*
* @param keyName the name of the key to get
* @return the SecretKey, or null if not found
*/
public SecretKey getKey(String keyName) {
if (keyName == null || keyName.isEmpty()) {
keyName = DEFAULT_KEY_NAME;
}

String envVarName = getEnvVarName(keyName);
String envValue = System.getenv(envVarName);

// Fallback for test environments where setting System.getenv is not possible
if (envValue == null || envValue.trim().isEmpty()) {
envValue = System.getProperty(envVarName);
}

if (envValue != null && !envValue.trim().isEmpty()) {
try {
byte[] keyBytes = Base64.getDecoder().decode(envValue.trim());
if (keyBytes.length == KEY_SIZE_BITS / 8) {
return new SecretKeySpec(keyBytes, "AES");
} else {
log.warn("Invalid key length in " + envVarName + ": expected " + (KEY_SIZE_BITS / 8) + " bytes, got " + keyBytes.length);
}
} catch (IllegalArgumentException e) {
log.warn("Invalid Base64 in " + envVarName + ": " + e.getMessage());
}
}

return null;
}

/**
* Gets the default key. Returns null if key doesn't exist.
*
* @return the default SecretKey, or null if not found
*/
SecretKey getDefaultKey() {
return getKey(DEFAULT_KEY_NAME);
}

/**
* Gets the Base64-encoded key by name.
*
* @param keyName the name of the key
* @return Base64-encoded key, or null if not found
*/
String getKeyBase64(String keyName) {
SecretKey key = getKey(keyName);
return key != null ? Base64.getEncoder().encodeToString(key.getEncoded()) : null;
}

/**
* Generates a new random key using OS-provided SecureRandom entropy.
* The user is responsible for setting the environment variable manually.
*
* @param keyName the name to give the key
* @return the generated key in Base64
*/
public String generateKey(String keyName) {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(KEY_SIZE_BITS, new SecureRandom());
SecretKey key = keyGen.generateKey();
return Base64.getEncoder().encodeToString(key.getEncoded());
} catch (Exception e) {
throw new RuntimeException("Failed to generate key", e);
}
}

/**
* Generates a new default key from SystemSeed.
*
* @return the Base64-encoded generated key
*/
public String generateDefaultKey() {
return generateKey(DEFAULT_KEY_NAME);
}

/**
* Gets the environment variable name for a key.
* Non-alphanumeric characters in the key name (including hyphens) are normalized to underscores.
* For example, "my-key" becomes "JPOS_ENCRYPTION_KEY_MY_KEY".
*
* @param keyName the name of the key
* @return the environment variable name
*/
public String getEnvVarName(String keyName) {
if (keyName == null || keyName.isEmpty()) {
keyName = DEFAULT_KEY_NAME;
}
return DEFAULT_ENV_VAR + (DEFAULT_KEY_NAME.equals(keyName) ? "" : "_" + keyName.toUpperCase().replaceAll("[^A-Z0-9]", "_"));
}
}
Loading
Loading