--- name: threat-model-apikey description: LexAI API-key at-rest threat model and the storage/migration invariants an auditor must preserve metadata: type: project --- LexAI is a BYO-LLM-key MV3 extension with no backend. The user's provider API key is the crown-jewel secret. Storage scheme (src/lib/crypto.ts): `encKey` = base64(32-byte secretbox key), `apiKeyEnc` = base64(nonce||ciphertext). Legacy plaintext `apiKey` is back-compat fallback and must not be dropped without a migration. `migratePlaintextApiKey()` runs on background startup: encrypts legacy plaintext into the secretbox scheme, removes plaintext only after the encrypted copy is persisted (and, when one already exists, only after verifying it decrypts). **Why:** The secretbox key lives in the same chrome.storage.local as the ciphertext, so this is obfuscation against casual inspection only — anyone who can read extension storage can decrypt. This is honestly documented in crypto.ts and UI copy must not over-promise. **How to apply when auditing key-path changes:** - Never log or transmit the key except to the user's chosen provider endpoint. Grep console.* for key/config exposure; error strings in providers.ts must carry only provider label + response message, never headers/key. - Preserve write-before-delete ordering in any migration so an interruption can't lose the only key. Note: getOrCreateEncKey persists encKey fire-and-forget (no awaited callback) — relies on Chrome FIFO storage ordering; awaiting it would be stricter. - Both onMessage listeners (background + content) guard `sender.id !== chrome.runtime.id`. Legit internal messages (content/popup/options + background→content tabs.sendMessage) all carry sender.id === chrome.runtime.id, so the guard is safe. It blocks other extensions / externally_connectable. - Content/popup must never fetch a provider directly — key handling belongs in the background worker.