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 React, { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import nacl from 'tweetnacl';
import { safeSendMessage, safeStorageGet, safeStorageSet } from '@lib/messaging';
import { encryptApiKey, getOrCreateEncKey } from '@lib/crypto';
// ─── Provider config ──────────────────────────────────────────────────────────
@@ -33,23 +33,6 @@ const PROVIDERS = [
},
];
// ─── Encryption helpers ───────────────────────────────────────────────────────
async function getOrCreateEncKey(): Promise<Uint8Array> {
return new Promise((resolve) => {
chrome.storage.local.get(['encKey'], (result) => {
if (result.encKey) {
resolve(Uint8Array.from(atob(result.encKey as string), c => c.charCodeAt(0)));
} else {
const key = nacl.randomBytes(32);
const keyB64 = btoa(String.fromCharCode(...key));
chrome.storage.local.set({ encKey: keyB64 });
resolve(key);
}
});
});
}
// ─── Styles ───────────────────────────────────────────────────────────────────
const styles = {
@@ -275,13 +258,7 @@ function OptionsPage() {
try {
const key = await getOrCreateEncKey();
const nonce = nacl.randomBytes(24);
const encoded = new TextEncoder().encode(apiKey.trim());
const encrypted = nacl.secretbox(encoded, nonce, key);
const combined = new Uint8Array(nonce.length + encrypted.length);
combined.set(nonce);
combined.set(encrypted, nonce.length);
const apiKeyEncB64 = btoa(String.fromCharCode(...combined));
const apiKeyEncB64 = encryptApiKey(apiKey.trim(), key);
safeStorageSet({ apiKeyEnc: apiKeyEncB64, provider, model }, () => {
if (chrome.runtime.lastError) {