Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
a2db1ad
refactor : replace System.getProperty with KeySupplier injection
chhil May 2, 2026
964f25e
fix: harden SystemKeyManager API, fix key rotation docs, support HSM
chhil May 3, 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
180 changes: 180 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,180 @@
/*
* 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 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 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);
String kName = keyName != null && !keyName.isEmpty() ? keyName : "default";
log.warn("Key not found in environment for name: " + kName + ". Please set " + envVarName);
throw new RuntimeException(
"Key not found in environment for name: " + kName + ". Please set " + envVarName);
}
// Decrypt with GCM authentication — pass SecretKey directly to support
// non-extractable keys (HSM/PKCS#11) where getEncoded() returns null
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
byte[] plaintext = cipher.doFinal(ciphertext);

return new String(plaintext, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
log.warn("Failed to decrypt value: invalid Base64 encoding or key length — config=" + config, e);
throw new RuntimeException("Failed to decrypt value: invalid Base64 encoding or key length", e);
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
log.warn("Failed to decrypt value: " + e.getMessage() + " — config=" + config, e);
throw new RuntimeException(
"Failed to decrypt value: " + e.getClass().getSimpleName() + " — " + e.getMessage(), 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);
String kName = keyName != null && !keyName.isEmpty() ? keyName : "default";
log.warn("Key not found in environment for name: " + kName + ". Please set " + envVarName);
throw new IllegalArgumentException(
"Key not found in environment for name: " + kName + ". Please set " + envVarName);
}
// Encrypt with GCM authentication — pass SecretKey directly to support
// non-extractable keys (HSM/PKCS#11) where getEncoded() returns null
// Generate secure random 12-byte IV
byte[] iv = new byte[IV_SIZE_BYTES];
new SecureRandom().nextBytes(iv);

Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, 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 (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
log.warn("Failed to encrypt value: " + e.getMessage() + " — keyName=" + keyName, e);
throw new RuntimeException(
"Failed to encrypt value: " + e.getClass().getSimpleName() + " — " + e.getMessage(), e);
}
}
}
Loading