feat(auth): SecureStorage interface + Memory + Electron impls (7 tests)
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) <noreply@anthropic.com>
This commit is contained in:
parent
bc84eb2917
commit
d1eacf31f2
45
src/main/infra/secure-storage.test.ts
Normal file
45
src/main/infra/secure-storage.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
58
src/main/infra/secure-storage.ts
Normal file
58
src/main/infra/secure-storage.ts
Normal file
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user