nomyo-js/docs/SECURITY.md

423 lines
12 KiB
Markdown

# 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<T>(fn: (data: ArrayBuffer) => Promise<T>): Promise<T> {
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 <sys/mman.h>
// Lock memory
mlock(data, length);
// Zero and unlock
memset(data, 0, length);
munlock(data, length);
```
**Windows:**
```c
#include <windows.h>
// 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)