npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@brashkie/signalis

v0.7.0

Published

Signal Protocol implementation in TypeScript — Identity Keys + PreKeys + X3DH + Double Ratchet + Sender Keys

Readme

@brashkie/signalis

Production-grade Signal Protocol implementation in TypeScript. X3DH · Double Ratchet · Sender Keys · Identity Management.

npm version License Node.js Version TypeScript Tests Coverage CI Powered by Rust

English · Español

Built with 🔐 + ❤️ by Hepein Oficial


📑 Table of Contents


✨ What is Signalis?

@brashkie/signalis is a TypeScript implementation of the Signal Protocol — the same end-to-end encryption protocol used by Signal, WhatsApp, and Skype. It provides:

  • 🔑 Identity Keys — Long-term identity management with XEd25519 signing
  • 🔁 PreKey Bundles — Asynchronous session establishment (X3DH)
  • 🪜 Double Ratchet — Forward & backward secrecy for messages (Sprint 3)
  • 👥 Sender Keys — Efficient group messaging (Sprint 5)
  • 💾 Storage Layer — Pluggable persistence for sessions/keys (Sprint 4)
  • 🛡️ Built on @brashkie/signalis-core — Rust-native crypto primitives

Part of the Hepein ecosystem. Foundation for @brashkie/waproto (WhatsApp Protocol) and ultimately a from-scratch alternative to Baileys.


🎉 What's New in v0.7.0

v0.7.0 ships the Storage Layer — the piece that separates a Signal Protocol library from a real messaging app. Now you don't have to BYO persistence.

🆕 New API

import { StoreBundle, SessionBuilder, ProtocolAddress } from '@brashkie/signalis';

// ─── Set up storage (1 line) ───────────────────────────────────────
const stores = StoreBundle.file('~/.myapp/signalis');
// or: StoreBundle.memory() for tests

const builder = new SessionBuilder(stores);

// ─── Send a message (auto X3DH on first send) ──────────────────────
const msg = await builder.encrypt(
  new ProtocolAddress('bob', 1),
  Buffer.from('Hola Bob'),
  bobBundle,   // bundle needed only on first send
);

// ─── Receive a message ─────────────────────────────────────────────
const plain = await builder.decrypt(
  new ProtocolAddress('alice', 1),
  msg,
);

// ─── App restart? Just rebuild the bundle from disk ────────────────
const stores2 = StoreBundle.file('~/.myapp/signalis');
const builder2 = new SessionBuilder(stores2);
// Continues conversation right where it left off ✨

🔥 What StoreBundle + SessionBuilder Handle Automatically

  • ✅ Loading/saving sessions across the 4 canonical Signal stores
  • ✅ X3DH bootstrap on first message to a new peer
  • ✅ TOFU + identity-change detection ("safety number changed")
  • ✅ One-time prekey consumption after first decrypt (replay protection)
  • ✅ Multi-device peer tracking via ProtocolAddress(userId, deviceId)
  • ✅ Atomic file writes (no corruption on crash)

🏗️ 4 Canonical Storage Interfaces

Plug in your own database (SQLite, IndexedDB, Postgres, Redis):

interface IdentityStore { ... }      // own identity + TOFU cache
interface PreKeyStore { ... }        // one-time prekeys
interface SignedPreKeyStore { ... }  // medium-term signed prekeys
interface SessionStore { ... }       // per-peer Double Ratchet

📊 Quality

✅ ~90 new tests for Storage Layer
✅ 100% backwards compatible with v0.6.0
✅ 100% test coverage maintained
✅ Zero new dependencies (node:fs + node:crypto only)

🎉 What's New in v0.6.0

v0.6.0 ships the Session class — the API the world will actually use. All the ratchet primitives from v0.5.0 collapse into 5 lines for the end user.

🆕 New API

import { X3DH, Session } from '@brashkie/signalis';

// ─── Alice (after X3DH) ─────────────────────────────────────────────
const { sharedSecret, initialMessage } = X3DH.initiate(alice, bobBundle, {
  myRegistrationId: 1234,
});

const aliceSession = Session.initiateFromX3DH({
  sharedSecret,
  theirIdentityKey: bobBundle.identityKey,
  theirSignedPreKeyPublic: bobBundle.signedPreKey.publicKey,
});

const packet = aliceSession.encrypt(Buffer.from('Hola Bob'));

// ─── Bob ────────────────────────────────────────────────────────────
const bobResult = X3DH.receive(bob, bobSpk, bobOpk, initialMessage);
const bobSession = Session.receiveFromX3DH({
  sharedSecret: bobResult.sharedSecret,
  myIdentityKey: bob.toPublic(),
  mySignedPreKeyPrivate: bobSpk.privateKey,
  mySignedPreKeyPublic: bobSpk.publicKey,
  theirIdentityKey: alice.toPublic(),
});

const plain = bobSession.decrypt(packet); // → "Hola Bob"

// ─── Persist + restore across app restarts ──────────────────────────
const saved = JSON.stringify(aliceSession.serialize());
await db.save('session:bob', saved);

