STVOR SDK
Production-grade E2EE with Signal Protocol — minimal API
STVOR v3.0.0 — настоящее end-to-end шифрование на основе Signal Protocol (X3DH + Double Ratchet). Сообщения шифруются на клиенте — сервер никогда не видит открытый текст. Ноль внешних крипто-зависимостей — только Node.js built-in crypto module.
- X3DH key agreement (symmetric)
- Double Ratchet (Signal)
- AES-256-GCM + AAD
- ECDSA P-256 подписи
- HKDF-SHA256 деривация
- Forward Secrecy (DH ratchet)
- Post-Compromise Security
- Replay protection
- TOFU identity verification
- Crypto-verified metrics
Installation
Install the SDK (v3.0.0)
npm install @stvor/sdk@^3.0.0
📋 Architecture: Three Roles
SDK (trusted) — handles all encryption/decryption. Never sends plaintext.
Backend (trusted) — verifies metrics proofs, stores audit log. Never decrypts.
Display/Relay (untrusted) — routes encrypted messages, displays read-only metrics.
Module Setup (ESM & CommonJS)
STVOR SDK supports both ESM and CommonJS. Choose the format that matches your project:
ESM (recommended)
Works with "type": "module" in package.json or .mjs files:
import { Stvor } from '@stvor/sdk';
const app = await Stvor.init({
appToken: 'stvor_dev_test123',
relayUrl: 'ws://localhost:4444'
});
CommonJS
Works with require() in .cjs files or projects without "type": "module":
const sdk = require('@stvor/sdk');
// Use the async loader
const { Stvor } = await sdk.load();
const app = await Stvor.init({
appToken: 'stvor_dev_test123',
relayUrl: 'ws://localhost:4444'
});
TypeScript
Full type support out of the box. Set moduleResolution to "NodeNext" or "Bundler":
// tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2020"
}
}
📋 package.json для нового проекта
{
"name": "my-e2ee-app",
"type": "module",
"dependencies": {
"@stvor/sdk": "^3.0.0",
"ws": "^8.13.0"
}
}
Quick Start (v3.0.0)
Два пользователя, зашифрованное сообщение — без знания криптографии:
import { createApp } from '@stvor/sdk';
// Инициализация
const app = await createApp({
appToken: 'sk_live_xxx',
relayUrl: 'http://localhost:3002'
});
// Подключение пользователей
const alice = await app.connect('alice@example.com');
const bob = await app.connect('bob@example.com');
// Подписка на сообщения
bob.onMessage((msg) => {
console.log(`📩 ${msg.senderId}: ${msg.content}`);
});
// E2EE отправка — X3DH + Double Ratchet автоматически
await alice.send('bob@example.com', 'Hello! 🔐');
console.log('✅ Sent');
Minimal Example (Dev Mode)
For development/testing without storage adapters:
import { createApp } from '@stvor/sdk';
const app = await createApp({
appToken: 'demo_token',
relayUrl: 'ws://localhost:8080'
// Storage adapters optional in dev mode
});
const user = await app.connect('user@test.com');
user.onMessage((from, msg) => console.log(msg));
await user.send('peer@test.com', 'Encrypted message!');
Local Development Server
STVOR SDK включает встроенный mock relay сервер для локальной разработки без интернета и без необходимости запускать свой relay:
# Запуск через npx (рекомендуется)
npx @stvor/sdk mock-relay
# Или с кастомным портом
STVOR_MOCK_PORT=9000 npx @stvor/sdk mock-relay
# С подробным логированием
STVOR_MOCK_VERBOSE=1 npx @stvor/sdk mock-relay
✅ Что Mock Relay умеет:
- WebSocket на
ws://localhost:4444 - Health check:
GET http://localhost:4444/health - Автороутинг сообщений между подключёнными клиентами
- Хранение сообщений для оффлайн-получателей
- Принимает любой AppToken начинающийся с
stvor_
Использование в коде
import { Stvor } from '@stvor/sdk';
// Укажите адрес mock relay вместо продакшен-сервера
const app = await Stvor.init({
appToken: 'stvor_dev_test123', // любой токен с stvor_ префиксом
relayUrl: 'ws://localhost:4444' // mock relay
});
const alice = await app.connect('alice');
const bob = await app.connect('bob');
bob.onMessage((from, msg) => {
console.log(`${from}: ${msg}`);
});
await alice.send('bob', 'Hello from local dev! 🔐');
Проверка статуса
# Health check
curl http://localhost:4444/health
# Проверить пользователя онлайн
curl http://localhost:4444/status/alice
What's New in v3.0
Breaking Changes from v2.4
| ❌ v2.4 (сломано) | ✅ v3.0 (исправлено) |
| Подпись SPK через HMAC с публичным ключом | Настоящая ECDSA P-256 подпись |
| X3DH с эфемерным ключом внутри функции | Symmetric X3DH — обе стороны получают идентичный секрет |
| Nonce не передаётся при дешифровке | Nonce в заголовке (85 bytes: pubkey + counters + nonce) |
| Заголовок обрезает P-256 ключ (32 из 65 байт) | Полный 65-byte uncompressed P-256 ключ в заголовке |
| DH ratchet условие инвертировано | Корректный DH ratchet с skip message keys |
| Зависимость от libsodium-wrappers | Только Node.js built-in crypto (0 зависимостей) |
Migration Guide: v2.4 → v3.0
v3.0 полностью переписывает крипто-модуль. Сессии, созданные в v2.4, несовместимы с v3.0. Все пользователи должны переподключиться.
Шаг 1: Обновите SDK
# Обновление до v3.0.0
npm install @stvor/sdk@^3.0.0
# Удалите libsodium (больше не нужен)
npm uninstall libsodium-wrappers
Шаг 2: Удалите старые сессии
Крипто-формат полностью изменился. Старые сессии не могут быть десериализованы:
# Если используете Redis — очистите кеш сессий:
redis-cli FLUSHDB
# Если используете PostgreSQL:
# DELETE FROM sessions;
# DELETE FROM tofu_fingerprints;
Шаг 3: Обновите код (если использовали низкоуровневое API)
Высокоуровневый API (createApp, Stvor.init, send, onMessage) — не изменился. Если вы использовали только его — обновление автоматическое.
Если вы использовали ratchet/crypto-session напрямую:
// ❌ v2.4 — libsodium, внешний nonce
import sodium from 'libsodium-wrappers';
await sodium.ready;
const kp = sodium.crypto_box_keypair();
const { ciphertext, nonce, header } = encrypt(msg);
// header.publicKey — обрезанный 32 байта
// header.nonce — отдельное поле
// ✅ v3.0 — Node.js crypto, nonce в заголовке
import { generateKeyPair, encryptMessage } from '@stvor/sdk/ratchet';
const kp = generateKeyPair(); // ECDH P-256
const { ciphertext, header } = encryptMessage(session, plaintext);
// header = 85 bytes (65 pubkey + 4 prevChain + 4 msgNum + 12 nonce)
// Всё в одном base64url строке
Шаг 4: Проверьте работоспособность
# 1. Запустите mock relay
npx @stvor/sdk mock-relay
# 2. В другом терминале — тест E2EE
node -e "
import { createApp } from '@stvor/sdk';
const app = await createApp({ appToken: 'stvor_test', relayUrl: 'ws://localhost:4444' });
const alice = await app.connect('alice');
const bob = await app.connect('bob');
bob.onMessage((from, msg) => { console.log('✅ E2EE работает:', msg); process.exit(0); });
await alice.send('bob', 'Hello from v3.0! 🔐');
"
Что изменилось внутри
| Компонент | v2.4 | v3.0 |
| Крипто-библиотека | libsodium-wrappers | node:crypto (built-in) |
| Key exchange | X25519 (асимметричный X3DH) | ECDH P-256 (symmetric X3DH) |
| Подписи SPK | HMAC (подделываемые) | ECDSA P-256 (настоящие цифровые подписи) |
| Шифрование | XChaCha20-Poly1305 | AES-256-GCM + AAD |
| Заголовок | Обрезанный (32/65 байт) | Полный 85 байт (65 pub + 4+4 counters + 12 nonce) |
| Nonce | Не передавался | Внутри заголовка (байты 73-84) |
| Внешние зависимости | libsodium-wrappers (~500KB) | 0 (только Node.js built-in) |
✅ После миграции:
- Все 23 интеграционных теста проходят
- Размер бандла уменьшен (нет libsodium)
- Настоящая ECDSA верификация SPK
- Nonce корректно передаётся в заголовке
- DH ratchet работает правильно
Production Architecture
Component Roles
| 📂 Component | Role | Trust Level | Handles Data |
| SDK | Encryption/decryption, signing | ✅ Trusted | Plaintext, keys |
| Relay | Route encrypted messages | ⚠️ Untrusted | Ciphertexts only |
| Backend | Verify metrics, audit, storage | ✅ Trusted | Signatures, ciphertexts, audit log |
| Dashboard | Display metrics (read-only) | ⚠️ Untrusted | Verified counts only |
Production vs Development Modes
| Property | Dev Mode | Prod Mode |
| Encryption | ✅ Full | ✅ Full |
| Replay Protection | ❌ Memory only | ✅ Persistent |
| Session Persistence | ❌ Lost on restart | ✅ Survives restarts |
| Storage Adapters | ⚠️ Optional | ✅ Required |
| Metrics Verification | ❌ None (dev logging) | ✅ HMAC-signed |
| Startup Failure | ❌ Warnings only | ✅ Hard fail |
⚠️ Dev mode ensures encryption but disables durability guarantees. Suitable for local development and testing. Production fails intentionally if storage adapters are missing.
Storage Adapters (Required for Production)
Production mode requires pluggable storage. SDK will refuse to start if adapters are missing.
// Storage Adapter Types (implement these interfaces)
interface IReplayCache {
// Check if nonce was seen, mark if not
checkAndMark(peerId: string, nonce: Uint8Array): Promise
}
interface ITofuStore {
// Store & verify fingerprints (immutable)
storeFingerprint(peerId: string, fingerprint: string): Promise
verifyFingerprint(peerId: string, fingerprint: string): Promise
}
interface ISessionStore {
// Persist encrypted session state
save(peerId: string, state: SessionState): Promise
load(peerId: string): Promise
}
interface IIdentityStore {
// Get or generate identity keypair (immutable)
getIdentityKeyPair(): Promise
}
Example: Redis + PostgreSQL Setup
import { createApp } from '@stvor/sdk';
import Redis from 'redis';
import { Pool } from 'pg';
// Redis for replay protection (fast, ephemeral)
const redis = Redis.createClient({ url: 'redis://localhost:6379' });
// PostgreSQL for durable storage
const pg = new Pool({
connectionString: 'postgres://user:pass@localhost/stvor'
});
// Create storage adapters
const replayCache = {
async checkAndMark(peerId, nonce) {
const key = `replay:${peerId}:${Buffer.from(nonce).toString('hex')}`;
const exists = await redis.getex(key, { EX: 300 }); // 5 min TTL
if (exists) return true; // Replay detected
await redis.set(key, '1', { EX: 300 });
return false; // First time seeing this nonce
}
};
const sessionStore = {
async save(peerId, state) {
const encrypted = await encryptState(state); // User's KMS
await pg.query(
'INSERT INTO sessions (peer_id, encrypted_state, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (peer_id) DO UPDATE SET encrypted_state = $2, updated_at = NOW()',
[peerId, encrypted]
);
},
async load(peerId) {
const result = await pg.query('SELECT encrypted_state FROM sessions WHERE peer_id = $1', [peerId]);
if (!result.rows[0]) return null;
return await decryptState(result.rows[0].encrypted_state);
}
};
// Create app with adapters
const app = await createApp({
appToken: 'sk_live_xxx',
relayUrl: 'wss://relay.stvor.com',
storageAdapters: {
replay: replayCache,
sessions: sessionStore,
// ... other adapters
},
productionMode: true // Hard-fail if adapters missing
});
Security Model & Threat Assumptions
Threat Model
🎯 Attacker Capabilities (We Protect Against):
- Network-level attacker: Can read/modify all traffic (we use Signal Protocol for protection)
- Relay compromise: Can see ciphertexts, but not read them
- Database compromise: Can see encrypted sessions, but not decrypt
- Metrics tampering: Cannot forge valid attestations (HMAC-signed)
❌ Attacker Limitations (Out of Scope):
- SDK compromise: Cannot prevent if SDK binary is modified
- Both endpoints compromised: Plaintext visible on both ends (by definition)
- KMS compromise: If your key DB is breached, we lose everything
- Endpoint state dumps: Memory forensics can recover keys (use OS protections)
6 Cryptographic Invariants
v3.0 enforces 6 cryptographic invariants that prevent protocol-level attacks:
I1: State Machine Constraint
if (state !== ESTABLISHED) → cannot send() or receive()
Prevents operations on uninitialized or corrupted sessions.
I2: Key Memory Hygiene
rootKey never exists in plaintext after initialization
Reduces window for key compromise from process memory.
I3: Atomic State Transitions
if decrypt() fails → session state unchanged
Prevents state corruption from decryption errors.
I4: AAD Authentication
AAD = HASH(publicKey || nonce || counters || timestamp)
Cryptographically binds metadata to ciphertext. Relay cannot tamper with headers.
I5: Replay Protection
nonce must be unique within 5-minute TTL window
Persistent cache survives process restart.
I6: Identity Immutability
Identity keys generated once, never rotate, stored securely
Trust anchor cannot be compromised by accident.
Full Working Example
Copy these files to get E2EE working in 2 minutes:
Step 1: Create relay-server.mjs
This relay server is a minimal example for local development. It is NOT the STVOR API — it's a simple WebSocket forwarder to help you test encryption. For production, you'll need proper auth, persistence, and scaling.
The relay server forwards encrypted messages between clients. It cannot decrypt anything.
// relay-server.mjs
import WebSocket, { WebSocketServer } from 'ws';
const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });
console.log(`🔌 Relay running on ws://localhost:${PORT}`);
const clients = new Map();
const pubkeys = new Map();
wss.on('connection', (ws) => {
// Send all known keys to new client
for (const [user, pub] of pubkeys.entries()) {
ws.send(JSON.stringify({ type: 'announce', user, pub }));
}
ws.on('message', (data) => {
let msg;
try { msg = JSON.parse(data.toString()); } catch { return; }
if (msg.type === 'announce' && msg.user) {
clients.set(msg.user, ws);
pubkeys.set(msg.user, msg.pub);
// Broadcast to all
for (const [_, client] of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'announce',
user: msg.user,
pub: msg.pub
}));
}
}
}
if (msg.type === 'message' && msg.to) {
const target = clients.get(msg.to);
if (target && target.readyState === WebSocket.OPEN) {
target.send(JSON.stringify(msg));
}
}
});
});
Step 2: Create app.mjs
Your application code with E2EE messaging (only STVOR imports needed!):
// app.mjs
import { createApp } from '@stvor/sdk';
async function main() {
const app = await createApp({
appToken: 'stvor_demo_token',
relayUrl: 'ws://localhost:8080'
});
// Connect Bob (receiver)
const bob = await app.connect('bob@example.com');
bob.onMessage((from, msg) => {
console.log(`📩 [Bob] from ${from}: ${msg}`);
});
// Connect Alice (sender)
const alice = await app.connect('alice@example.com');
alice.onMessage((from, msg) => {
console.log(`📩 [Alice] from ${from}: ${msg}`);
});
// SDK handles all E2EE internally (no WebSocket code needed!)
await alice.send('bob@example.com', 'Hello Bob! This is E2EE! 🔐');
console.log('✅ Alice sent message');
// Bob replies
await bob.send('alice@example.com', 'Hi Alice! Got it! 🎉');
console.log('✅ Bob sent message');
// Keep alive to see messages
await new Promise(r => setTimeout(r, 1000));
}
main().catch(console.error);
Step 3: Create package.json
{
"name": "my-e2ee-app",
"type": "module",
"dependencies": {
"@stvor/sdk": "^3.0.0",
"ws": "^8.13.0"
}
}
Step 4: Run it!
# Terminal 1: Start relay
node relay-server.mjs
# Terminal 2: Run app
npm install
node app.mjs
✅ Expected Output:
✅ Alice sent message
✅ Bob sent message
📩 [Bob] from alice@example.com: Hello Bob! This is E2EE! 🔐
📩 [Alice] from bob@example.com: Hi Alice! Got it! 🎉
Security Model
✅ What STVOR Guarantees
- End-to-end encryption — plaintext never leaves the device
- Forward Secrecy — compromise of current keys doesn't expose past messages (automatic DH ratchet)
- Post-Compromise Security — after key rotation, attacker loses access (forced ratchet every 50 msgs)
- Zero-knowledge relay — server cannot decrypt messages
- Header Integrity — metadata cannot be tampered with (AAD authentication)
- Replay Protection — each message is unique (persistent nonce cache)
Implementation Threat Model
🔒 Key Security Properties
- Forward Secrecy: Old messages remain encrypted even if current root key is compromised
- Break-in Recovery: Perfect forward secrecy prevents long-term key recovery (DH ratchet forces new ephemeral keys)
- Header Integrity (AAD): Relay cannot modify sender/recipient/timestamp without breaking cryptographic proof
- Replay Detection: Your replay cache (Redis/PostgreSQL) is critical — the SDK checks every nonce against it
- Session Durability: Production mode refusal ensures you don't lose keys on restart
📌 Your Deployment Checklist
- ✅ Use HTTPS/TLS between client ↔ relay (WebSocket over TLS required)
- ✅ Redis/PostgreSQL for replay cache — protect with firewall
- ✅ Backend API accessible only internally (metrics verification should not be public)
- ✅ Rotate relay API keys quarterly
- ✅ Monitor failed attestations in audit log (indicates tampering)
- ✅ Use encrypted storage for session keys (OS keychain or KMS)
Полностью рабочий Signal Protocol на Node.js crypto. Все 23 интеграционных теста пройдены: X3DH key agreement, ECDSA подписи, Double Ratchet encrypt/decrypt, forward secrecy, session serialisation. Ноль внешних крипто-зависимостей.
When to Use (and When Not To)
✅ Perfect For (v3.0):
- Healthcare/Finance applications (production-grade E2EE)
- Internal tools with high security requirements
- IoT & sensor networks
- Enterprise messaging
- Any application requiring NIST-compliant crypto
❌ Not Suitable For (v3.0):
- Browser/client-side JavaScript (Node.js only)
- Multi-device synchronization (each device = separate identity)
- Applications needing distributed consensus
- Completely offline operation (requires relay server)
📝 Architecture Notes
- Cryptография: Node.js built-in crypto — ECDH P-256, ECDSA P-256, AES-256-GCM, HKDF-SHA256
- Протокол: Signal Protocol (Symmetric X3DH + Double Ratchet)
- Зависимости: 0 внешних крипто-библиотек — только
node:crypto - Transport: HTTP relay (Fastify)
- State: Pluggable storage (Redis, PostgreSQL, in-memory)
- Метрики: HMAC-SHA256 подписаны для аудита
Troubleshooting
ERR_PACKAGE_PATH_NOT_EXPORTED
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in @stvor/sdk/package.json
Причина: Устаревшая версия SDK (<3.0.0) без dual CJS/ESM exports.
Решение:
npm install @stvor/sdk@latest
Relay handshake timeout
StvorError: Relay handshake timeout [RELAY_UNAVAILABLE]
Причины:
- Relay сервер не запущен
- Неправильный
relayUrl - AppToken невалидный
Решение: Запустите mock relay для тестирования: npx @stvor/sdk mock-relay
Cannot find module 'ws'
Error: Cannot find module 'ws'
Решение:
npm install ws
ws — dependency SDK. Устанавливается автоматически, но в некоторых пакетных менеджерах (pnpm) нужно устанавливать явно.
INVALID_APP_TOKEN / AUTH_FAILED
Убедитесь что токен начинается с stvor_ для mock relay. Для продакшен — получите токен через Dashboard.
CommonJS: require() возвращает Proxy
В CommonJS SDK экспортирует Proxy-объект. Используйте async .load():
const sdk = require('@stvor/sdk');
const { Stvor } = await sdk.load();
// Теперь Stvor доступен
const app = await Stvor.init({ ... });