# Security Documentation - NOMYO.js ## Overview NOMYO.js implements end-to-end encryption for OpenAI-compatible chat completions using hybrid cryptography. This document details the security architecture, current implementation status, and limitations. --- ## Encryption Architecture ### Hybrid Encryption (AES-256-GCM + RSA-OAEP) **Request Encryption Flow:** 1. Client generates ephemeral AES-256 key (32 bytes) 2. Payload serialized to JSON and encrypted with AES-256-GCM 3. AES key encrypted with server's RSA-4096 public key (RSA-OAEP-SHA256) 4. Encrypted package sent to server with client's public key **Response Decryption Flow:** 1. Server encrypts response with new AES-256 key 2. AES key encrypted with client's RSA public key 3. Client decrypts AES key with private RSA key 4. Client decrypts response with AES key ### Cryptographic Primitives | Component | Algorithm | Parameters | |-----------|-----------|------------| | **Symmetric Encryption** | AES-256-GCM | 256-bit key, 96-bit nonce, 128-bit tag | | **Asymmetric Encryption** | RSA-OAEP | 4096-bit modulus, SHA-256 hash, MGF1-SHA256 | | **Key Derivation** | PBKDF2 | 100,000 iterations, SHA-256, 16-byte salt | | **Private Key Encryption** | AES-256-CBC | 256-bit key (from PBKDF2), 128-bit IV | --- ## Current Implementation Status ### ✅ Fully Implemented 1. **Web Crypto API Integration** - Platform-agnostic cryptography (Node.js 15+ and modern browsers) - Hardware-accelerated when available - Constant-time operations (timing attack resistant) 2. **Hybrid Encryption** - AES-256-GCM for payload encryption - RSA-OAEP-SHA256 for key exchange - Authenticated encryption (GCM provides AEAD) - Unique nonce per encryption (96-bit random) 3. **Key Management** - 4096-bit RSA keys (default, configurable to 2048) - Automatic key generation on first use - File-based persistence (Node.js) - In-memory keys (browsers) - Password protection via PBKDF2 + AES-256-CBC (minimum 8-character password enforced) - Automatic periodic key rotation (default: 24 hours, configurable, or disabled with `keyRotationInterval: 0`) - `dispose()` method severs in-memory key references and cancels the rotation timer 4. **Transport Security** - HTTPS enforcement using proper URL parsing (`new URL()`) — not string prefix matching - Certificate validation (browsers/Node.js) - Optional HTTP for local development (explicit opt-in) - API key validated to reject CR/LF characters (prevents HTTP header injection) - Server error detail truncated to 100 printable characters (prevents log injection) 5. **Memory Protection (Pure JavaScript)** - Immediate zeroing of sensitive buffers - Context managers for automatic cleanup (`SecureByteContext`) with guarded `finally` blocks - Intermediate crypto buffers (password bytes, salt, IV) wrapped in `SecureByteContext` during key encryption - HTTP request body (`ArrayBuffer`) zeroed after data is handed to the socket - Best-effort memory management 6. **Response Integrity** - Decrypted response validated against required `ChatCompletionResponse` schema fields before use - Generic error messages from all crypto operations (no internal engine details leaked) ### ⚠️ Limitations (Pure JavaScript) 1. **No OS-Level Memory Locking** - Cannot use `mlock()` (Linux/macOS) or `VirtualLock()` (Windows) - JavaScript GC controls memory lifecycle - Memory may be paged to swap - **Impact**: Sensitive data could be written to disk during high memory pressure 2. **Memory Zeroing Only** - Zeroes `ArrayBuffer` contents immediately after use - Cannot prevent GC from copying data internally - Cannot guarantee memory won't be swapped - **Mitigation**: Minimizes exposure window 3. **Browser Limitations** - Keys not persisted (in-memory only) - No file system access - Subject to browser security policies - **Impact**: Keys regenerated on each page load --- ## Security Best Practices ### Deployment ✅ **DO:** - Use HTTPS in production (enforced by default) - Enable secure memory protection (default: `secureMemory: true`) - Use password-protected private keys in Node.js (minimum 8 characters) - Set private key file permissions to 600 (owner-only) - Rely on automatic key rotation (`keyRotationInterval`, default 24h) to limit fingerprint lifetime - Call `dispose()` when the client is no longer needed - Validate server public key fingerprint on first use ❌ **DON'T:** - Use HTTP in production (only for localhost development) - Disable secure memory unless absolutely necessary - Store unencrypted private keys - Share private keys across systems - Store keys in public repositories ### Key Management **Node.js (Recommended):** ```javascript const client = new SecureCompletionClient({ routerUrl: 'https://...' }); // Generate with password protection await client.generateKeys({ saveToFile: true, keyDir: 'client_keys', password: process.env.KEY_PASSWORD // From environment variable }); ``` **Browsers (In-Memory):** ```javascript // Keys generated automatically, not persisted const client = new SecureChatCompletion({ baseUrl: 'https://...' }); ``` ### Environment Variables ```bash # .env file (never commit to git) NOMYO_API_KEY=your-api-key NOMYO_KEY_PASSWORD=your-key-password NOMYO_SERVER_URL=https://api.nomyo.ai:12434 ``` --- ## Cryptographic Implementation Details ### AES-256-GCM ```typescript // Key generation const key = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, // extractable ['encrypt', 'decrypt'] ); // Encryption const nonce = crypto.getRandomValues(new Uint8Array(12)); // 96 bits const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: nonce, tagLength: 128 }, key, plaintext ); ``` **Security Properties:** - **Authenticated Encryption with Associated Data (AEAD)** - **128-bit authentication tag** prevents tampering - **Unique nonce requirement** - Never reuse nonce with same key - **Ephemeral keys** - New AES key per request provides forward secrecy ### RSA-OAEP ```typescript // Key generation const keyPair = await crypto.subtle.generateKey( { name: 'RSA-OAEP', modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), // 65537 hash: 'SHA-256' }, true, ['encrypt', 'decrypt'] ); // Encryption const encrypted = await crypto.subtle.encrypt( { name: 'RSA-OAEP' }, publicKey, data ); ``` **Security Properties:** - **OAEP padding** prevents chosen ciphertext attacks - **SHA-256 hash** in MGF1 mask generation - **4096-bit keys** provide ~152-bit security level - **No label** (standard practice for hybrid encryption) ### Password-Protected Keys ```typescript // Derive key from password const salt = crypto.getRandomValues(new Uint8Array(16)); const passwordKey = await crypto.subtle.importKey( 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey'] ); const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt, iterations: 100000, // OWASP recommendation hash: 'SHA-256' }, passwordKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt'] ); // Encrypt private key const iv = crypto.getRandomValues(new Uint8Array(16)); const encrypted = await crypto.subtle.encrypt( { name: 'AES-CBC', iv: iv }, derivedKey, privateKeyData ); // Store: salt + iv + encrypted ``` **Security Properties:** - **100,000 PBKDF2 iterations** (meets OWASP 2023 recommendations) - **SHA-256 hash function** - **16-byte random salt** (unique per key) - **AES-256-CBC** for key encryption --- ## Memory Protection Details ### Current Implementation (Pure JavaScript) ```typescript class SecureByteContext { async use(fn: (data: ArrayBuffer) => Promise): Promise { try { return await fn(this.data); } finally { // Always zero, even if exception occurs. // zeroMemory failure is swallowed so it cannot mask the original error. if (this.useSecure) { try { this.secureMemory.zeroMemory(this.data); } catch (_zeroErr) { /* intentional */ } } } } } ``` **What it does:** - ✅ Zeroes memory immediately after use - ✅ Guarantees cleanup even on exceptions - ✅ Minimizes exposure window **What it cannot do:** - ❌ Prevent JavaScript GC from copying data - ❌ Lock memory pages (no swap) - ❌ Prevent core dumps from containing data - ❌ Guarantee OS won't page data to disk ### Future: Native Addon (Optional) A native Node.js addon can provide true memory protection: **Linux/macOS:** ```c #include // Lock memory mlock(data, length); // Zero and unlock memset(data, 0, length); munlock(data, length); ``` **Windows:** ```c #include // Lock memory VirtualLock(data, length); // Zero and unlock SecureZeroMemory(data, length); VirtualUnlock(data, length); ``` **Installation:** ```bash # Optional dependency npm install nomyo-native # Will use native addon if available, fallback to pure JS otherwise ``` --- ## Threat Model ### Protected Against ✅ **Network Eavesdropping** - All data encrypted end-to-end - HTTPS transport encryption - Authenticated encryption prevents tampering ✅ **MITM Attacks** - HTTPS certificate validation - Server public key verification - Warning on HTTP usage ✅ **Replay Attacks** - Unique nonce per encryption - Authenticated encryption with GCM - Server timestamp validation (server-side) ✅ **Timing Attacks (Partial)** - Web Crypto API uses constant-time operations - No length leakage in comparisons - Generic error messages from all crypto operations (RSA, AES) — internal engine errors not forwarded ✅ **Concurrent Key Generation Race** - Promise-chain mutex serialises all `ensureKeys()` callers - No risk of multiple simultaneous key generations overwriting each other ✅ **Key Compromise (Forward Secrecy)** - Ephemeral AES keys - Each request uses new AES key - Compromise of one key affects only that request ### Not Protected Against (Pure JS) ⚠️ **Memory Inspection** - Admin/root can read process memory - Debuggers can access sensitive data - Core dumps may contain keys - **Mitigation**: Use native addon for `mlock` support ⚠️ **Swap File Exposure** - OS may page memory to disk - Sensitive data could persist in swap - **Mitigation**: Disable swap or use native addon ⚠️ **Local Malware** - Keyloggers can capture passwords - Memory scrapers can extract keys - **Mitigation**: Standard OS security practices --- ## Comparison: JavaScript vs Python Implementation | Feature | Python | JavaScript (Pure) | JavaScript (+ Native Addon) | |---------|--------|-------------------|----------------------------| | **Encryption** | AES-256-GCM | ✅ AES-256-GCM | ✅ AES-256-GCM | | **Key Exchange** | RSA-OAEP-4096 | ✅ RSA-OAEP-4096 | ✅ RSA-OAEP-4096 | | **Memory Locking** | ✅ `mlock` | ❌ Not available | ✅ `mlock` | | **Memory Zeroing** | ✅ Guaranteed | ✅ Best-effort | ✅ Guaranteed | | **Key Persistence** | ✅ File-based | ✅ Node.js only | ✅ Node.js only | | **Browser Support** | ❌ | ✅ | ❌ | | **Zero Dependencies** | ❌ | ✅ | ❌ (native addon) | --- ## Audit & Compliance ### Recommendations for Production 1. **Code Review** - Review cryptographic implementations - Verify key generation randomness - Check for timing vulnerabilities 2. **Penetration Testing** - Test against MITM attacks - Verify HTTPS enforcement - Test key management security 3. **Compliance** - Document security architecture - Risk assessment for pure JS vs native - Decide if `mlock` is required for your use case ### Known Limitations This implementation uses **pure JavaScript** without native addons. For maximum security: - Consider implementing the optional native addon - Or use the Python client in security-critical server environments - Or accept the risk given other security controls (encrypted disk, no swap, etc.) --- ## References - [Web Crypto API Specification](https://www.w3.org/TR/WebCryptoAPI/) - [NIST SP 800-38D](https://csrc.nist.gov/publications/detail/sp/800-38d/final) - GCM mode - [RFC 8017](https://datatracker.ietf.org/doc/html/rfc8017) - RSA-OAEP - [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)