const restored = Session.deserialize(JSON.parse(saved));
restored.encrypt(Buffer.from('also after restart')); // works seamlessly

🔥 What Session Handles Automatically

  • ✅ DH ratchet rotation when peer sends new DH key (forward + backward secrecy)
  • ✅ Symmetric chain advancement per message
  • ✅ Out-of-order delivery via skipped-keys cache
  • ✅ Replay detection (counter must be monotonic or in cache)
  • ✅ Anti-DoS cap on skipped key derivation (default 2000, configurable)
  • ✅ Full serialization for app restarts / multi-device sync

🛡️ Security Built-In

  • ✅ Encrypt-then-MAC (AES-256-CBC + HMAC-SHA256, Signal classic spec)
  • ✅ MAC verified BEFORE decrypt (no padding oracle)
  • ✅ Constant-time MAC compare (timingSafeEqual)
  • ✅ Counter replay protection
  • ✅ Anti-DoS skipped-key cap

📊 Quality

✅ ~80 new tests for Session (E2E, validation, serialize, etc.)
✅ 100% backwards compatible with v0.5.0
✅ 100% test coverage maintained

🎉 What's New in v0.4.0

🆕 New API

import { X3DH, InitialMessage } from '@brashkie/signalis';

// Alice initiates a session with Bob (Bob can be offline)
const { sharedSecret, initialMessage } = X3DH.initiate(
  myIdentity,   // IdentityKeyPair
  bobBundle,    // PreKeyBundle (verified from server)
  { myRegistrationId: 12345 },
);

// Later, Bob processes Alice's initial message
const { sharedSecret, oneTimePreKeyId } = X3DH.receive(
  myIdentity,        // IdentityKeyPair
  mySignedPreKey,    // SignedPreKey matching initialMessage.signedPreKeyId
  myOneTimePreKey,   // OneTimePreKey (or null)
  initialMessage,    // Alice's payload
);

// Both Alice and Bob now have THE SAME sharedSecret (32 bytes)
// → ready to seed the Double Ratchet in v0.5.0

🔬 What's Inside

| Feature | Description | |---------|-------------| | X3DH.initiate(...) | Initiator (Alice) flow: generate ephemeral, 4-way DH, HKDF | | X3DH.receive(...) | Responder (Bob) flow: mirror the 4 DHs from his side | | InitialMessage | Wire-format payload class with fromPayload() / toPayload() | | computeInitiatorSharedSecret(...) | Low-level primitive (advanced use) | | computeResponderSharedSecret(...) | Low-level primitive (advanced use) | | Expired SPK rejection | Defaults to 30-day max age (configurable) | | SPK/OPK ID validation | Throws PreKeyError on mismatch |

📐 Spec Compliance

Matches the Signal X3DH spec exactly:

F  = 0xFF × 32        (prefix for Curve25519)
KM = F || DH1 || DH2 || DH3 [|| DH4]
SK = HKDF-SHA256(salt=0x00×32, IKM=KM, info="Signalis_X3DH_v1", L=32)

DH1 = DH(IK_A,  SPK_B)    — my identity ↔ their signed prekey
DH2 = DH(EK_A,  IK_B)     — my ephemeral ↔ their identity
DH3 = DH(EK_A,  SPK_B)    — my ephemeral ↔ their signed prekey
DH4 = DH(EK_A,  OPK_B)    — my ephemeral ↔ their one-time prekey (if used)

📊 Quality Metrics

✅ 458 tests passing  (was 395 in v0.3.0)
✅ 100% coverage      (preserved)
✅ Full Signal spec compliance
✅ Mallory-style attack tests included

100% backwards compatible with v0.3.0.


🎉 What's New in v0.4.0

v0.4.0 ships X3DH — the heart of Signal Protocol. Alice and Bob can now derive the same 32-byte shared secret without being online simultaneously. This is the asynchronous handshake that makes E2E messaging work when one party is offline.

🆕 New APIs

import { X3DH } from '@brashkie/signalis';

// Alice initiates (knows Bob is offline, fetches bundle from server)
const { sharedSecret, initialMessage } = X3DH.initiate(alice, bobBundle, {
  myRegistrationId: 12345,
});

// Bob receives later
const { sharedSecret } = X3DH.receive(bob, bobSpk, bobOpk, initialMessage);

// Both have the SAME secret → ready for Double Ratchet (Sprint 3)

| Class / Function | Purpose | |---|---| | X3DH.initiate(myIdentity, theirBundle, options) | Alice's side: 4 DHs + HKDF → SharedSecret | | X3DH.receive(myIdentity, mySpk, myOpk, msg) | Bob's side: derives same SharedSecret | | InitialMessage | Structured wire-format Alice attaches to first encrypted message | | computeInitiatorSharedSecret | Low-level primitive (advanced use) | | computeResponderSharedSecret | Low-level primitive (advanced use) |

