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

251 lines
8.6 KiB
Markdown

# 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](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`:
```javascript
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.
```javascript
// 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):
```typescript
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
```javascript
const client = new SecureChatCompletion({
apiKey: process.env.NOMYO_API_KEY,
keyDir: '/etc/myapp/nomyo-keys', // custom path, outside project directory
});
```
### Password-Protected Keys (Recommended for Production)
Protect key files with a password so they cannot be used even if the file is leaked:
```javascript
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:
```javascript
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:
```javascript
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 `.gitignore``client_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:
```javascript
// 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:
```javascript
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"` |