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:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user