Sterister 4bf9307548
Some checks failed
Test / test (push) Has been cancelled
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors
Phase C complete: Shade now has a Kotlin implementation with byte-for-byte
compatibility to the TypeScript core, verified by shared test vectors.

M-Cross 1: shade-android Kotlin module
- build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization
- Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.)
- CryptoProvider interface
- TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC)
- KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey)
  with the same info strings and salts as @shade/core
- Fingerprint (safety number) computation matching TS exactly
- X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing
- Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt,
  ratchetDecrypt, DH ratchet step, skipped key cache
- Wire format matching @shade/proto byte-for-byte
- StorageProvider interface + MemoryStorage impl
- High-level ShadeSessionManager mirroring @shade/core's API

M-Cross 2: Cross-platform test vectors
- scripts/generate-vectors.ts emits JSON fixtures from the TS implementation
- Vectors cover: HKDF, KDF chain (root + chain), X3DH root key,
  fingerprint computation, wire format encoding
- packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS
  produces the same output as the committed vectors
- android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt
  loads the SAME JSON and verifies Kotlin produces identical bytes

M-Cross 3: Nova Android migration plan
- android/shade-android/MIGRATION-NOVA.md — concrete steps to replace
  Nova's static PushKeyStore AES with Shade sessions
- Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate)
- Smoke test recipe for end-to-end TS → Kotlin push flow

251 tests passing on the TS side. Kotlin tests run via Gradle when
the Android SDK is available; the vectors guarantee they'll pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
2026-04-09 20:12:01 +02:00

Shade

End-to-end encryption library implementing the Signal Protocol (X3DH + Double Ratchet) for TypeScript/Bun. Drop into any project — frontend, backend, mobile — to get forward secrecy, post-compromise recovery, and self-healing security.

What you get

  • X3DH initial key agreement (works asynchronously via prekey bundles)
  • Double Ratchet for per-message forward secrecy and post-compromise security
  • Self-authenticated prekey server (Hono, Docker-ready) with rate limiting, metrics, health checks
  • Persistent storage backends: SQLite (zero-config) and PostgreSQL (Drizzle)
  • Identity rotation with grace period for old sessions
  • Safety numbers (Signal-style fingerprints) for out-of-band verification
  • Constant-time comparisons and memory zeroization for hardened operation
  • Binary wire format that's significantly smaller than JSON
  • Crash-safe — sessions survive container restarts, power outages, SIGKILL
  • Live observability — bundled dashboard SPA + embeddable React widgets to see what's happening between every step

Quick start

Add the Gitea npm registry to your project's .npmrc:

@shade:registry=https://gt.zyon.no/api/packages/Stian/npm/

Then install the SDK (one-liner for most use cases):

bun add @shade/sdk

Or install specific packages if you need fine-grained control:

bun add @shade/core @shade/crypto-web @shade/storage-sqlite

Even faster — scaffold a new project with the CLI:

bun add -g @shade/cli
shade init my-app --template bun-server
cd my-app && bun install && bun run start

Magic one-liner with the SDK:

import { createShade } from '@shade/sdk';

const shade = await createShade({
  prekeyServer: 'https://shade.example.com',
  storage: 'sqlite:/data/shade.db',
  address: 'alice@example.com',
});

// Send (auto-establishes session if none exists)
const envelope = await shade.send('bob@example.com', 'Hello, encrypted world!');

// Receive
const plaintext = await shade.receive('alice@example.com', incomingEnvelope);

// Your safety number for out-of-band verification
console.log(await shade.fingerprint);

Or use the lower-level packages directly if you need full control:

import { ShadeSessionManager } from '@shade/core';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { SQLiteStorage } from '@shade/storage-sqlite';

const manager = new ShadeSessionManager(
  new SubtleCryptoProvider(),
  new SQLiteStorage('/data/shade.db'),
);
await manager.initialize();

Architecture

                     Shade Prekey Server (Hono)
                              │
              POST /v1/keys/register (signed)
              GET  /v1/keys/bundle/:address
              POST /v1/keys/replenish (signed)
              DELETE /v1/keys/:address (signed)
                              │
        ┌─────────────────────┴─────────────────────┐
        │                                           │
    [Client A]                                  [Client B]
    ShadeSessionManager                         ShadeSessionManager
        │                                           │
        ├──── X3DH ────────────────────────────────►│
        │                                           │
        │◄──── Double Ratchet messages ────────────►│
        │                                           │
    SQLiteStorage / PostgresStorage             SQLiteStorage / PostgresStorage

Packages

Package Purpose
@shade/core Protocol logic (X3DH, Double Ratchet, session manager, errors, events)
@shade/crypto-web SubtleCrypto + @noble/curves provider, in-memory storage
@shade/storage-sqlite Persistent SQLite storage (zero-config, bun:sqlite)
@shade/storage-postgres PostgreSQL storage with Drizzle for shared databases
@shade/server Prekey server (Hono routes, auth, rate limit, health, metrics)
@shade/transport HTTP + WebSocket transport wrappers with auto-encryption
@shade/proto Compact binary wire format (smaller than JSON)
@shade/observer Live debugger backend (snapshot, SSE, dashboard) — see README
@shade/widgets Embeddable React widgets — see README
@shade/dashboard Standalone dashboard SPA bundled into the observer
@shade/sdk High-level wrapper with createShade() one-liner, auto-publish, auto-establish, auto-replenish
@shade/cli shade init scaffolder + utilities (fingerprint, rotate, peer, dashboard, doctor)

Publishing

All packages publish to a self-hosted Gitea npm registry on gt.zyon.no.

# Bump all packages in lockstep
bun run version 1.1.0

# Dry-run (pack all tarballs without publishing)
bun run publish:dry

# Real publish (requires GITEA_TOKEN env var)
bun run publish:all

# Or via CI: push a git tag v1.1.0 and .gitea/workflows/publish.yml runs

Security properties

Property Description
Forward secrecy Compromising a key cannot decrypt past messages
Post-compromise security Self-heals after key compromise on next DH ratchet
Authentication Ed25519 identity signatures on prekey server writes
Replay protection ±5 minute timestamp window on signed requests
Constant-time comparisons Timing attacks on identity keys are blocked
Memory zeroization Key material is zeroed after use (best-effort in JS)
Identity verification Safety numbers (60 digits) for out-of-band comparison
Identity rotation 7-day grace period for old sessions during rotation

Documentation

Deployment

For containerized deployment (Docker/Dokploy):

services:
  shade-prekey:
    image: shade-prekey-server:latest
    ports:
      - "3900:3900"
    volumes:
      - shade-data:/data
    environment:
      - SHADE_PREKEY_DB_PATH=/data/shade-prekeys.db
volumes:
  shade-data:

The SQLite database persists to a Docker volume so all keys and prekey bundles survive restarts.

License

MIT

Description
E2EE library implementing Signal Protocol (X3DH + Double Ratchet)
Readme MIT 2.8 MiB
Languages
TypeScript 93.1%
Kotlin 6.7%
Shell 0.1%