nomyo-js/docs/SECURITY.md

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:

  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):

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 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