refactor: consolidate API-key crypto into src/lib/crypto with compat tests
Encrypt (Options) and decrypt (background) now share one module; wire format unchanged. Tests include fixtures proving values encrypted by the old inline Options code still decrypt, plus tamper/wrong-key cases. The module documents the honest threat model (key co-located with ciphertext = obfuscation, not encryption). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
83
tests/unit/crypto.test.ts
Normal file
83
tests/unit/crypto.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import nacl from 'tweetnacl';
|
||||
import {
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
generateEncKey,
|
||||
encryptApiKey,
|
||||
decryptApiKey,
|
||||
} from '@lib/crypto';
|
||||
|
||||
// Reproduces the ORIGINAL inline implementations verbatim (Options.tsx encrypt,
|
||||
// background.ts decrypt) to prove the extracted module is wire-compatible with
|
||||
// keys already sitting in users' storage.
|
||||
function legacyEncrypt(plaintext: string, key: Uint8Array): { encKeyB64: string; apiKeyEncB64: string } {
|
||||
const nonce = nacl.randomBytes(24);
|
||||
const encoded = Uint8Array.from(new TextEncoder().encode(plaintext));
|
||||
const encrypted = nacl.secretbox(encoded, nonce, key);
|
||||
const combined = new Uint8Array(nonce.length + encrypted.length);
|
||||
combined.set(nonce);
|
||||
combined.set(encrypted, nonce.length);
|
||||
return {
|
||||
encKeyB64: btoa(String.fromCharCode(...key)),
|
||||
apiKeyEncB64: btoa(String.fromCharCode(...combined)),
|
||||
};
|
||||
}
|
||||
|
||||
function legacyDecrypt(encKeyB64: string, apiKeyEncB64: string): string | null {
|
||||
const key = Uint8Array.from(atob(encKeyB64), (c) => c.charCodeAt(0));
|
||||
const combined = Uint8Array.from(atob(apiKeyEncB64), (c) => c.charCodeAt(0));
|
||||
const nonce = combined.slice(0, 24);
|
||||
const cipher = combined.slice(24);
|
||||
const decrypted = nacl.secretbox.open(cipher, nonce, key);
|
||||
if (!decrypted) return null;
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
describe('base64 helpers', () => {
|
||||
it('roundtrips arbitrary bytes', () => {
|
||||
const bytes = nacl.randomBytes(64);
|
||||
expect(base64ToBytes(bytesToBase64(bytes))).toEqual(bytes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt/decrypt roundtrip', () => {
|
||||
it('decrypts what it encrypted', () => {
|
||||
const key = generateEncKey();
|
||||
const enc = encryptApiKey('sk-test-1234567890', key);
|
||||
expect(decryptApiKey(bytesToBase64(key), enc)).toBe('sk-test-1234567890');
|
||||
});
|
||||
|
||||
it('handles unicode and long keys', () => {
|
||||
const key = generateEncKey();
|
||||
const secret = 'sk-or-v1-' + 'a'.repeat(128) + '-héllo-👋';
|
||||
expect(decryptApiKey(bytesToBase64(key), encryptApiKey(secret, key))).toBe(secret);
|
||||
});
|
||||
|
||||
it('returns null when the ciphertext is tampered', () => {
|
||||
const key = generateEncKey();
|
||||
const enc = encryptApiKey('sk-test', key);
|
||||
const bytes = base64ToBytes(enc);
|
||||
bytes[bytes.length - 1] ^= 0xff;
|
||||
expect(decryptApiKey(bytesToBase64(key), bytesToBase64(bytes))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null with the wrong key', () => {
|
||||
const enc = encryptApiKey('sk-test', generateEncKey());
|
||||
expect(decryptApiKey(bytesToBase64(generateEncKey()), enc)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('backward compatibility with the pre-extraction inline code', () => {
|
||||
it('decrypts a value encrypted by the OLD Options.tsx code path', () => {
|
||||
const key = nacl.randomBytes(32);
|
||||
const { encKeyB64, apiKeyEncB64 } = legacyEncrypt('sk-legacy-user-key', key);
|
||||
expect(decryptApiKey(encKeyB64, apiKeyEncB64)).toBe('sk-legacy-user-key');
|
||||
});
|
||||
|
||||
it('produces output the OLD background.ts decrypt understands', () => {
|
||||
const key = generateEncKey();
|
||||
const enc = encryptApiKey('sk-new-key', key);
|
||||
expect(legacyDecrypt(bytesToBase64(key), enc)).toBe('sk-new-key');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user