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 javax .crypto .spec .SecretKeySpec ;
28+ import java .security .SecureRandom ;
29+ import java .util .Base64 ;
30+
31+ /**
32+ * Manages encryption keys derived from SystemSeed.
33+ *
34+ * <p>
35+ * This class:
36+ * <ul>
37+ * <li>Derives a consistent key from SystemSeed (32 bytes = 256 bits)</li>
38+ * <li>Provides methods to encrypt/decrypt data using these keys</li>
39+ * <li>Loads keys strictly from environment variables (no internal caching)</li>
40+ * </ul>
41+ */
42+ public class SystemKeyManager {
43+ private static final String DEFAULT_KEY_NAME = "default" ;
44+ private static final String DEFAULT_ENV_VAR = "JPOS_ENCRYPTION_KEY" ;
45+ private static final int KEY_SIZE_BITS = 256 ;
46+
47+ private static final SystemKeyManager instance ;
48+
49+ static {
50+ try {
51+ instance = new SystemKeyManager ();
52+ } catch (Exception e ) {
53+ throw new RuntimeException ("Failed to initialize SystemKeyManager" , e );
54+ }
55+ }
56+
57+ private SystemKeyManager () {
58+ }
59+
60+ /**
61+ * Returns the singleton SystemKeyManager instance.
62+ *
63+ * @return the SystemKeyManager instance
64+ */
65+ public static SystemKeyManager getInstance () {
66+ return instance ;
67+ }
68+
69+ /**
70+ * Gets a key by name. Returns null if key doesn't exist in environment.
71+ * Never caches the key.
72+ *
73+ * @param keyName the name of the key to get
74+ * @return the SecretKey, or null if not found
75+ */
76+ public SecretKey getKey (String keyName ) {
77+ if (keyName == null || keyName .isEmpty ()) {
78+ keyName = DEFAULT_KEY_NAME ;
79+ }
80+
81+ String envVarName = getEnvVarName (keyName );
82+ String envValue = System .getenv (envVarName );
83+
84+ // Fallback for test environments where setting System.getenv is not possible
85+ if (envValue == null || envValue .trim ().isEmpty ()) {
86+ envValue = System .getProperty (envVarName );
87+ }
88+
89+ if (envValue != null && !envValue .trim ().isEmpty ()) {
90+ try {
91+ byte [] keyBytes = Base64 .getDecoder ().decode (envValue .trim ());
92+ if (keyBytes .length == KEY_SIZE_BITS / 8 ) {
93+ return new SecretKeySpec (keyBytes , "AES" );
94+ }
95+ } catch (IllegalArgumentException e ) {
96+ // Invalid Base64 or length
97+ }
98+ }
99+
100+ return null ;
101+ }
102+
103+ /**
104+ * Gets the default key. Returns null if key doesn't exist.
105+ *
106+ * @return the default SecretKey, or null if not found
107+ */
108+ public SecretKey getDefaultKey () {
109+ return getKey (DEFAULT_KEY_NAME );
110+ }
111+
112+ /**
113+ * Gets the Base64-encoded key by name.
114+ *
115+ * @param keyName the name of the key
116+ * @return Base64-encoded key, or null if not found
117+ */
118+ public String getKeyBase64 (String keyName ) {
119+ SecretKey key = getKey (keyName );
120+ return key != null ? Base64 .getEncoder ().encodeToString (key .getEncoded ()) : null ;
121+ }
122+
123+ /**
124+ * Generates a new key from SystemSeed and returns its Base64 string.
125+ * The user is responsible for setting the environment variable manually.
126+ *
127+ * @param keyName the name to give the key
128+ * @return the generated key in Base64
129+ */
130+ public String generateKey (String keyName ) {
131+ try {
132+ byte [] seed = SystemSeed .getSeed (0 , 32 );
133+ KeyGenerator keyGen = KeyGenerator .getInstance ("AES" );
134+ keyGen .init (KEY_SIZE_BITS , new SecureRandom (seed ));
135+ SecretKey key = keyGen .generateKey ();
136+ return Base64 .getEncoder ().encodeToString (key .getEncoded ());
137+ } catch (Exception e ) {
138+ throw new RuntimeException ("Failed to generate key from SystemSeed" , e );
139+ }
140+ }
141+
142+ /**
143+ * Generates a new default key from SystemSeed.
144+ *
145+ * @return the Base64-encoded generated key
146+ */
147+ public String generateDefaultKey () {
148+ return generateKey (DEFAULT_KEY_NAME );
149+ }
150+
151+ private static final int IV_SIZE_BYTES = 12 ;
152+ private static final int TAG_LENGTH_BITS = 128 ;
153+
154+ /**
155+ * Encrypts data using the default key.
156+ *
157+ * @param data the data to encrypt
158+ * @return encrypted data (with IV prepended)
159+ */
160+ public byte [] encrypt (byte [] data ) {
161+ return encrypt (data , DEFAULT_KEY_NAME );
162+ }
163+
164+ /**
165+ * Encrypts data using a named key.
166+ *
167+ * @param data the data to encrypt
168+ * @param keyName the name of the key to use
169+ * @return encrypted data (with IV prepended)
170+ */
171+ public byte [] encrypt (byte [] data , String keyName ) {
172+ try {
173+ Cipher cipher = Cipher .getInstance ("AES/GCM/NoPadding" );
174+ SecretKey key = getKey (keyName );
175+
176+ if (key == null ) {
177+ throw new IllegalArgumentException ("No key found in environment for name: " +
178+ (keyName != null && !keyName .isEmpty () ? keyName : DEFAULT_KEY_NAME ) + ". Please set " + getEnvVarName (keyName ));
179+ }
180+
181+ byte [] iv = new byte [IV_SIZE_BYTES ];
182+ new SecureRandom ().nextBytes (iv );
183+
184+ GCMParameterSpec gcmParameterSpec = new GCMParameterSpec (TAG_LENGTH_BITS , iv );
185+ cipher .init (Cipher .ENCRYPT_MODE , key , gcmParameterSpec );
186+ byte [] ciphertext = cipher .doFinal (data );
187+
188+ byte [] result = new byte [iv .length + ciphertext .length ];
189+ System .arraycopy (iv , 0 , result , 0 , iv .length );
190+ System .arraycopy (ciphertext , 0 , result , iv .length , ciphertext .length );
191+
192+ return result ;
193+ } catch (Exception e ) {
194+ throw new RuntimeException ("Encryption failed" , e );
195+ }
196+ }
197+
198+ /**
199+ * Decrypts data using the default key.
200+ *
201+ * @param encryptedData the encrypted data (with IV prepended)
202+ * @return decrypted data
203+ */
204+ public byte [] decrypt (byte [] encryptedData ) {
205+ return decrypt (encryptedData , DEFAULT_KEY_NAME );
206+ }
207+
208+ /**
209+ * Decrypts data using a named key.
210+ *
211+ * @param encryptedData the encrypted data (with IV prepended)
212+ * @param keyName the name of the key to use
213+ * @return decrypted data
214+ */
215+ public byte [] decrypt (byte [] encryptedData , String keyName ) {
216+ try {
217+ Cipher cipher = Cipher .getInstance ("AES/GCM/NoPadding" );
218+ SecretKey key = getKey (keyName );
219+
220+ if (key == null ) {
221+ throw new IllegalArgumentException ("No key found in environment for name: " +
222+ (keyName != null && !keyName .isEmpty () ? keyName : DEFAULT_KEY_NAME ) + ". Please set " + getEnvVarName (keyName ));
223+ }
224+
225+ byte [] iv = new byte [IV_SIZE_BYTES ];
226+ System .arraycopy (encryptedData , 0 , iv , 0 , iv .length );
227+
228+ byte [] ciphertext = new byte [encryptedData .length - iv .length ];
229+ System .arraycopy (encryptedData , iv .length , ciphertext , 0 , ciphertext .length );
230+
231+ GCMParameterSpec gcmParameterSpec = new GCMParameterSpec (TAG_LENGTH_BITS , iv );
232+ cipher .init (Cipher .DECRYPT_MODE , key , gcmParameterSpec );
233+ return cipher .doFinal (ciphertext );
234+ } catch (Exception e ) {
235+ throw new RuntimeException ("Decryption failed" , e );
236+ }
237+ }
238+
239+ /**
240+ * Gets the environment variable name for a key.
241+ *
242+ * @param keyName the name of the key
243+ * @return the environment variable name
244+ */
245+ public String getEnvVarName (String keyName ) {
246+ if (keyName == null || keyName .isEmpty ()) {
247+ keyName = DEFAULT_KEY_NAME ;
248+ }
249+ return DEFAULT_ENV_VAR + (DEFAULT_KEY_NAME .equals (keyName ) ? "" : "_" + keyName .toUpperCase ());
250+ }
251+
252+ /**
253+ * Clears all keys (for testing purposes).
254+ */
255+ public void clearKeys () {
256+ // No-op as internal cache is removed
257+ }
258+ }
0 commit comments