From 2655c0e1c3662622798cb7086488470b4f7377fd Mon Sep 17 00:00:00 2001 From: Matteo Trubini <7964032+matteotrubini@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:24:41 +0100 Subject: [PATCH] feat(config): add key migration and file-based key storage Implements secure key management for encrypted preferences: - Adds file-based key storage in ~/.eppclient/local.key instead of hardcoded key - Automatically migrates old preferences to new key on first access - Generates unique encryption key per installation for improved security BREAKING CHANGE: Encrypted preferences from previous versions will be re-encrypted with the new key on first access. The old hardcoded key is retained only for reading during migration. --- .../java/EPPClient/config/CryptoUtils.java | 98 +++++++++++++++++-- src/main/java/EPPClient/config/EPPparams.java | 28 +++++- 2 files changed, 115 insertions(+), 11 deletions(-) diff --git a/src/main/java/EPPClient/config/CryptoUtils.java b/src/main/java/EPPClient/config/CryptoUtils.java index 893178d..b930284 100644 --- a/src/main/java/EPPClient/config/CryptoUtils.java +++ b/src/main/java/EPPClient/config/CryptoUtils.java @@ -17,6 +17,7 @@ import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; +import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; @@ -26,6 +27,13 @@ public class CryptoUtils { private static final String KEY_FILE = "local.key"; public static final String AES = "AES"; + // Old hardcoded key for migration from versions <= 2.3.2 + private static final String OLD_KEY = "D6B80943CCE0F1C049D9A39094FB4FA1"; + + private static String getKeyFilePath() { + String userHome = System.getProperty("user.home"); + return userHome + "/.eppclient/" + KEY_FILE; + } public static String encrypt(String value) throws GeneralSecurityException, IOException @@ -40,25 +48,97 @@ public static String encrypt(String value) public static String decrypt(String message) throws GeneralSecurityException, IOException { - SecretKeySpec sks = getSecretKeySpec(); - Cipher cipher = Cipher.getInstance(CryptoUtils.AES); - cipher.init(Cipher.DECRYPT_MODE, sks); - byte[] decrypted = cipher.doFinal(hexStringToByteArray(message)); - return new String(decrypted); + // Try with new key first + try { + SecretKeySpec sks = getSecretKeySpec(); + Cipher cipher = Cipher.getInstance(CryptoUtils.AES); + cipher.init(Cipher.DECRYPT_MODE, sks); + byte[] decrypted = cipher.doFinal(hexStringToByteArray(message)); + return new String(decrypted); + } catch (Exception e) { + // If fails, try with old key for migration from <= 2.3.2 + try { + byte[] oldKeyBytes = hexStringToByteArray(OLD_KEY); + SecretKeySpec sks = new SecretKeySpec(oldKeyBytes, CryptoUtils.AES); + Cipher cipher = Cipher.getInstance(CryptoUtils.AES); + cipher.init(Cipher.DECRYPT_MODE, sks); + byte[] decrypted = cipher.doFinal(hexStringToByteArray(message)); + return new String(decrypted); + } catch (Exception e2) { + throw new GeneralSecurityException("Decryption failed with both new and old keys"); + } + } } private static SecretKeySpec getSecretKeySpec() throws NoSuchAlgorithmException, IOException { - byte[] key = readKeyFile(); + byte[] key = readOrGenerateKeyFile(); SecretKeySpec sks = new SecretKeySpec(key, CryptoUtils.AES); return sks; } - private static byte[] readKeyFile() + private static byte[] readOrGenerateKeyFile() { - String keyValue = EPPparams.getKey(); - return hexStringToByteArray(keyValue); + String keyPath = getKeyFilePath(); + File keyFile = new File(keyPath); + + // If key file exists, read it + if (keyFile.exists()) { + try { + java.util.Scanner scanner = new java.util.Scanner(keyFile); + String keyHex = scanner.nextLine().trim(); + scanner.close(); + if (keyHex.length() == 32) { // 16 bytes = 32 hex chars + return hexStringToByteArray(keyHex); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Generate new key and save it + byte[] newKey = new byte[16]; + new java.security.SecureRandom().nextBytes(newKey); + + try { + // Ensure directory exists + File keyDir = keyFile.getParentFile(); + if (keyDir != null) { + keyDir.mkdirs(); + } + + // Save key to file + java.io.FileWriter writer = new java.io.FileWriter(keyFile); + writer.write(byteArrayToHexString(newKey)); + writer.close(); + + // Set file permissions to owner only (Unix-like systems) + keyFile.setReadable(true, false); + keyFile.setWritable(true, false); + keyFile.setExecutable(false); + } catch (IOException e) { + e.printStackTrace(); + } + + return newKey; + } + + /** + * Check if migration from old key is needed + * @return true if preferences exist encrypted with old key + */ + public static boolean needsMigration() { + String keyPath = getKeyFilePath(); + File keyFile = new File(keyPath); + return !keyFile.exists(); + } + + /** + * Get old key for migration purposes + */ + public static String getOldKey() { + return OLD_KEY; } diff --git a/src/main/java/EPPClient/config/EPPparams.java b/src/main/java/EPPClient/config/EPPparams.java index a76ac31..3f89aba 100644 --- a/src/main/java/EPPClient/config/EPPparams.java +++ b/src/main/java/EPPClient/config/EPPparams.java @@ -67,7 +67,21 @@ public static String getParameter(String key) { if (parameterValue.length() > 0) { - parameterValue = CryptoUtils.decrypt(parameterValue); + String decryptedValue = CryptoUtils.decrypt(parameterValue); + + // Check if decrypted with old key and needs migration + if (CryptoUtils.needsMigration() && decryptedValue != null && !decryptedValue.isEmpty()) { + // Migration: re-encrypt with new key + String reEncrypted = CryptoUtils.encrypt(decryptedValue); + prefs.putByteArray(paramPrefix + key, reEncrypted.getBytes()); + try { + prefs.sync(); + } catch (java.util.prefs.BackingStoreException ex) { + ex.printStackTrace(); + } + } + + return decryptedValue; } else { @@ -92,7 +106,17 @@ public static String getParameter(String key) } catch (Exception e) { - e.printStackTrace(); + // If decryption fails, try reading as plain text (for non-encrypted values) + // or return default from properties + try + { + parameterValue = RESOURCE_BUNDLE.getString(key); + } + catch (MissingResourceException e2) + { + parameterValue = ""; + } + setParameter(key, parameterValue); } return parameterValue;