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); + }, + }; +}