nomyo-js/doc/security-guide.md
alpha nerd 987acf8816
Some checks failed
NYX Security Scan / nyx-scan (pull_request) Failing after 5m51s
feat!: make RSA keys ephemeral by default
BREAKING CHANGE: keyDir now defaults to null (ephemeral) instead of
'client_keys'. Clients that relied on keys persisting across restarts
must now pass keyDir explicitly.

The Python SDK defaults to key_dir=None: a key pair is generated in
memory for the session and never written to disk. The JS port defaulted
to 'client_keys' and always persisted, so merely constructing a client
wrote an RSA private key into the working directory. That is a weaker
default than the client it ports, and one users never asked for.

  - keyDir?: string | null, defaulting to undefined. null and undefined
    both mean ephemeral, matching Python's None.
  - Persistent mode is unchanged when keyDir is set: load the existing
    pair from that directory, otherwise generate and save one there.
  - Browsers are always ephemeral; they have no filesystem.

Key rotation follows the same rule. It previously hardcoded
'client_keys' as its fallback directory, so an ephemeral client would
have started writing private keys to disk on the first rotation tick.
Rotated keys are now persisted only where keyDir or keyRotationDir is
explicitly configured.

Tests assert the intent (that saveKeys is never called) rather than
probing the filesystem, since a leftover client_keys/ from the old
default would otherwise make them pass or fail for the wrong reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:19:03 +02:00

8.6 KiB

Security Guide

Overview

NOMYO.js provides end-to-end encryption for all communication between your application and NOMYO inference endpoints. Your prompts and responses are encrypted before leaving your process — the inference server never processes plaintext.

For the full cryptographic architecture and threat model see SECURITY.md.


Encryption Mechanism

Hybrid Encryption

Each request uses a two-layer scheme:

  1. AES-256-GCM encrypts the payload (authenticated encryption — prevents tampering).
  2. RSA-OAEP-SHA256 wraps the AES key for secure key exchange.

The server holds the RSA private key; your client generates the AES key fresh for every request.

Per-Request Ephemeral AES Keys

  • A new 256-bit AES key is generated for every create() call using the Web Crypto API.
  • The key is never reused — forward secrecy is ensured per request.
  • The key is zeroed from memory immediately after encryption.

Key Exchange

Your client's RSA public key is sent in the X-Public-Key request header. The server encrypts the response with it so only your client can decrypt the reply.


Memory Protection

What the Library Does

All intermediate sensitive buffers (AES key, plaintext payload, decrypted response bytes) are wrapped in SecureByteContext. This guarantees they are zeroed in a finally block immediately after use, even if an exception occurs.

The encrypted request body (ArrayBuffer) is also zeroed by the Node.js HTTP client after the data is handed to the socket.

Limitations (Pure JavaScript)

JavaScript has no direct access to OS memory management. The library cannot:

  • Lock pages to prevent swapping (mlock / VirtualLock)
  • Prevent the garbage collector from copying data internally
  • Guarantee memory won't appear in core dumps

Impact: On a system under memory pressure, sensitive data could briefly be written to swap. For environments where this is unacceptable (PHI, classified), install the optional native addon or run on a system with swap disabled.

Native Addon (Optional)

The nomyo-native addon adds true mlock support. When installed, getMemoryProtectionInfo() reports method: 'mlock' and canLock: true:

import { getMemoryProtectionInfo } from 'nomyo-js';

const info = getMemoryProtectionInfo();
// Without addon: { method: 'zero-only', canLock: false }
// With addon:    { method: 'mlock',     canLock: true  }

Minimise Response Lifetime

The library protects all intermediate crypto material in secure memory. However, the final parsed response object is returned to your code, and you are responsible for how long it lives.

// GOOD — extract what you need, then drop the response immediately
const response = await client.create({
  model: 'Qwen/Qwen3.5-9B',
  messages: [{ role: 'user', content: 'Summarise patient record #1234' }],
  security_tier: 'maximum',
});
const reply = response.choices[0].message.content;
// Let response go out of scope here — don't hold it in a variable
// longer than necessary

// BAD — holding the full response object in a long-lived scope
this.lastResponse = response;  // stored for minutes / hours

JavaScript's delete and variable reassignment do not zero the underlying memory. For sensitive data (PHI, classified), process and discard as quickly as possible — do not store in class attributes, global caches, or log files.


Key Management

Default Behaviour

Keys are ephemeral by default: a fresh pair is generated in memory on first use, lives only for the lifetime of the client, and is never written to disk. Nothing to protect at rest, nothing to rotate out of a directory, no key material surviving the process. This matches the Python SDK's key_dir=None default.