🔒 Security Built-In

  • ✅ Rejects expired SignedPreKeys by default (30-day max)
  • ✅ Mismatched SPK/OPK IDs throw PreKeyError
  • ✅ Wire-format tampering caught at deserialization
  • ✅ Spec-compliant with Signal X3DH (F=0xFF×32 prefix, HKDF salt=0x00×32, info="Signalis_X3DH_v1")

📊 Quality Metrics

✅ 458 tests passing
✅ 100% statements
✅ 100% branches
✅ 100% functions
✅ 100% lines

100% backwards compatible with v0.3.0.


🕰️ Previous: v0.3.0 — PreKey Layer

v0.3.0 ships the PreKey layer — the foundation for X3DH. Bob can now publish a PreKeyBundle containing identity-signed prekeys, and Alice can fetch and automatically verify that bundle before initiating a session.

🆕 New Classes

| Class | Purpose | |-------|---------| | OneTimePreKey | Single-use ephemeral Curve25519 keypairs | | SignedPreKey | Medium-term keypair signed with identity (XEd25519) — rotated weekly | | PreKeyBundle | Complete server-facing payload for X3DH | | PublicOneTimePreKey | Public-only form (server-uploaded) | | PublicSignedPreKey | Verified public form for received bundles |

🛡️ Security Built-In

  • ✅ Signed prekeys verified automatically on fromPayload() (throws SignatureError if tampered)
  • ✅ Rotation lifecycle helpers (needsRotation(), isExpired(), ageMs())
  • ✅ Mallory-forgery prevention tested with explicit tests
  • ✅ Strict input validation (key sizes, IDs, hex format, registration ranges)

📊 Quality Metrics

✅ 395 tests passing
✅ 100% statements
✅ 100% branches
✅ 100% functions
✅ 100% lines

100% backwards compatible with v0.2.0.


🚀 Features

| Feature | Status | Sprint | |---------|--------|--------| | 🔑 Identity Key Pair (Curve25519) | ✅ | 1 | | ✍️ XEd25519 signing on identity keys | ✅ | 1.2 | | 🛡️ Branded types (PublicKey vs PrivateKey at compile time) | ✅ | 1 | | 🎯 Type-safe with full TypeScript 6.0 support | ✅ | 1 | | 📦 Dual ESM/CJS package | ✅ | 1 | | 🧪 100% test coverage with RFC vectors | ✅ | 2.1 | | 🔐 Safe defaults (no toString leaks of private keys) | ✅ | 1 | | 💾 Serialization/deserialization (JSON-safe) | ✅ | 1 | | 🔁 One-Time PreKeys | ✅ | 2.1 | | ✍️ Signed PreKeys (signed with identity via XEd25519) | ✅ | 2.1 | | 📦 PreKey Bundles (server-facing X3DH payload) | ✅ | 2.1 | | 🤝 X3DH (Extended Triple Diffie-Hellman) | ✅ NEW | 2.2 | | 🪜 Double Ratchet (forward + backward secrecy) | 🚧 | 3 | | 💾 Pluggable storage layer | 🚧 | 4 | | 👥 Sender Keys (group messaging) | 🚧 | 5 |


🤔 Why Signalis?

vs. Building your own crypto

❌ Roll your own: bugs, side-channels, footguns everywhere
✅ Signalis: built on audited Rust primitives (RustCrypto, dalek-cryptography)

vs. Using Node's built-in crypto

❌ node:crypto: low-level, easy to misuse (nonce reuse, mode confusion)
✅ Signalis: high-level API matching Signal Protocol semantics

vs. Other Signal Protocol libraries

| Need | Signalis | libsignal-node | @privacyresearch/libsignal-protocol-typescript | |------|----------|----------------|------------------------------------------------| | TypeScript-first | ✅ Branded types + strict mode | 🟡 Has types | 🟡 Has types | | Pure JS install | ✅ (depends on signalis-core native) | ❌ Requires Rust toolchain | ✅ | | Speed | 🔥 Rust-native via NAPI | 🔥 Rust-native | 🐢 Pure JS (slow) | | Test Coverage | ✅ 100% | 🟡 Unknown | ❌ Unknown | | Active maintenance | ✅ Active | 🟡 Sporadic | ❌ Stale | | Dual ESM/CJS | ✅ | ❌ | ❌ | | Modern API | ✅ Class-based, async-friendly | 🟡 Old-style | 🟡 Callback-heavy | | License | Apache-2.0 | GPL-3.0 | GPL-3.0 |


📦 Installation

npm install @brashkie/signalis

This also installs @brashkie/signalis-core (cryptographic primitives), which uses prebuilt native binaries for:

  • ✅ Windows x64 (MSVC), Windows arm64 (MSVC)
  • ✅ macOS x64 (Intel), macOS arm64 (Apple Silicon)
  • ✅ Linux x64 (glibc + musl), Linux arm64 (glibc)

No Rust toolchain required for installation.

Requirements

  • Node.js: 18.x, 20.x, 22.x, or 24.x
  • TypeScript (optional but recommended): 6.0+

⚡ 5-Minute Quick Start

Want to see Signalis work in 5 minutes? Copy-paste this:

quickstart.ts

