Skip to content

Commit 1813a92

Browse files
committed
feat(core): Implement named key support for AES-256-GCM encryption
- Renamed prefix from crypto:: to enc:: for consistency - Added SystemKeyManager class to manage encryption keys derived from SystemSeed - Implemented named key support (enc::<keyname>::) for multiple encryption contexts - Updated CryptoEnvironmentProvider to use SystemKeyManager for key retrieval - Added CRYPTO CLI command with generate and encrypt subcommands - Added comprehensive tests for named key functionality - Updated documentation in CRYPTO_IMPLEMENTATION.md - Replaced ISOUtil import with direct SystemSeed usage - All tests passing (18 CryptoEnvironmentProviderTest, 4 CRYPTOTest, 13 SystemKeyManagerTest, 1 DeployWithEncryptedPropsTest)
1 parent 1c10ff2 commit 1813a92

File tree

8 files changed

+843
-81
lines changed

8 files changed

+843
-81
lines changed

jpos/src/main/java/org/jpos/core/CryptoEnvironmentProvider.java

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
package org.jpos.core;
2020

21-
import org.jpos.iso.ISOUtil;
2221
import org.jpos.security.SystemSeed;
2322

2423
import javax.crypto.Cipher;
24+
import javax.crypto.SecretKey;
2525
import javax.crypto.spec.GCMParameterSpec;
2626
import javax.crypto.spec.SecretKeySpec;
2727
import java.nio.ByteBuffer;
@@ -32,7 +32,7 @@
3232
* EnvironmentProvider that encrypts/decrypts values using AES256.
3333
*
3434
* <p>
35-
* Format: {@code crypto::<base64-encoded-ciphertext>}
35+
* Format: {@code enc::<base64-encoded-ciphertext>}
3636
* <ul>
3737
* <li>Algorithm: AES-256-GCM with authenticated encryption</li>
3838
* <li>Key: 32 bytes derived from SystemSeed using SHA-256</li>
@@ -49,16 +49,25 @@ public class CryptoEnvironmentProvider implements EnvironmentProvider {
4949

5050
@Override
5151
public String prefix() {
52-
return "crypto::";
52+
return "enc::";
5353
}
5454

5555
@Override
5656
public String get(String config) {
5757
try {
58-
// Remove the crypto:: prefix if present
58+
String keyName = null;
5959
String encoded = config;
60-
if (config.startsWith("crypto::")) {
61-
encoded = config.substring(8);
60+
61+
// Check for key name prefix: enc::keyname::encoded_data
62+
if (config.startsWith("enc::")) {
63+
String[] parts = config.substring(5).split("::", 2);
64+
if (parts.length == 2) {
65+
keyName = parts[0];
66+
encoded = parts[1];
67+
} else {
68+
// Default key
69+
encoded = config.substring(5);
70+
}
6271
}
6372

6473
byte[] decoded = Base64.getDecoder().decode(encoded);
@@ -72,9 +81,12 @@ public String get(String config) {
7281
byte[] ciphertext = new byte[buf.remaining()];
7382
buf.get(ciphertext);
7483

75-
// Use first 32 bytes of SystemSeed as the 256-bit AES key
76-
byte[] seed = SystemSeed.getSeed(0, 32);
77-
SecretKeySpec keySpec = new SecretKeySpec(seed, ALGORITHM);
84+
// Use SystemKeyManager to get the derived key
85+
SecretKey key = SystemKeyManager.getInstance().getKey(keyName);
86+
if (key == null) {
87+
throw new RuntimeException("Key not found: " + keyName);
88+
}
89+
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), ALGORITHM);
7890

7991
// Decrypt with GCM authentication
8092
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
@@ -92,13 +104,29 @@ public String get(String config) {
92104
* Helper method to encrypt a value (for generating encrypted config).
93105
*
94106
* @param value the plaintext value to encrypt
95-
* @return base64-encoded ciphertext with crypto:: prefix
107+
* @return base64-encoded ciphertext with enc:: prefix
96108
*/
97109
public static String encrypt(String value) {
110+
return encrypt(value, null);
111+
}
112+
113+
/**
114+
* Helper method to encrypt a value with a named key.
115+
*
116+
* @param value the plaintext value to encrypt
117+
* @param keyName the name of the key to use (null for default)
118+
* @return base64-encoded ciphertext with enc::keyname:: prefix
119+
*/
120+
public static String encrypt(String value, String keyName) {
98121
try {
99-
// Use first 32 bytes of SystemSeed as the 256-bit AES key
100-
byte[] seed = SystemSeed.getSeed(0, 32);
101-
SecretKeySpec keySpec = new SecretKeySpec(seed, ALGORITHM);
122+
// Use SystemKeyManager to get the key, generating it if it doesn't exist
123+
SecretKey key = SystemKeyManager.getInstance().getKey(keyName);
124+
if (key == null) {
125+
// Key doesn't exist, generate it
126+
SystemKeyManager.getInstance().generateKey(keyName);
127+
key = SystemKeyManager.getInstance().getKey(keyName);
128+
}
129+
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), ALGORITHM);
102130

103131
// Generate secure random 12-byte IV
104132
byte[] iv = new byte[IV_SIZE_BYTES];
@@ -115,7 +143,14 @@ public static String encrypt(String value) {
115143
buf.put(iv);
116144
buf.put(ciphertext);
117145

118-
return "crypto::" + Base64.getEncoder().encodeToString(buf.array());
146+
String base64 = Base64.getEncoder().encodeToString(buf.array());
147+
148+
// If keyName is provided, include it in the prefix
149+
if (keyName != null && !keyName.isEmpty()) {
150+
return "enc::" + keyName + "::" + base64;
151+
} else {
152+
return "enc::" + base64;
153+
}
119154
} catch (Exception e) {
120155
throw new RuntimeException("Failed to encrypt value", e);
121156
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2026 jPOS Software SRL
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.jpos.core;
20+
21+
import org.jpos.security.SystemSeed;
22+
23+
import javax.crypto.Cipher;
24+
import javax.crypto.KeyGenerator;
25+
import javax.crypto.SecretKey;
26+
import javax.crypto.spec.GCMParameterSpec;
27+
import java.security.SecureRandom;
28+
import java.util.Base64;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.ConcurrentMap;
31+
32+
/**
33+
* Manages encryption keys derived from SystemSeed.
34+
*
35+
* <p>
36+
* This class:
37+
* <ul>
38+
* <li>Derives a consistent key from SystemSeed (32 bytes = 256 bits)</li>
39+
* <li>Supports multiple named keys for different encryption contexts</li>
40+
* <li>Provides methods to encrypt/decrypt data using these keys</li>
41+
* </ul>
42+
*/
43+
public class SystemKeyManager {
44+
private static final String DEFAULT_KEY_NAME = "default";
45+
private static final String DEFAULT_ENV_VAR = "JPOS_ENCRYPTION_KEY";
46+
private static final int KEY_SIZE_BITS = 256;
47+
48+
private static final SystemKeyManager instance;
49+
private static final ConcurrentMap<String, SecretKey> keys = new ConcurrentHashMap<>();
50+
51+
static {
52+
try {
53+
instance = new SystemKeyManager();
54+
} catch (Exception e) {
55+
throw new RuntimeException("Failed to initialize SystemKeyManager", e);
56+
}
57+
}
58+
59+
private SystemKeyManager() {
60+
}
61+
62+
/**
63+
* Returns the singleton SystemKeyManager instance.
64+
*
65+
* @return the SystemKeyManager instance
66+
*/
67+
public static SystemKeyManager getInstance() {
68+
return instance;
69+
}
70+
71+
/**
72+
* Gets a key by name. Returns null if key doesn't exist.
73+
*
74+
* @param keyName the name of the key to get
75+
* @return the SecretKey, or null if not found
76+
*/
77+
public SecretKey getKey(String keyName) {
78+
if (keyName == null || keyName.isEmpty()) {
79+
keyName = DEFAULT_KEY_NAME;
80+
}
81+
82+
return keys.get(keyName);
83+
}
84+
85+
/**
86+
* Gets the default key. Returns null if key doesn't exist.
87+
*
88+
* @return the default SecretKey, or null if not found
89+
*/
90+
public SecretKey getDefaultKey() {
91+
return getKey(DEFAULT_KEY_NAME);
92+
}
93+
94+
/**
95+
* Gets the Base64-encoded key by name.
96+
*
97+
* @param keyName the name of the key
98+
* @return Base64-encoded key, or null if not found
99+
*/
100+
public String getKeyBase64(String keyName) {
101+
SecretKey key = getKey(keyName);
102+
return key != null ? Base64.getEncoder().encodeToString(key.getEncoded()) : null;
103+
}
104+
105+
/**
106+
* Generates a new key from SystemSeed and stores it with the given name.
107+
* The user is responsible for setting the environment variable manually.
108+
*
109+
* @param keyName the name to give the key
110+
* @return the environment variable name where the key should be stored
111+
*/
112+
public String generateKey(String keyName) {
113+
if (keyName == null || keyName.isEmpty()) {
114+
keyName = DEFAULT_KEY_NAME;
115+
}
116+
117+
keys.computeIfAbsent(keyName, k -> {
118+
try {
119+
byte[] seed = SystemSeed.getSeed(0, 32);
120+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
121+
keyGen.init(KEY_SIZE_BITS, new SecureRandom(seed));
122+
return keyGen.generateKey();
123+
} catch (Exception e) {
124+
throw new RuntimeException("Failed to generate key from SystemSeed", e);
125+
}
126+
});
127+
128+
return getEnvVarName(keyName);
129+
}
130+
131+
/**
132+
* Generates a new default key from SystemSeed.
133+
*
134+
* @return the environment variable name where the key is stored
135+
*/
136+
public String generateDefaultKey() {
137+
return generateKey(DEFAULT_KEY_NAME);
138+
}
139+
140+
private static final int IV_SIZE_BYTES = 12;
141+
private static final int TAG_LENGTH_BITS = 128;
142+
143+
/**
144+
* Encrypts data using the default key.
145+
*
146+
* @param data the data to encrypt
147+
* @return encrypted data (with IV prepended)
148+
*/
149+
public byte[] encrypt(byte[] data) {
150+
return encrypt(data, DEFAULT_KEY_NAME);
151+
}
152+
153+
/**
154+
* Encrypts data using a named key.
155+
*
156+
* @param data the data to encrypt
157+
* @param keyName the name of the key to use
158+
* @return encrypted data (with IV prepended)
159+
*/
160+
public byte[] encrypt(byte[] data, String keyName) {
161+
try {
162+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
163+
SecretKey key = getKey(keyName);
164+
165+
byte[] iv = new byte[IV_SIZE_BYTES];
166+
new SecureRandom().nextBytes(iv);
167+
168+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
169+
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
170+
byte[] ciphertext = cipher.doFinal(data);
171+
172+
byte[] result = new byte[iv.length + ciphertext.length];
173+
System.arraycopy(iv, 0, result, 0, iv.length);
174+
System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
175+
176+
return result;
177+
} catch (Exception e) {
178+
throw new RuntimeException("Encryption failed", e);
179+
}
180+
}
181+
182+
/**
183+
* Decrypts data using the default key.
184+
*
185+
* @param encryptedData the encrypted data (with IV prepended)
186+
* @return decrypted data
187+
*/
188+
public byte[] decrypt(byte[] encryptedData) {
189+
return decrypt(encryptedData, DEFAULT_KEY_NAME);
190+
}
191+
192+
/**
193+
* Decrypts data using a named key.
194+
*
195+
* @param encryptedData the encrypted data (with IV prepended)
196+
* @param keyName the name of the key to use
197+
* @return decrypted data
198+
*/
199+
public byte[] decrypt(byte[] encryptedData, String keyName) {
200+
try {
201+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
202+
SecretKey key = getKey(keyName);
203+
204+
byte[] iv = new byte[IV_SIZE_BYTES];
205+
System.arraycopy(encryptedData, 0, iv, 0, iv.length);
206+
207+
byte[] ciphertext = new byte[encryptedData.length - iv.length];
208+
System.arraycopy(encryptedData, iv.length, ciphertext, 0, ciphertext.length);
209+
210+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
211+
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
212+
return cipher.doFinal(ciphertext);
213+
} catch (Exception e) {
214+
throw new RuntimeException("Decryption failed", e);
215+
}
216+
}
217+
218+
/**
219+
* Gets the environment variable name for a key.
220+
*
221+
* @param keyName the name of the key
222+
* @return the environment variable name
223+
*/
224+
public String getEnvVarName(String keyName) {
225+
if (keyName == null || keyName.isEmpty()) {
226+
keyName = DEFAULT_KEY_NAME;
227+
}
228+
return DEFAULT_ENV_VAR + (DEFAULT_KEY_NAME.equals(keyName) ? "" : "_" + keyName.toUpperCase());
229+
}
230+
231+
/**
232+
* Clears all keys (for testing purposes).
233+
*/
234+
public void clearKeys() {
235+
keys.clear();
236+
}
237+
}

0 commit comments

Comments
 (0)