BrainCue

API Key Security Plan

v2 provider layer: the OpenAI adapters in src/main/providers/openai never read or store keys — they delegate to the existing service modules, so the OpenAI key still flows exclusively through services/openai/client.ts (and realtime.ts for the socket header). Since v2.2 the other providers (Anthropic, Google Gemini, Groq, OpenRouter) follow the same shape through one store, services/security/providerKeys.ts — see “Keys per provider” below: main process only, safeStorage at rest, resolved per provider at call time, never over IPC. The architecture test pins that no key-store or API host markers enter the renderer bundle.

Principles

  1. The OpenAI API key is only ever present in the main process memory.
  2. It is never sent over IPC to the renderer.
  3. It is never hardcoded.
  4. It is never committed (.env is gitignored; only .env.example exists).
  5. It is never logged (logger redacts anything matching sk-...).

Sources & precedence (main process)

  1. Dev: process.env.OPENAI_API_KEY (from .env, loaded only in dev).
  2. User-provided: pasted in Settings → encrypted at rest in SQLite.

Resolution order at call time: env var (if set) → decrypted stored key. If neither exists, OpenAI calls fail fast with a clear “No API key configured” error and the UI prompts the user to add one.

Storage at rest

Primary backend: Electron safeStorage.

import { safeStorage } from 'electron';
// save:
const cipher = safeStorage.encryptString(plaintextKey);     // Buffer
settings.set('openai_api_key_enc', cipher.toString('base64'));
settings.set('openai_api_key_present', '1');
// load (main only):
const enc = settings.get('openai_api_key_enc');
const key = enc ? safeStorage.decryptString(Buffer.from(enc, 'base64')) : null;

Interface (services/security/apiKey.ts)

interface ApiKeyStore {
  isPresent(): boolean;          // safe to expose via IPC (boolean only)
  set(plaintext: string): void;  // encrypts + persists
  clear(): void;
  getDecrypted(): string | null; // MAIN ONLY — never crosses IPC
}

What the renderer can know

Keys per provider (v2.2, services/security/providerKeys.ts)

Every cloud provider in shared/providers.ts gets its own key under exactly the rules above — the principles are per key, not per vendor:

Test flow

settings:test-api-key does a cheap call (e.g. list models / tiny embedding) in main and returns { ok, model } or { ok:false, error } — without revealing the key.

Logging & telemetry

Threat notes

Memory privacy

The local memory subsystem’s standing guarantees:

Voice privacy

The voice/summon layer’s standing guarantees:

Encryption at rest — design (implementation deferred)

Goal: memory content unreadable if app.db is copied off the machine, without breaking migrations or packaging.