import {
  IdentityKeyPair,
  PublicIdentityKey,
  SignedPreKey,
  OneTimePreKey,
  PreKeyBundle,
  X3DH,
} from '@brashkie/signalis';

// ════════════════════════════════════════════════════════════════════════
// PART 1: Identity & Signing (✅ Available in v0.2.0+)
// ════════════════════════════════════════════════════════════════════════

console.log('\n🔑 PART 1 — Identity & Signing\n');

// Generate Alice's long-term identity (do once at registration)
const alice = IdentityKeyPair.generate();
console.log('Alice fingerprint:', alice.shortFingerprint());

// Alice signs a message
const message = Buffer.from('I am authorizing this transaction');
const signature = alice.sign(message);
console.log('Signature length:', signature.length, 'bytes');

// Bob verifies using only Alice's public key
const alicePub = alice.toPublic();
const alicePublicHex = alicePub.toHex(); // shared via network

// On Bob's side:
const aliceKeyReceived = PublicIdentityKey.fromHex(alicePublicHex);
const isValid = aliceKeyReceived.verifyBool(message, signature);
console.log('Signature valid?', isValid); // → true ✅

// Mallory tries to forge:
const mallory = IdentityKeyPair.generate();
const forgedSig = mallory.sign(message);
const forgedValid = aliceKeyReceived.verifyBool(message, forgedSig);
console.log('Mallory forgery accepted?', forgedValid); // → false ✅

// ════════════════════════════════════════════════════════════════════════
// PART 2: PreKey Bundle Setup (✅ Available in v0.3.0+)
// ════════════════════════════════════════════════════════════════════════

console.log('\n📦 PART 2 — PreKey Bundle Setup\n');

// Bob publishes his prekey bundle to the server
const bob = IdentityKeyPair.generate();
const bobSpk = SignedPreKey.generate(bob, 1);  // signed with bob's identity
const bobOtpks = OneTimePreKey.generateBatch(1, 100); // 100 one-time keys

console.log('Bob fingerprint:', bob.shortFingerprint());
console.log('Bob SPK ID:', bobSpk.id);
console.log('Bob SPK signature verifies?', bobSpk.verify(bob.toPublic())); // → true ✅
console.log('Generated', bobOtpks.length, 'one-time prekeys');

// Server picks the first available one-time prekey for Alice's request
const pickedOtpk = bobOtpks[0];

const bundleForAlice = PreKeyBundle.build({
  registrationId: 12345,
  deviceId: 1,
  identityKey: bob.toPublic(),
  signedPreKey: bobSpk.toPublic(),
  oneTimePreKey: pickedOtpk.toPublic(),
});

// Server stores this as JSON
const serverPayload = bundleForAlice.toPayload();
console.log('Bundle address:', bundleForAlice.address());

// ════════════════════════════════════════════════════════════════════════
// PART 3: Alice Receives & Verifies Bob's Bundle
// ════════════════════════════════════════════════════════════════════════

console.log('\n🔍 PART 3 — Alice Verifies Bob\'s Bundle\n');

// Alice fetches Bob's bundle from the server (e.g., HTTPS GET /bundles/bob)
// fromPayload() AUTOMATICALLY verifies the signed prekey's signature.
// If anyone tampered or forged anything, it throws SignatureError.
const verifiedBundle = PreKeyBundle.fromPayload(serverPayload);

console.log('Bundle verified ✅');
console.log('  Bob identity:', verifiedBundle.identityKey.shortFingerprint());
console.log('  Bob SPK id:', verifiedBundle.signedPreKey.id);
console.log('  Bob OTPK id:', verifiedBundle.oneTimePreKey?.id);
console.log('  Has OTPK?', verifiedBundle.hasOneTimePreKey());

// ════════════════════════════════════════════════════════════════════════
// PART 4: Tamper Detection
// ════════════════════════════════════════════════════════════════════════

console.log('\n🛡️ PART 4 — Tampering Is Caught\n');

// What if a malicious server tries to swap Bob's identity?
const evil = IdentityKeyPair.generate();
const evilBundle = {
  ...serverPayload,
  identityKey: evil.toPublic().toHex(), // ← attacker substitutes their key
};

try {
  PreKeyBundle.fromPayload(evilBundle);
  console.log('⚠️ This should NEVER print');
} catch (e) {
  console.log('Tampered bundle rejected ✅');
  console.log('Error:', (e as Error).message);
}

// ════════════════════════════════════════════════════════════════════════
// PART 5: X3DH Handshake — Alice & Bob derive the SAME secret ✨ NEW v0.4.0
// ════════════════════════════════════════════════════════════════════════

console.log('\n🤝 PART 5 — X3DH Handshake\n');

// Alice initiates: she generates a fresh ephemeral key, runs 4 DHs,
// derives a shared secret via HKDF, and produces an "initial message"
// to send Bob along with her first encrypted message.
const handshake = X3DH.initiate(alice, verifiedBundle, {
  myRegistrationId: 6789,
  myDeviceId: 1,
});