To reuse a key pair across runs, set keyDir explicitly (Node.js only):

const client = new SecureChatCompletion({
  baseUrl: 'https://api.nomyo.ai',
  keyDir: '/var/lib/myapp/nomyo-keys',
});

The directory is created on first use and reloaded on subsequent runs:

<keyDir>/
  private_key.pem    # permissions 0600 (owner-only)
  public_key.pem     # permissions 0644

Persisting keys is a deliberate trade-off: it buys session continuity at the cost of a long-lived private key on disk. Prefer the ephemeral default unless you specifically need key reuse.

Configure the Key Directory

const client = new SecureChatCompletion({
  apiKey: process.env.NOMYO_API_KEY,
  keyDir: '/etc/myapp/nomyo-keys',  // custom path, outside project directory
});

Protect key files with a password so they cannot be used even if the file is leaked:

import { SecureCompletionClient } from 'nomyo-js';

const client = new SecureCompletionClient({ routerUrl: 'https://api.nomyo.ai' });

await client.generateKeys({
  saveToFile: true,
  keyDir: 'client_keys',
  password: process.env.NOMYO_KEY_PASSWORD,  // minimum 8 characters
});

To load password-protected keys manually:

await client.loadKeys(
  'client_keys/private_key.pem',
  'client_keys/public_key.pem',
  process.env.NOMYO_KEY_PASSWORD
);

Key Rotation

Keys rotate automatically every 24 hours by default. Configure or disable:

const client = new SecureChatCompletion({
  apiKey: process.env.NOMYO_API_KEY,
  keyRotationInterval: 3600000,              // rotate every hour
  keyRotationDir: '/var/lib/myapp/keys',
  keyRotationPassword: process.env.KEY_PWD,
});

// Or disable entirely for short-lived processes
const client2 = new SecureChatCompletion({
  apiKey: process.env.NOMYO_API_KEY,
  keyRotationInterval: 0,
});

File Permissions

This applies only when you opt into persistence via keyDir; ephemeral keys never reach the filesystem. Private key files are saved with 0600 permissions (owner read/write only) on Unix-like systems. Add your keyDir and *.pem to your .gitignoreclient_keys/ and *.pem are already included if you use this package's default .gitignore.


Security Tiers

Tier Hardware Use case
"standard" GPU General secure inference
"high" CPU/GPU balanced Sensitive business data, enforces secure tokenizer
"maximum" CPU only HIPAA PHI, classified data — maximum isolation

Higher tiers add round-trip latency but increase hardware-level isolation.


HTTPS Enforcement

The client enforces HTTPS by default. HTTP connections require explicit opt-in and print a visible warning:

// Production — HTTPS only (default)
const client = new SecureChatCompletion({ baseUrl: 'https://api.nomyo.ai' });

// Local development — HTTP allowed with explicit flag
const devClient = new SecureChatCompletion({
  baseUrl: 'http://localhost:12435',
  allowHttp: true,   // prints: "WARNING: Using HTTP instead of HTTPS..."
});

Without allowHttp: true, connecting over HTTP throws SecurityError.

The server's public key is fetched over HTTPS with TLS certificate verification to prevent man-in-the-middle attacks.


API Key Security

API keys are sent as Bearer tokens in the Authorization header. The client validates that the key does not contain CR or LF characters to prevent HTTP header injection.

Never hardcode API keys in source code — use environment variables:

const client = new SecureChatCompletion({
  apiKey: process.env.NOMYO_API_KEY,
});

Production Checklist

  • Always use HTTPS (allowHttp is false by default)
  • Load API key from environment variable, not hardcoded
  • Enable secureMemory: true (default)
  • Prefer the ephemeral key default; only set keyDir if you genuinely need key reuse across runs
  • If persisting keys: use password-protected key files (keyRotationPassword)
  • If persisting keys: store them outside the project directory and outside version control
  • If persisting keys: add your keyDir and *.pem to .gitignore
  • Call client.dispose() when the client is no longer needed
  • Consider the native addon if swap-file exposure is unacceptable

Compliance Considerations

HIPAA

For Protected Health Information (PHI):

  • Use security_tier: 'maximum' on requests containing PHI
  • Enable password-protected key files
  • Ensure HTTPS is enforced (the default)
  • Minimise response lifetime in memory (extract, use, discard)

Data Classification

Classification Recommended tier
Public / internal "standard"
Confidential business data "high"
PHI, PII, classified "maximum"