diff --git a/src/encryptionService.test.ts b/src/encryptionService.test.ts index 80c3820e..df0c007a 100644 --- a/src/encryptionService.test.ts +++ b/src/encryptionService.test.ts @@ -31,34 +31,6 @@ window.atob = jest .fn() .mockImplementation((str: string) => Buffer.from(str, "base64").toString("latin1")); -/** - * Ensure `window.localStorage` is usable for tests. - */ -function ensureTestLocalStorage(): { clear: () => void } { - try { - const storage = window.localStorage; - storage.setItem("__copilot_test__", "1"); - storage.removeItem("__copilot_test__"); - return { clear: () => window.localStorage.clear() }; - } catch { - const data = new Map(); - const shim = { - getItem: jest.fn((key: string) => data.get(key) ?? null), - setItem: jest.fn((key: string, value: string) => { - data.set(key, value); - }), - removeItem: jest.fn((key: string) => { - data.delete(key); - }), - clear: jest.fn(() => data.clear()), - }; - Object.defineProperty(window, "localStorage", { value: shim, configurable: true }); - return { clear: () => data.clear() }; - } -} - -const testLocalStorage = ensureTestLocalStorage(); - let randomCounter = 1; Object.defineProperty(window.crypto, "getRandomValues", { value: jest.fn((arr: Uint8Array) => { @@ -94,7 +66,6 @@ describe("EncryptionService", () => { beforeEach(() => { jest.clearAllMocks(); randomCounter = 1; - testLocalStorage.clear(); mockElectron.remote.safeStorage.isEncryptionAvailable.mockReturnValue(true); }); @@ -149,57 +120,6 @@ describe("EncryptionService", () => { expect(decrypted).toBe(plaintext); }); - it("should decrypt CP01 per-device payloads when localStorage key exists", async () => { - mockElectron.remote.safeStorage.isEncryptionAvailable.mockReturnValue(false); - - // Reason: CP01 uses a per-device AES key stored in localStorage. - // Seed it so getExistingWebCryptoKey() returns a usable key. - const keyBytes = new Uint8Array(32); - window.localStorage.setItem( - "obsidian-copilot:webcrypto-key:v1", - Buffer.from(keyBytes).toString("base64") - ); - - const plaintext = "cp01DeviceSecret"; - const iv = new Uint8Array(12); - iv[0] = 2; - - const ciphertext = new Uint8Array( - await crypto.subtle.encrypt( - { name: "AES-GCM", iv }, - "mockCryptoKey" as unknown as CryptoKey, - new TextEncoder().encode(plaintext) - ) - ); - - const cp01 = new Uint8Array([0x43, 0x50, 0x30, 0x31]); // "CP01" - const payload = new Uint8Array(cp01.length + iv.length + ciphertext.length); - payload.set(cp01, 0); - payload.set(iv, cp01.length); - payload.set(ciphertext, cp01.length + iv.length); - - const base64 = Buffer.from(payload).toString("base64"); - const decrypted = await getDecryptedKey(`enc_web_${base64}`); - expect(decrypted).toBe(plaintext); - }); - - it("should return empty string for CP01 when localStorage key is missing", async () => { - mockElectron.remote.safeStorage.isEncryptionAvailable.mockReturnValue(false); - // Reason: no key in localStorage — CP01 decryption should fail gracefully. - - const cp01 = new Uint8Array([0x43, 0x50, 0x30, 0x31]); // "CP01" - const iv = new Uint8Array(12); - const fakeCiphertext = new Uint8Array([1, 2, 3, 4]); - const payload = new Uint8Array(cp01.length + iv.length + fakeCiphertext.length); - payload.set(cp01, 0); - payload.set(iv, cp01.length); - payload.set(fakeCiphertext, cp01.length + iv.length); - - const base64 = Buffer.from(payload).toString("base64"); - const result = await getDecryptedKey(`enc_web_${base64}`); - expect(result).toBe(""); - }); - it("should decrypt legacy enc_ prefix payloads via WebCrypto fallback", async () => { mockElectron.remote.safeStorage.isEncryptionAvailable.mockReturnValue(false); diff --git a/src/encryptionService.ts b/src/encryptionService.ts index bf10ba7a..b934ae40 100644 --- a/src/encryptionService.ts +++ b/src/encryptionService.ts @@ -110,11 +110,8 @@ function looksLikeBase64(data: string): boolean { // --------------------------------------------------------------------------- const WEBCRYPTO_IV_LENGTH = 12; -const WEBCRYPTO_KEY_STORAGE_KEY = "obsidian-copilot:webcrypto-key:v1"; /** Magic header for portable payloads (hardcoded key + random IV). */ const WEBCRYPTO_PORTABLE_MAGIC = new Uint8Array([0x43, 0x50, 0x30, 0x30]); // "CP00" -/** Magic header for legacy per-device payloads (kept for backward-compat decryption). */ -const WEBCRYPTO_DEVICE_MAGIC = new Uint8Array([0x43, 0x50, 0x30, 0x31]); // "CP01" /** * Portable AES-GCM key material shared across all devices. @@ -129,32 +126,6 @@ function assertWebCryptoAvailable(): void { } } -function getLocalStorageSafe(): Storage | null { - try { - return window.localStorage ?? null; - } catch { - return null; - } -} - -/** - * Read an existing per-device AES-256 key from localStorage. - * Reason: kept for backward-compat decryption of CP01 payloads. - */ -async function getExistingWebCryptoKey(): Promise { - try { - const storage = getLocalStorageSafe(); - if (!storage) return null; - const existing = storage.getItem(WEBCRYPTO_KEY_STORAGE_KEY); - if (!existing) return null; - const raw = base64ToArrayBuffer(existing); - return await crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]); - } catch (error) { - console.warn("Failed to load WebCrypto key from localStorage.", error); - return null; - } -} - /** * Import the portable WebCrypto key for CP00 decryption * and legacy fixed-IV decryption. @@ -174,7 +145,7 @@ function hasWebCryptoMagic(bytes: Uint8Array, magic: Uint8Array): boolean { /** * Decrypt a WEBCRYPTO_PREFIX value. - * Supports portable CP00, legacy per-device CP01, and legacy fixed-IV formats. + * Supports portable CP00 and legacy fixed-IV formats. */ async function decryptWebCryptoValue(base64Data: string): Promise { assertWebCryptoAvailable(); @@ -193,21 +164,6 @@ async function decryptWebCryptoValue(base64Data: string): Promise { return new TextDecoder().decode(decrypted); } - // Legacy per-device format: [CP01][IV][CIPHERTEXT] - if (hasWebCryptoMagic(raw, WEBCRYPTO_DEVICE_MAGIC)) { - const ivStart = WEBCRYPTO_DEVICE_MAGIC.length; - const ivEnd = ivStart + WEBCRYPTO_IV_LENGTH; - if (raw.length <= ivEnd) throw new Error("Invalid WebCrypto CP01 payload: too short."); - - const iv = raw.slice(ivStart, ivEnd); - const ciphertext = raw.slice(ivEnd); - const key = await getExistingWebCryptoKey(); - if (!key) throw new Error("Per-device WebCrypto key unavailable for CP01 decryption."); - - const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext); - return new TextDecoder().decode(decrypted); - } - // Legacy format: ciphertext only, fixed IV + hardcoded key const key = await getPortableWebCryptoKey(); const decrypted = await crypto.subtle.decrypt(