console.log('Alice derived secret:',
  handshake.sharedSecret.toString('hex').slice(0, 16) + '...');
console.log('Initial message: SPK id =', handshake.initialMessage.signedPreKeyId,
            ', OTPK id =', handshake.initialMessage.oneTimePreKeyId);

// Bob (when he comes back online) receives Alice's initial message and
// uses his stored private prekeys to derive the SAME shared secret.
// In a real app, Bob would look up the SPK/OTPK by ID from his store.
const bobResult = X3DH.receive(bob, bobSpk, bobOpks[0], handshake.initialMessage);

console.log('Bob derived secret:  ',
  bobResult.sharedSecret.toString('hex').slice(0, 16) + '...');
console.log('Bob should now DELETE OneTimePreKey id', bobResult.oneTimePreKeyId,
            'from his store (forward secrecy)');

const secretsMatch = handshake.sharedSecret.equals(bobResult.sharedSecret);
console.log('\n🎉 Same secret on both sides?', secretsMatch);
//          ↑ true ✅ — Alice and Bob now share a 32-byte secret
//          without ever being online at the same time. This seeds
//          the Double Ratchet in Sprint 3 (v0.5.0).

console.log('\n🎉 Quick Start Complete!\n');

Run It

# 1. Install
npm install @brashkie/signalis

# 2. Save the code above as quickstart.ts

# 3. Run with tsx (or compile with tsc)
npx tsx quickstart.ts

Expected Output

🔑 PART 1 — Identity & Signing

Alice fingerprint: a1b2c3d4e5f60718
Signature length: 64 bytes
Signature valid? true
Mallory forgery accepted? false

📦 PART 2 — PreKey Bundle Setup

Bob fingerprint: f1e2d3c4b5a60798
Bob SPK ID: 1
Bob SPK signature verifies? true
Generated 100 one-time prekeys

🔍 PART 3 — Alice Verifies Bob's Bundle

Bundle verified ✅
  Bob identity: f1e2d3c4b5a60798
  Bob SPK id: 1
  Bob OTPK id: 1
  Has OTPK? true

🛡️ PART 4 — Tampering Is Caught

Tampered bundle rejected ✅
Error: SignedPreKey signature verification failed for prekey id 1

🤝 PART 5 — X3DH Handshake

Alice derived secret: 8a3f5b9d2c7e1042...
Initial message: SPK id = 1 , OTPK id = 1
Bob derived secret:   8a3f5b9d2c7e1042...
Bob should now DELETE OneTimePreKey id 1 from his store (forward secrecy)

🎉 Same secret on both sides? true

🎉 Quick Start Complete!

🧪 Complete Examples

Example 1: Persisting Identity

import { IdentityKeyPair } from '@brashkie/signalis';
import { writeFile, readFile } from 'node:fs/promises';

// At registration (do once)
async function setupIdentity() {
  const identity = IdentityKeyPair.generate();
  const data = identity.serialize();
  // ⚠️ STORE IN ENCRYPTED STORAGE
  await writeFile('./alice-identity.json', JSON.stringify(data));
  console.log('Identity created. Fingerprint:', identity.shortFingerprint());
}

// On app startup
async function loadIdentity(): Promise<IdentityKeyPair> {
  const raw = await readFile('./alice-identity.json', 'utf-8');
  return IdentityKeyPair.deserialize(JSON.parse(raw));
}

Example 2: Signing API Requests

import { IdentityKeyPair } from '@brashkie/signalis';

const identity = await loadIdentity();

async function signedFetch(url: string, options: RequestInit = {}) {
  const timestamp = Date.now().toString();
  const body = options.body?.toString() ?? '';
  
  // Sign request: method + url + timestamp + body
  const toSign = Buffer.from(
    `${options.method ?? 'GET'}|${url}|${timestamp}|${body}`,
  );
  const signature = identity.sign(toSign);
  
  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'X-Public-Key': identity.toPublic().toHex(),
      'X-Timestamp': timestamp,
      'X-Signature': signature.toString('base64'),
    },
  });
}

// On server side, verify:
function verifyRequest(req) {
  const pubKey = PublicIdentityKey.fromHex(req.headers['x-public-key']);
  const timestamp = req.headers['x-timestamp'];
  const sig = Buffer.from(req.headers['x-signature'], 'base64');
  
  // Reject old timestamps (clock skew prevention)
  if (Math.abs(Date.now() - parseInt(timestamp, 10)) > 5 * 60 * 1000) {
    throw new Error('Request expired');
  }
  
  const toVerify = Buffer.from(
    `${req.method}|${req.url}|${timestamp}|${req.body ?? ''}`,
  );
  
  if (!pubKey.verifyBool(toVerify, sig)) {
    throw new Error('Invalid signature');
  }
  
  return pubKey.toHex(); // Trust the user
}

Example 3: PreKey Bundle Lifecycle

import {
  IdentityKeyPair,
  SignedPreKey,
  OneTimePreKey,
  PreKeyBundle,
} from '@brashkie/signalis';

