Files
trackgo-fe/apps/desktop/docs/IPC_ARCHITECTURE.md
T
Firman Ramdhani 75deeece9f refactor: Revise configuration and IPC architecture documentation for clarity and security enhancements
- Updated CONFIGURATION.md to reflect changes in target app orchestration, environment variables, and production routing.
- Enhanced IPC_ARCHITECTURE.md with a focus on privilege separation, standardized operating procedures, and critical audit checklists.
- Added detailed guidelines for extending the IPC bridge and maintaining security integrity.
- Introduced new package scripts for macOS, Windows, and Linux builds in package.json.
2026-04-06 10:17:32 +07:00

421 lines
18 KiB
Markdown

# 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
```
┌──────────────────────────────────────────────────────────────────┐
│ MAIN PROCESS [Fully Trusted] │
│ │
│ UNRESTRICTED PRIVILEGES │
│ Filesystem · Network · Printers · Native APIs · Child Processes │
│ Auto-Updater · OS Integration · System Notifications │
│ │
│ ipcMain.handle('channel', handler) ← Command handlers │
│ ipcMain.on('channel', handler) ← Event listeners │
│ webContents.send('channel', data) ← Downstream push │
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
│ PRELOAD SCRIPT [Secure Gateway] │
│ │
│ HERMETICALLY SEALED CONTEXT │
│ Performs Interface Narrowing: transforms broad IPC capabilities │
│ into a minimal, auditable API surface. Acts as the sole │
│ authorized mediator between trusted and untrusted contexts. │
│ │
│ contextBridge.exposeInMainWorld('electronAPI', { ... }) │
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
│ RENDERER [Zero-Trust Environment] │
│ │
│ UNTRUSTED WEB CONTENT │
│ Standard browser sandbox. Zero access to: require, __dirname, │
│ process, fs, child_process, net, os, ipcRenderer. │
│ │
│ ONLY authorized interaction vector: │
│ window.electronAPI.methodName(args) │
└──────────────────────────────────────────────────────────────────┘
```
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.