From d1eacf31f26477ce5d9695e9be012c904e1f7507 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 22:12:23 +0200 Subject: [PATCH] feat(auth): SecureStorage interface + Memory + Electron impls (7 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Electron impl wraps safeStorage (Win Credential Manager). MemoryImpl uses base64 (no real crypto) — clearly marks isEncryptionAvailable()=false for test/headless envs. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/infra/secure-storage.test.ts | 45 +++++++++++++++++++++ src/main/infra/secure-storage.ts | 58 +++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/main/infra/secure-storage.test.ts create mode 100644 src/main/infra/secure-storage.ts diff --git a/src/main/infra/secure-storage.test.ts b/src/main/infra/secure-storage.test.ts new file mode 100644 index 0000000..463e00d --- /dev/null +++ b/src/main/infra/secure-storage.test.ts @@ -0,0 +1,45 @@ +import { test, expect, describe } from 'vitest'; +import { MemorySecureStorage, createElectronSecureStorage, type SecureStorage } from './secure-storage'; + +describe('MemorySecureStorage', () => { + test('isEncryptionAvailable returns false (kennzeichnet Memory-Mode)', () => { + const s: SecureStorage = new MemorySecureStorage(); + expect(s.isEncryptionAvailable()).toBe(false); + }); + + test('roundtrip ascii', () => { + const s = new MemorySecureStorage(); + const cipher = s.encrypt('hello'); + expect(cipher).not.toBe('hello'); // base64-Kodierung greift + expect(s.decrypt(cipher)).toBe('hello'); + }); + + test('roundtrip multi-byte', () => { + const s = new MemorySecureStorage(); + expect(s.decrypt(s.encrypt('aeoeue-test'))).toBe('aeoeue-test'); + }); + + test('roundtrip empty string', () => { + const s = new MemorySecureStorage(); + expect(s.decrypt(s.encrypt(''))).toBe(''); + }); + + test('long token (simuliert OAuth access_token Groesse)', () => { + const s = new MemorySecureStorage(); + const token = 'a'.repeat(256); + expect(s.decrypt(s.encrypt(token))).toBe(token); + }); +}); + +describe('createElectronSecureStorage', () => { + test('is exported as function', () => { + expect(typeof createElectronSecureStorage).toBe('function'); + }); + + test('throws useful error if called outside Electron (vitest env)', () => { + // In vitest (Node-only) ist electron entweder nicht installiert oder hat keine + // app-context-Funktionen. Genaues Error-Wording ist nicht stable, aber Aufruf + // muss throwen statt undefined zurueckgeben. + expect(() => createElectronSecureStorage()).toThrow(); + }); +}); diff --git a/src/main/infra/secure-storage.ts b/src/main/infra/secure-storage.ts new file mode 100644 index 0000000..90dbe4e --- /dev/null +++ b/src/main/infra/secure-storage.ts @@ -0,0 +1,58 @@ +// Verschluesselt String-Payloads im OS-Keystore (Win Credential Manager via +// Electron safeStorage). MemorySecureStorage ist fuer Tests/Headless-Envs — +// gibt plaintext zurueck und meldet isEncryptionAvailable() === false, damit +// Caller das in den Log schreiben oder verweigern koennen. + +export interface SecureStorage { + isEncryptionAvailable(): boolean; + encrypt(plaintext: string): string; + decrypt(ciphertext: string): string; +} + +export class MemorySecureStorage implements SecureStorage { + isEncryptionAvailable(): boolean { + return false; + } + encrypt(plaintext: string): string { + // Base64 als Kennzeichnung — kein Schutz, nur damit `decrypt(encrypt(x)) === x` + // semantisch konsistent ist (kein literal plaintext zwischen den Methoden). + return Buffer.from(plaintext, 'utf-8').toString('base64'); + } + decrypt(ciphertext: string): string { + return Buffer.from(ciphertext, 'base64').toString('utf-8'); + } +} + +interface SafeStorageLike { + isEncryptionAvailable(): boolean; + encryptString(plain: string): Buffer; + decryptString(buf: Buffer): string; +} + +/** + * Wrappt electron.safeStorage. Setzt voraus, dass `app.whenReady()` gefired ist. + * Wird per Lazy-Require konstruiert, sodass Module ausserhalb von Electron + * (zB Tests) das Modul importieren koennen ohne Crash. + */ +export function createElectronSecureStorage(): SecureStorage { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const electron = require('electron'); + const safeStorage = electron?.safeStorage as SafeStorageLike | undefined; + if (!safeStorage) { + throw new Error('Electron safeStorage not available (called before app.whenReady?)'); + } + + return { + isEncryptionAvailable(): boolean { + return safeStorage.isEncryptionAvailable(); + }, + encrypt(plaintext: string): string { + const buf = safeStorage.encryptString(plaintext); + return buf.toString('base64'); + }, + decrypt(ciphertext: string): string { + const buf = Buffer.from(ciphertext, 'base64'); + return safeStorage.decryptString(buf); + }, + }; +}