class UserSetup {
  identity: IdentityKeyPair;
  signedPreKey: SignedPreKey;
  oneTimePreKeys: OneTimePreKey[];
  
  constructor() {
    this.identity = IdentityKeyPair.generate();
    this.signedPreKey = SignedPreKey.generate(this.identity, 1);
    this.oneTimePreKeys = OneTimePreKey.generateBatch(1, 100);
  }
  
  // Publish to server
  async publish(serverUrl: string) {
    await fetch(`${serverUrl}/keys/register`, {
      method: 'POST',
      body: JSON.stringify({
        identityKey: this.identity.toPublic().toHex(),
        signedPreKey: this.signedPreKey.toPayload(),
        oneTimePreKeys: this.oneTimePreKeys.map(k => ({
          id: k.id,
          publicKey: k.publicKey.toString('hex'),
        })),
      }),
    });
  }
  
  // Run weekly: check if SignedPreKey needs rotation
  async rotateIfNeeded(serverUrl: string) {
    if (this.signedPreKey.needsRotation()) {
      const newId = this.signedPreKey.id + 1;
      this.signedPreKey = SignedPreKey.generate(this.identity, newId);
      
      await fetch(`${serverUrl}/keys/signed-prekey`, {
        method: 'POST',
        body: JSON.stringify(this.signedPreKey.toPayload()),
      });
      
      console.log('Rotated signed prekey to id', newId);
    }
  }
  
  // Run when running low on OneTimePreKeys
  async refillOneTimeKeys(serverUrl: string, currentCount: number) {
    if (currentCount < 10) {
      const startId = Math.max(...this.oneTimePreKeys.map(k => k.id)) + 1;
      const newBatch = OneTimePreKey.generateBatch(startId, 90);
      this.oneTimePreKeys.push(...newBatch);
      
      await fetch(`${serverUrl}/keys/one-time`, {
        method: 'POST',
        body: JSON.stringify(
          newBatch.map(k => ({
            id: k.id,
            publicKey: k.publicKey.toString('hex'),
          })),
        ),
      });
      
      console.log('Refilled with 90 new one-time prekeys');
    }
  }
}

Example 4: Receiving and Verifying a Bundle

import { PreKeyBundle, SignatureError } from '@brashkie/signalis';

async function fetchUserBundle(userId: string): Promise<PreKeyBundle | null> {
  const response = await fetch(`/api/users/${userId}/bundle`);
  
  if (!response.ok) {
    console.error('Failed to fetch bundle');
    return null;
  }
  
  const payload = await response.json();
  
  try {
    // fromPayload AUTOMATICALLY verifies the signed prekey signature
    return PreKeyBundle.fromPayload(payload);
  } catch (e) {
    if (e instanceof SignatureError) {
      console.error('SECURITY: Bundle signature invalid!', e.message);
      // → server may be compromised, do NOT use this bundle
    }
    throw e;
  }
}

// Use it:
const bobBundle = await fetchUserBundle('bob');
if (bobBundle) {
  console.log('Bob is at address:', bobBundle.address());
  console.log('Bob fingerprint:', bobBundle.identityKey.fingerprint());
  // → ready for X3DH.initiate(bundle) in v0.4.0
}

For more examples, see EXAMPLES.md.


📚 API Overview

Identity Module

class IdentityKeyPair {
  static generate(): IdentityKeyPair;
  static fromKeys(pub: Buffer, priv: Buffer): IdentityKeyPair;
  static deserialize(data: SerializedKeyPair): IdentityKeyPair;
  
  // Signing (XEd25519)
  sign(message: Buffer): Signature;
  signWithRandom(message: Buffer, random: Buffer): Signature;
  verify(message: Buffer, signature: Buffer): void;
  verifyBool(message: Buffer, signature: Buffer): boolean;
  
  // Accessors
  toPublic(): PublicIdentityKey;
  fingerprint(): string;
  shortFingerprint(): string;
  serialize(): SerializedKeyPair;
}

class PublicIdentityKey {
  static fromHex(hex: string): PublicIdentityKey;
  static fromBase64(b64: string): PublicIdentityKey;
  
  verify(message: Buffer, signature: Buffer): void;
  verifyBool(message: Buffer, signature: Buffer): boolean;
  
  toHex(): string;
  toBase64(): string;
  fingerprint(): string;
}

PreKeys Module (NEW in v0.3.0)

class OneTimePreKey {
  static generate(id: number): OneTimePreKey;
  static generateBatch(startId: number, count: number): OneTimePreKey[];
  static fromKeys(id, pub, priv): OneTimePreKey;
  static deserialize(data): OneTimePreKey;
  
  toPublic(): PublicOneTimePreKey;
  serialize(): SerializedOneTimePreKey;
}

class SignedPreKey {
  static generate(identity: IdentityKeyPair, id: number): SignedPreKey;
  static fromKeys(id, pub, priv, sig, timestamp): SignedPreKey;
  static deserialize(data): SignedPreKey;
  
  verify(identityPub: PublicIdentityKey): boolean;
  needsRotation(threshold?: number): boolean;
  isExpired(maxAge?: number): boolean;
  ageMs(now?: number): number;
  
