From 8bd565b72011fe0173ea36243c0a48c5caca1b19 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 09:08:56 +0000 Subject: [PATCH] Fix plaintext storage of credentials by implementing AES encryption Implemented AES-128 GCM encryption in `FileCredentialStore.java`. The AES key is randomly generated and securely stored via Java Preferences API upon first use. This prevents storing Steam refresh tokens as plaintext on the filesystem. Implemented a transparent fallback to read legacy plaintext tokens during load, which will be automatically encrypted upon re-save. Co-authored-by: SirDank <52797753+SirDank@users.noreply.github.com> --- .../co/frenchpress/FileCredentialStore.java | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/main/java/co/frenchpress/FileCredentialStore.java b/src/main/java/co/frenchpress/FileCredentialStore.java index 2b5e68d..4519737 100644 --- a/src/main/java/co/frenchpress/FileCredentialStore.java +++ b/src/main/java/co/frenchpress/FileCredentialStore.java @@ -1,10 +1,20 @@ package co.frenchpress; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; +import java.security.SecureRandom; +import java.util.Base64; import java.util.Set; +import java.util.prefs.Preferences; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; /** * Default {@link CredentialStore}: a single file in a private directory, @@ -24,7 +34,13 @@ public final class FileCredentialStore implements CredentialStore { Path p = path(); if (!Files.isReadable(p)) return null; String s = Files.readString(p).strip(); - return s.isEmpty() ? null : s; + if (s.isEmpty()) return null; + try { + return decrypt(s); + } catch (Exception e) { + // Fallback for legacy plaintext credentials + return s; + } } catch (Throwable t) { System.err.println("[frenchpress] credential load failed: " + t); return null; @@ -40,7 +56,7 @@ public final class FileCredentialStore implements CredentialStore { trySetPerms(dir, Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE)); } - Files.writeString(p, data); + Files.writeString(p, encrypt(data)); trySetPerms(p, Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); } catch (Throwable t) { @@ -60,6 +76,46 @@ private static void trySetPerms (Path p, Set perms) { } } + private static SecretKey getOrGenerateKey () throws Exception { + Preferences prefs = Preferences.userNodeForPackage(FileCredentialStore.class); + String encodedKey = prefs.get("frenchpress_aes_key", null); + if (encodedKey != null) { + byte[] decodedKey = Base64.getDecoder().decode(encodedKey); + return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + } + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(128); + SecretKey secretKey = keyGen.generateKey(); + prefs.put("frenchpress_aes_key", Base64.getEncoder().encodeToString(secretKey.getEncoded())); + return secretKey; + } + + private static String encrypt (String data) throws Exception { + SecretKey key = getOrGenerateKey(); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] iv = new byte[12]; + new SecureRandom().nextBytes(iv); + GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); + byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + + ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedData.length); + byteBuffer.put(iv); + byteBuffer.put(encryptedData); + return Base64.getEncoder().encodeToString(byteBuffer.array()); + } + + private static String decrypt (String encryptedData) throws Exception { + SecretKey key = getOrGenerateKey(); + byte[] cipherMessage = Base64.getDecoder().decode(encryptedData); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec parameterSpec = new GCMParameterSpec(128, cipherMessage, 0, 12); + cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); + byte[] plainText = cipher.doFinal(cipherMessage, 12, cipherMessage.length - 12); + return new String(plainText, StandardCharsets.UTF_8); + } + private static Path path () { String override = System.getenv("FRENCHPRESS_CRED_FILE"); if (override != null && !override.isEmpty()) return Path.of(override);