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:
john kevin asprec
2026-07-14 21:38:42 +08:00
parent 47d9152bda
commit c4634d4965
4 changed files with 158 additions and 39 deletions

View File

@@ -1,7 +1,7 @@
import { defineBackground } from 'wxt/utils/define-background';
import nacl from 'tweetnacl';
import type { AnalyzePayload, LexAIConfig, LexAIResponse } from '@lib/types';
import { ACTIONS, ACTION_LABELS, CONTEXT_MENU_STYLES } from '@lib/actions';
import { decryptApiKey } from '@lib/crypto';
// ─── Fetch with timeout ───────────────────────────────────────────────────────
@@ -49,24 +49,12 @@ function getSystemPrompt(action: string, style?: string): string {
return base + styleModifier;
}
// ─── Encryption helpers ───────────────────────────────────────────────────────
async function decryptApiKey(encKeyB64: string, apiKeyEncB64: string): Promise<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);
}
// Resolve the usable API key from stored config: prefer the encrypted path,
// fall back to plaintext for backward compat. Returns null if none is set.
async function resolveApiKey(config: LexAIConfig): Promise<string | null> {
let apiKey = config.apiKey;
if (config.apiKeyEnc && config.encKey) {
const decrypted = await decryptApiKey(config.encKey, config.apiKeyEnc);
const decrypted = decryptApiKey(config.encKey, config.apiKeyEnc);
if (decrypted) apiKey = decrypted;
}
if (!apiKey || apiKey.trim() === '') return null;