  toPayload(): PublicSignedPreKeyPayload;
  toPublic(): PublicSignedPreKey;
}

class PreKeyBundle {
  static build(args): PreKeyBundle;
  static fromPayload(payload): PreKeyBundle; // ← auto-verifies signature
  
  hasOneTimePreKey(): boolean;
  address(): string; // "registrationId.deviceId"
  toPayload(): PreKeyBundlePayload;
}

See API.md for the complete reference.


🏗️ Architecture

┌─────────────────────────────────────────────────────────────┐
│  Your Application                                            │
│  (Chat client, IoT device, server, etc.)                    │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  @brashkie/signalis  ← YOU ARE HERE (v0.3.0)                 │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │  Identity   │  │   PreKeys   │  │    X3DH     │         │
│  │    ✅       │  │    ✅       │  │   🚧 next   │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                              │
│  • IdentityKeyPair (XEd25519 signing)                       │
│  • PublicIdentityKey (verification)                          │
│  • OneTimePreKey / SignedPreKey / PreKeyBundle              │
│  • Branded types (compile-time safety)                       │
│  • Typed errors with codes                                   │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  @brashkie/signalis-core (Rust + NAPI)                       │
│                                                              │
│  Curve25519 · Ed25519 · XEd25519 · HKDF · AES-GCM           │
│  HMAC-SHA256 · SHA-256                                      │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  Audited Rust Crates                                         │
│  curve25519-dalek · ed25519-dalek · RustCrypto              │
└─────────────────────────────────────────────────────────────┘

Directory Structure

signalis/
├── 📁 media/
│   └── logo.png                ← Project logo
├── 📁 src/
│   ├── index.ts                ← Public API surface
│   ├── constants.ts            ← Sizes, info strings, validators
│   ├── crypto.ts               ← Wrappers over signalis-core
│   ├── errors.ts               ← Typed error hierarchy
│   ├── types.ts                ← Branded types + type guards
│   ├── 📁 identity/
│   │   ├── index.ts
│   │   └── identity-key.ts     ← IdentityKeyPair + PublicIdentityKey
│   └── 📁 prekeys/            ← NEW v0.3.0
│       ├── index.ts
│       ├── one-time-prekey.ts  ← OneTimePreKey + PublicOneTimePreKey
│       ├── signed-prekey.ts    ← SignedPreKey + PublicSignedPreKey
│       └── prekey-bundle.ts    ← PreKeyBundle
│
├── 📁 __tests__/              ← 395 tests, 100% coverage
├── 📁 .github/
│   └── workflows/
│       ├── ci.yml              ← Lint + test on PR
│       └── release.yml         ← Auto-publish on tag
│
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── vitest.config.mts
├── eslint.config.mjs
├── README.md                   ← This file
├── README.es.md                ← Spanish version
├── CHANGELOG.md
├── MIGRATION.md
├── ROADMAP.md
├── ROADMAP-ECOSYSTEM.md        ← Long-term vision
├── API.md
├── EXAMPLES.md
├── SECURITY.md
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── LICENSE
└── NOTICE

🔒 Security

Design Principles

  1. Audited primitives only — All crypto comes from @brashkie/signalis-core (built on RustCrypto and curve25519-dalek)
  2. Constant-time operations — For all comparisons and verifications (delegated to Rust)
  3. Memory hygiene — Private keys are zeroized on drop (in Rust layer)
  4. Type safety — Branded types prevent mixing public/private keys at compile time
  5. Safe defaultstoString(), toJSON(), and Node.js inspect output NEVER leak private keys
  6. Input validation — Every public API validates sizes, types, and ID ranges
  7. Automatic verificationfromPayload() methods verify signatures by default

Security-Critical Rules

🚨 NEVER log a serialized IdentityKeyPair / OneTimePreKey / SignedPreKey
🚨 NEVER transmit a serialized keypair over the network
🚨 ALWAYS store identity keys in encrypted storage (KMS, keychain, etc.)
🚨 ALWAYS verify fingerprints out-of-band before trusting public keys
🚨 NEVER reuse a (key, nonce) pair in AES-GCM
🚨 NEVER use ECDH shared secrets directly — always derive via HKDF
🚨 ROTATE SignedPreKeys every 7 days (use needsRotation())
🚨 NEVER use expired SignedPreKeys for new sessions

What's SAFE to Output

const alice = IdentityKeyPair.generate();

// ✅ All of these are SAFE — they do NOT include the private key
console.log(alice);                  // "IdentityKeyPair(public=a1b2c3...)"
console.log(alice.toString());       // Same
console.log(JSON.stringify(alice));  // {"type":"IdentityKeyPair","publicKey":"..."}
console.log(alice.fingerprint());    // SHA-256 hex of public key
console.log(alice.toPublic().toHex()); // public key only

Reporting Vulnerabilities

Please see SECURITY.md for the responsible disclosure process. Do NOT open public GitHub issues for security vulnerabilities.


