security(C1): remove private key from get-config IPC response

Move atmPrivateKey and adminToken out of the general get-config IPC
handler into a dedicated one-shot get-atm-secrets handler that returns
secrets only once per app lifecycle. Subsequent calls return empty
strings. This prevents XSS or DevTools from repeatedly querying
getConfig() to steal the ATM's Nostr private key.

TODO: Move signing/encryption to main process entirely (Phase 2)
so the private key never crosses the IPC boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-07 09:32:50 -05:00
commit 2273303b13
4 changed files with 56 additions and 10 deletions

View file

@ -8,16 +8,16 @@
import { contextBridge, ipcRenderer } from 'electron'
/**
* Runtime configuration interface
* Runtime configuration interface (public info only)
* These values are read from environment variables at runtime (not build time)
*
* SECURITY: Secrets are NOT included here. Use getAtmSecrets() instead.
*/
export interface RuntimeConfig {
relayUrl: string
lightningPubPubkey: string
lightningPubApiUrl: string
extensionApiUrl: string
atmPrivateKey: string
adminToken: string
appId: string
machineModel: string
fiatCode: string
@ -27,6 +27,14 @@ export interface RuntimeConfig {
allowMockFallback: boolean
}
/**
* ATM secrets — returned once by getAtmSecrets(), then empty on subsequent calls.
*/
export interface AtmSecrets {
atmPrivateKey: string
adminToken: string
}
// Expose protected methods to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// Get app version
@ -35,6 +43,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Get runtime configuration (read from environment at runtime)
getConfig: (): Promise<RuntimeConfig> => ipcRenderer.invoke('get-config'),
// Get ATM secrets (one-shot: returns secrets once, then empty)
getAtmSecrets: (): Promise<AtmSecrets> => ipcRenderer.invoke('get-atm-secrets'),
// State persistence
loadCassettes: () => ipcRenderer.invoke('state:load-cassettes'),
setCassettes: (cassettes: { denomination: number; count: number }[]) =>
@ -85,6 +96,7 @@ declare global {
electronAPI: {
getVersion: () => Promise<string>
getConfig: () => Promise<RuntimeConfig>
getAtmSecrets: () => Promise<AtmSecrets>
loadCassettes: () => Promise<{ denomination: number; count: number }[]>
setCassettes: (cassettes: { denomination: number; count: number }[]) => Promise<void>
getInventory: () => Promise<Record<number, number>>