diff --git a/doc/SECURITY.md b/doc/SECURITY.md new file mode 100644 index 0000000..e229b6a --- /dev/null +++ b/doc/SECURITY.md @@ -0,0 +1,423 @@ +# 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 +``` + +--- + +## 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)