- Updated CONFIGURATION.md to improve navigation and added mermaid diagrams for better visualization of processes. - Revised IPC_ARCHITECTURE.md to clarify the security model and added diagrams to illustrate the architecture. - Improved README.md files in core-api, core-events, core-i18n, and core-storage for consistency and clarity, including better descriptions and structural enhancements.
436 lines
19 KiB
Markdown
436 lines
19 KiB
Markdown
[← Back to Root](../../../README.md)
|
|
|
|
# IPC Architecture & Security Model
|
|
|
|
The Secure Communication Blueprint.
|
|
|
|
> This document defines the **Hardened Security Perimeter** and communication topology governing the Desktop Wrapper. Every native capability exposed to the renderer is mediated through a **Non-Bypassable IPC Bridge**, ensuring that the Node.js Main Process remains cryptographically and logically isolated from untrusted web content. Adherence to this document is **mandatory** — deviations constitute security violations subject to immediate remediation.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Privilege Separation Model](#privilege-separation-model)
|
|
- [Standard Operating Procedure: The Three-Step Bridge](#standard-operating-procedure-the-three-step-bridge)
|
|
- [Verified Channel Manifest](#verified-channel-manifest)
|
|
- [Extending the Bridge: Guided Walkthrough](#extending-the-bridge-guided-walkthrough)
|
|
- [Critical Audit Checklist: Anti-Patterns](#critical-audit-checklist-anti-patterns)
|
|
- [The Gold Standard for Native Integration](#the-gold-standard-for-native-integration)
|
|
|
|
---
|
|
|
|
## Privilege Separation Model
|
|
|
|
The desktop wrapper enforces a **strict privilege separation** between three execution contexts, each operating under fundamentally different trust levels. This architecture ensures that a compromise in any single layer cannot escalate to full system access.
|
|
|
|
### Trust Level Matrix
|
|
|
|
| Context | Trust Level | Privilege Scope | Security Guarantee |
|
|
| ------------------ | ------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
| **Main Process** | Fully Trusted | Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes | Only code authored by the engineering team executes here |
|
|
| **Preload Script** | Controlled | Restricted to `ipcRenderer.invoke()` and `ipcRenderer.send()` — no direct Node.js access | Executes in a **Hermetically Sealed Context** — isolated from both the Main Process globals and the Renderer's DOM |
|
|
| **Renderer** | Untrusted | Standard browser sandbox — zero Node.js API surface | Designated as a **Zero-Trust Environment** — may execute third-party code, npm packages, or XSS payloads |
|
|
|
|
### Process Topology
|
|
|
|
```mermaid
|
|
graph TD
|
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
|
classDef trustedLayer fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#0f172a
|
|
classDef gatewayLayer fill:#f0fdf4,stroke:#10b981,stroke-width:2px,color:#064e3b
|
|
classDef untrustedLayer fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
|
|
classDef functionNode fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
|
|
|
%% ─── Subgraphs ───
|
|
subgraph Main ["MAIN PROCESS [Fully Trusted]"]
|
|
M_DESC["Unrestricted Node.js Privileges"]
|
|
IPC_MAIN_H[ipcMain.handle]
|
|
IPC_MAIN_O[ipcMain.on]
|
|
end
|
|
|
|
subgraph Preload ["PRELOAD SCRIPT [Secure Gateway]"]
|
|
P_DESC["Hermetically Sealed Context (Interface Narrowing)"]
|
|
CTX_BRIDGE{contextBridge.exposeInMainWorld}
|
|
end
|
|
|
|
subgraph Renderer ["RENDERER [Zero-Trust Environment]"]
|
|
R_DESC["Standard Browser Sandbox (No Node.js APIs)"]
|
|
E_API([window.electronAPI])
|
|
end
|
|
|
|
%% ─── Flow & Relationships ───
|
|
E_API ===>|Only Authorized Vector| CTX_BRIDGE
|
|
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_H
|
|
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_O
|
|
|
|
%% ─── Apply Styles ───
|
|
class Main trustedLayer;
|
|
class Preload gatewayLayer;
|
|
class Renderer untrustedLayer;
|
|
class M_DESC,P_DESC,R_DESC,IPC_MAIN_H,IPC_MAIN_O functionNode;
|
|
class CTX_BRIDGE gatewayLayer;
|
|
class E_API untrustedLayer;
|
|
|
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
|
style Main fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
|
style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
|
|
style Renderer fill:transparent,stroke:#ef4444,stroke-width:2px,stroke-dasharray: 5 5
|
|
```
|
|
|
|
The Preload Script functions as a **Secure Gateway** that performs **Interface Narrowing** — it transforms the broad, unrestricted IPC capabilities of the Main Process into a deliberately narrow, type-safe API surface. The renderer communicates with native functionality **exclusively** through this gateway. There are no alternative paths, no escape hatches, and no backdoors.
|
|
|
|
### Enforcement Configuration
|
|
|
|
These settings are declared in `BrowserWindow.webPreferences` and are **non-negotiable**:
|
|
|
|
| Setting | Value | Enforcement |
|
|
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. |
|
|
| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. |
|
|
| `sandbox` | `true` | The renderer process runs inside a **Chromium OS-level sandbox**, restricting system calls and file access at the kernel level. |
|
|
| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. |
|
|
|
|
---
|
|
|
|
## Standard Operating Procedure: The Three-Step Bridge
|
|
|
|
Every native feature in this architecture **must** follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface.
|
|
|
|
> [!IMPORTANT] > **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production).
|
|
|
|
### Step 1: Register the Handler — Main Process
|
|
|
|
**File:** `apps/desktop/src/main/index.ts`
|
|
|
|
```typescript
|
|
// COMMAND PATTERN: Use ipcMain.handle for request/response operations
|
|
// The handler returns a value to the renderer via a resolved Promise.
|
|
ipcMain.handle('feature:action', async (_event, arg1: string, arg2: number) => {
|
|
// Validate inputs. Never trust data from the renderer.
|
|
if (typeof arg1 !== 'string' || typeof arg2 !== 'number') {
|
|
throw new Error('Invalid arguments');
|
|
}
|
|
const result = await someNativeAPI(arg1, arg2);
|
|
return result;
|
|
});
|
|
|
|
// EVENT PATTERN: Use ipcMain.on for fire-and-forget operations
|
|
// No return value — the renderer does not wait for a response.
|
|
ipcMain.on('feature:fire', (_event, data: SomeType) => {
|
|
performSideEffect(data);
|
|
});
|
|
```
|
|
|
|
**Channel naming convention:** `namespace:action` — examples: `printer:get-list`, `updater:check`, `app:get-version`. Namespaces must be unique, descriptive, and never generic.
|
|
|
|
### Step 2: Expose via contextBridge — Preload Gateway
|
|
|
|
**File:** `apps/desktop/src/preload/index.ts`
|
|
|
|
```typescript
|
|
const electronAPI = {
|
|
// Command pattern exposure
|
|
featureAction: (arg1: string, arg2: number): Promise<ResultType> => {
|
|
return ipcRenderer.invoke('feature:action', arg1, arg2);
|
|
},
|
|
|
|
// Event pattern exposure
|
|
featureFire: (data: SomeType): void => {
|
|
ipcRenderer.send('feature:fire', data);
|
|
},
|
|
|
|
// Main→Renderer push events (with automatic lifecycle cleanup)
|
|
onFeatureEvent: createEventSubscription<EventDataType>('feature:event'),
|
|
};
|
|
|
|
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
|
|
```
|
|
|
|
**Non-negotiable rules:**
|
|
|
|
- **Never** expose `ipcRenderer` directly — this is a **Catastrophic Failure** pattern.
|
|
- **Never** expose `ipcRenderer.on` without cleanup — use `createEventSubscription()`, which returns an unsubscribe function for React `useEffect` lifecycle management.
|
|
- **Always** declare explicit TypeScript types for all function signatures.
|
|
|
|
### Step 3: Declare the Interface — React Application
|
|
|
|
**File:** `apps/web/src/types/electron.d.ts`
|
|
|
|
```typescript
|
|
interface ElectronAPI {
|
|
// ... existing methods ...
|
|
|
|
featureAction: (arg1: string, arg2: number) => Promise<ResultType>;
|
|
featureFire: (data: SomeType) => void;
|
|
onFeatureEvent: (callback: (data: EventDataType) => void) => () => void;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Verified Channel Manifest
|
|
|
|
The following is the **complete, authoritative registry** of all authorized IPC channels. These channels are the **only** permitted vectors for native interaction. Any IPC channel not listed here is unauthorized and must be treated as a security anomaly.
|
|
|
|
### Printer Subsystem
|
|
|
|
| Channel | Direction | Pattern | Payload | Access Control |
|
|
| ------------------ | -------------------------- | ------------------- | --------------------------------------------------------------------- | ------------------------------ |
|
|
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` | Read-only hardware enumeration |
|
|
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Accepts `ElectronPrintOptions`, returns `{ success, failureReason? }` | Controlled hardware invocation |
|
|
|
|
**Main process handler:** `setupPrinterIPC()` in `src/main/index.ts`
|
|
|
|
**Preload Gateway surface:**
|
|
|
|
```typescript
|
|
getPrinters: () => ipcRenderer.invoke('printer:get-list');
|
|
print: (options?) => ipcRenderer.invoke('printer:print', options);
|
|
```
|
|
|
|
**React consumption hook:** `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
|
|
|
|
---
|
|
|
|
### Auto-Updater Subsystem
|
|
|
|
| Channel | Direction | Pattern | Payload | Access Control |
|
|
| ----------------------- | --------------- | ------------------- | -------------------------------------------------------------- | ---------------------------------- |
|
|
| `updater:check` | Renderer → Main | `invoke` / `handle` | Returns update check result | Read-only version query |
|
|
| `updater:install` | Renderer → Main | `send` / `on` | No payload | Privileged: quits app and installs |
|
|
| `updater:checking` | Main → Renderer | `send` | No payload | Status notification |
|
|
| `updater:available` | Main → Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
|
|
| `updater:not-available` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
|
| `updater:progress` | Main → Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
|
|
| `updater:downloaded` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
|
| `updater:error` | Main → Renderer | `send` | Error message string | Error telemetry |
|
|
|
|
**Main process handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts`
|
|
|
|
**React consumption hook:** `useElectronUpdater()` in `apps/web/src/hooks/use-electron-updater.ts`
|
|
|
|
> [!NOTE]
|
|
> The `updater:install` channel is the **highest-privilege IPC operation** in the system — it terminates the running process and launches a new binary. It should only be triggered by an explicit user action, never automatically.
|
|
|
|
---
|
|
|
|
## Extending the Bridge: Guided Walkthrough
|
|
|
|
**Scenario:** Expose the application version to the React UI.
|
|
|
|
### 1. Main Process — Register Handler
|
|
|
|
```typescript
|
|
// In app.whenReady() callback, src/main/index.ts
|
|
ipcMain.handle('app:get-version', () => {
|
|
return app.getVersion();
|
|
});
|
|
```
|
|
|
|
### 2. Preload Gateway — Expose Method
|
|
|
|
```typescript
|
|
// Add to the electronAPI object, src/preload/index.ts
|
|
const electronAPI = {
|
|
// ... existing methods ...
|
|
getAppVersion: (): Promise<string> => {
|
|
return ipcRenderer.invoke('app:get-version');
|
|
},
|
|
};
|
|
```
|
|
|
|
### 3. TypeScript Interface — Declare Type
|
|
|
|
```typescript
|
|
// Add to ElectronAPI interface, apps/web/src/types/electron.d.ts
|
|
interface ElectronAPI {
|
|
// ... existing methods ...
|
|
getAppVersion: () => Promise<string>;
|
|
}
|
|
```
|
|
|
|
### 4. React — Consume
|
|
|
|
```tsx
|
|
function VersionBadge() {
|
|
const [version, setVersion] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (window.electronAPI) {
|
|
window.electronAPI.getAppVersion().then(setVersion);
|
|
}
|
|
}, []);
|
|
|
|
if (!version) return null;
|
|
return <span className="version-badge">v{version}</span>;
|
|
}
|
|
```
|
|
|
|
### 5. Update This Manifest
|
|
|
|
After implementing a new channel, **add it to the Verified Channel Manifest** in this document. Undocumented channels are unauthorized channels.
|
|
|
|
---
|
|
|
|
## Critical Audit Checklist: Anti-Patterns
|
|
|
|
The following patterns constitute **critical security violations**. Each one expands the attack surface from "browser-level sandboxed web content" to "unrestricted OS-level code execution." Their presence in production code warrants **immediate incident response**.
|
|
|
|
---
|
|
|
|
### ❌ Exposing raw `ipcRenderer`
|
|
|
|
```typescript
|
|
// VIOLATION: Catastrophic Failure — Total Attack Surface Expansion
|
|
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
|
|
```
|
|
|
|
**Threat:** The renderer gains **unrestricted IPC access** — it can invoke any channel, including channels that were never intended to be callable from the renderer. A single XSS vulnerability escalates to arbitrary native code execution.
|
|
|
|
**Classification:** **Total System Compromise**
|
|
|
|
---
|
|
|
|
### ❌ Exposing `require` or Node.js APIs
|
|
|
|
```typescript
|
|
// VIOLATION: Unauthenticated Code Execution
|
|
contextBridge.exposeInMainWorld('require', require);
|
|
```
|
|
|
|
**Threat:** The renderer can `require('child_process').exec('rm -rf /')`. A single XSS vulnerability in _any_ dependency — including transitive ones — escalates to **full filesystem access, credential theft, reverse shells, and data exfiltration**.
|
|
|
|
**Classification:** **Total System Compromise**
|
|
|
|
---
|
|
|
|
### ❌ Enabling `nodeIntegration`
|
|
|
|
```typescript
|
|
// VIOLATION: Catastrophic Failure — Complete Boundary Collapse
|
|
new BrowserWindow({
|
|
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
|
});
|
|
```
|
|
|
|
**Threat:** Every `<script>` tag in the renderer — including XSS payloads, compromised npm packages, and injected analytics scripts — gains full Node.js capabilities. The isolation boundary **ceases to exist**.
|
|
|
|
**Classification:** **Total System Compromise**
|
|
|
|
---
|
|
|
|
### ❌ Passing unsanitized IPC data to shell commands
|
|
|
|
```typescript
|
|
// VIOLATION: Command Injection — Unauthenticated Code Execution
|
|
ipcMain.handle('run-cmd', (_event, cmd: string) => {
|
|
exec(cmd); // The renderer controls the command string
|
|
});
|
|
```
|
|
|
|
**Threat:** The renderer can execute **arbitrary system commands** with the privileges of the Electron main process (typically the current user). This is the most direct path from XSS to OS-level compromise.
|
|
|
|
**Classification:** **Unauthenticated Code Execution**
|
|
|
|
---
|
|
|
|
### ❌ Registering overly broad IPC channels
|
|
|
|
```typescript
|
|
// VIOLATION: Attack Surface Expansion — Unrestricted File Read
|
|
ipcMain.handle('file:read', (_event, path: string) => {
|
|
return readFileSync(path, 'utf-8'); // No validation
|
|
});
|
|
```
|
|
|
|
**Threat:** The renderer can read **any file** on the filesystem — SSH keys, environment files, database credentials, browser cookies. Input validation is not optional.
|
|
|
|
**Classification:** **Sensitive Data Exfiltration**
|
|
|
|
---
|
|
|
|
## The Gold Standard for Native Integration
|
|
|
|
The following patterns represent the **mandatory standard** for all IPC implementations. Adherence is non-negotiable.
|
|
|
|
### ✅ Validate and constrain all IPC arguments
|
|
|
|
```typescript
|
|
// GOLD STANDARD: Input validation, path confinement, scope restriction
|
|
ipcMain.handle('file:read', async (_event, filename: string) => {
|
|
// Reject path separators — confine to a single directory
|
|
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
|
throw new Error('Invalid filename');
|
|
}
|
|
|
|
// Resolve within a controlled directory only
|
|
const safePath = join(app.getPath('userData'), 'data', filename);
|
|
|
|
// Verify the resolved path stays within bounds
|
|
if (!safePath.startsWith(join(app.getPath('userData'), 'data'))) {
|
|
throw new Error('Path traversal detected');
|
|
}
|
|
|
|
return readFileSync(safePath, 'utf-8');
|
|
});
|
|
```
|
|
|
|
**Principle:** Never trust data originating from the renderer. Validate types, constrain scope, and verify resolved paths.
|
|
|
|
---
|
|
|
|
### ✅ Return unsubscribe functions — Memory Leak Mitigation
|
|
|
|
```typescript
|
|
// GOLD STANDARD: The createEventSubscription helper ensures automatic cleanup
|
|
function createEventSubscription<T>(channel: string) {
|
|
return (callback: (data: T) => void): (() => void) => {
|
|
const handler = (_event: Electron.IpcRendererEvent, data: T) => callback(data);
|
|
ipcRenderer.on(channel, handler);
|
|
|
|
// Return an unsubscribe function — critical for React lifecycle
|
|
return () => {
|
|
ipcRenderer.removeListener(channel, handler);
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
**In React's `useEffect`:**
|
|
|
|
```tsx
|
|
useEffect(() => {
|
|
if (!window.electronAPI) return;
|
|
|
|
// Subscribe — handler is registered in the Preload's IPC layer
|
|
const unsub = window.electronAPI.onSomeEvent((data) => {
|
|
setState(data);
|
|
});
|
|
|
|
// Cleanup on unmount — prevents listener accumulation
|
|
return () => unsub();
|
|
}, []);
|
|
```
|
|
|
|
**Principle:** Without the unsubscribe pattern, every component mount adds a **new IPC listener** that persists after unmount. Over time — especially with React's StrictMode double-mounting in development — this causes **memory leaks**, **duplicate event handling**, and **performance degradation**. The `createEventSubscription` helper enforces automatic, deterministic cleanup tied to React's component lifecycle.
|
|
|
|
---
|
|
|
|
### ✅ Gate all Electron calls behind runtime detection
|
|
|
|
```typescript
|
|
// GOLD STANDARD: Environment-safe consumption
|
|
function useElectronFeature() {
|
|
const isElectron = typeof window !== 'undefined' && !!window.electronAPI;
|
|
|
|
const doSomething = useCallback(() => {
|
|
if (!window.electronAPI) return; // No-op in browser
|
|
window.electronAPI.someMethod();
|
|
}, []);
|
|
|
|
return { isElectron, doSomething };
|
|
}
|
|
```
|
|
|
|
**Principle:** The React app must run identically in both Electron and standard browser environments. All `window.electronAPI` access must be gated behind a runtime check. Never assume the IPC bridge exists.
|