🧪 Testing

Test Coverage: 100%

✅ 395 tests passing
✅ 100% statements
✅ 100% branches
✅ 100% functions
✅ 100% lines

Test files:

  • constants.test.ts — 23 tests
  • types.test.ts — 22 tests
  • errors.test.ts — 27 tests
  • crypto.test.ts — 42 tests
  • identity-key.test.ts — 76 tests
  • one-time-prekey.test.ts — 53 tests
  • signed-prekey.test.ts — 53 tests
  • prekey-bundle.test.ts — 35 tests
  • coverage-boost.test.ts — 50 tests
  • coverage-final.test.ts — 14 tests

Test Vectors

  • ✅ RFC 8032 vector 1 (Ed25519)
  • ✅ NIST SHA-256 vectors
  • ✅ Round-trip tests for all serialize/deserialize paths
  • ✅ Mallory-forgery prevention tests
  • ✅ E2E scenarios (Alice → server → Bob)

Running Tests

# All tests
npm test

# Watch mode
npm run test:watch

# Coverage report
npm test -- --coverage

# Coverage UI (interactive)
npm run test:ui

📊 Comparison with Alternatives

Performance

Benchmarks (Node 22, x86_64, average of 10k iterations):

| Operation | Signalis (via Rust) | tweetnacl | libsignal-protocol-typescript | |-----------|---------------------|-----------|-------------------------------| | Identity keypair generation | ~50,000/sec | ~3,000/sec | ~2,000/sec | | Sign (XEd25519) | ~25,000/sec | N/A | ~1,500/sec | | Verify (XEd25519) | ~10,000/sec | N/A | ~800/sec | | ECDH (X25519) | ~30,000/sec | ~2,000/sec | ~1,500/sec |

Bundle Size

@brashkie/signalis:              ~16 KB minified
@brashkie/signalis-core (native): ~400 KB per platform (one binary)

Total install: ~600 KB (vs 1.5 MB+ for pure-JS alternatives)

🗺️ Roadmap

✅ Sprint 1: Identity Foundation (v0.1.0 - v0.2.0) — COMPLETE

  • IdentityKeyPair + PublicIdentityKey
  • ✅ Branded types, type guards, error hierarchy
  • ✅ XEd25519 signing on identity keys (v0.2.0)
  • ✅ Ed25519 standalone signing
  • ✅ AES-GCM with AAD

✅ Sprint 2 Part 1: PreKey Layer (v0.3.0) — COMPLETE

  • OneTimePreKey with batch generation
  • SignedPreKey with identity-signed verification
  • PreKeyBundle with automatic signature verification
  • ✅ Rotation/expiration lifecycle helpers
  • ✅ 395 tests, 100% coverage

✅ Sprint 2 Part 2: X3DH (v0.4.0) — COMPLETE

  • X3DH.initiate(myIdentity, bobBundle)
  • X3DH.receive(myIdentity, mySpk, myOtpk?, initialMessage)
  • ✅ 4-way DH (DH1 + DH2 + DH3 + DH4)
  • ✅ HKDF derivation with Signalis_X3DH_v1 domain separator
  • InitialMessage class with wire-format roundtrip
  • ✅ 458 tests, 100% coverage

🚧 Sprint 3: Double Ratchet (v0.5.0) — NEXT

  • 🔜 Symmetric key ratchet + DH ratchet
  • 🔜 Skipped message keys (out-of-order delivery)
  • 🔜 Full forward + backward secrecy

🔜 Sprint 4: Storage Layer (v0.6.0)

🔜 Sprint 5: Group Messaging / Sender Keys (v0.7.0)

🎯 v1.0.0 — Production-ready with external audit

See ROADMAP.md for detailed plans and ROADMAP-ECOSYSTEM.md for the long-term vision.


🔗 Ecosystem

The Hepein crypto/messaging stack:

┌─────────────────────────────────────────────┐
│  HepeinBaileys 2.0 (Future)                 │  ← Complete WhatsApp client
├─────────────────────────────────────────────┤
│  @brashkie/waproto (Future)                 │  ← WhatsApp wire protocol
├─────────────────────────────────────────────┤
│  @brashkie/signalis ← YOU ARE HERE (v0.3.0) │  ← Signal Protocol logic
├─────────────────────────────────────────────┤
│  @brashkie/signalis-core (v0.2.0 ✅)        │  ← Crypto primitives
├─────────────────────────────────────────────┤
│  Audited Rust (curve25519-dalek, etc.)      │  ← Battle-tested crypto
└─────────────────────────────────────────────┘

🤝 Contributing

Contributions are welcome! Please read CONTRIBUTING.md for guidelines.

git clone https://github.com/Brashkie/signalis.git
cd signalis
npm install
npm test

For new features or breaking changes, open an issue first to discuss.


📜 License

Apache License 2.0 — see LICENSE for details.

Third-party licenses are listed in NOTICE.


🙏 Acknowledgments

Built on the shoulders of giants:


🔐 + ❤️ by Hepein Oficial

Report Bug · Request Feature · Documentation