12 KiB
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:
- Client generates ephemeral AES-256 key (32 bytes)
- Payload serialized to JSON and encrypted with AES-256-GCM
- AES key encrypted with server's RSA-4096 public key (RSA-OAEP-SHA256)
- Encrypted package sent to server with client's public key
Response Decryption Flow:
- Server encrypts response with new AES-256 key
- AES key encrypted with client's RSA public key
- Client decrypts AES key with private RSA key
- 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
-
Web Crypto API Integration
- Platform-agnostic cryptography (Node.js 15+ and modern browsers)
- Hardware-accelerated when available
- Constant-time operations (timing attack resistant)
-
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)
-
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
-
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)
- HTTPS enforcement using proper URL parsing (
-
Memory Protection (Pure JavaScript)
- Immediate zeroing of sensitive buffers
- Context managers for automatic cleanup (
SecureByteContext) with guardedfinallyblocks - Intermediate crypto buffers (password bytes, salt, IV) wrapped in
SecureByteContextduring key encryption - HTTP request body (
ArrayBuffer) zeroed after data is handed to the socket - Best-effort memory management
-
Response Integrity
- Decrypted response validated against required
ChatCompletionResponseschema fields before use - Generic error messages from all crypto operations (no internal engine details leaked)
- Decrypted response validated against required
⚠️ Limitations (Pure JavaScript)
-
No OS-Level Memory Locking
- Cannot use
mlock()(Linux/macOS) orVirtualLock()(Windows) - JavaScript GC controls memory lifecycle
- Memory may be paged to swap
- Impact: Sensitive data could be written to disk during high memory pressure
- Cannot use
-
Memory Zeroing Only
- Zeroes
ArrayBuffercontents immediately after use - Cannot prevent GC from copying data internally
- Cannot guarantee memory won't be swapped
- Mitigation: Minimizes exposure window
- Zeroes
-
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):
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):
// Keys generated automatically, not persisted
const client = new SecureChatCompletion({ baseUrl: 'https://...' });
Environment Variables
# .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
// 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
// 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
// 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)
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:
#include <sys/mman.h>
// Lock memory
mlock(data, length);
// Zero and unlock
memset(data, 0, length);
munlock(data, length);
Windows:
#include <windows.h>
// Lock memory
VirtualLock(data, length);
// Zero and unlock
SecureZeroMemory(data, length);
VirtualUnlock(data, length);
Installation:
# 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
mlocksupport
⚠️ 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
-
Code Review
- Review cryptographic implementations
- Verify key generation randomness
- Check for timing vulnerabilities
-
Penetration Testing
- Test against MITM attacks
- Verify HTTPS enforcement
- Test key management security
-
Compliance
- Document security architecture
- Risk assessment for pure JS vs native
- Decide if
mlockis 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
- NIST SP 800-38D - GCM mode
- RFC 8017 - RSA-OAEP
- OWASP Password Storage Cheat Sheet