@ecync/libsignal
v1.0.2
Published
End-to-end encryption primitives implementing a Signal-style double ratchet with PreKeys for Node.js
Maintainers
Readme
@ecync/libsignal
End-to-end encryption primitives implementing a Signal-style double ratchet with PreKeys for Node.js. This package exposes a minimal API for building, persisting, and using sessions to encrypt and decrypt messages.
Overview
@ecync/libsignal provides the core building blocks of a Signal-like protocol:
- Double Ratchet state management (
SessionRecord,SessionCipher). - Asynchronous session bootstrapping with PreKeys (
SessionBuilder). - Curve25519/X25519 key agreement and signatures (
curve). - HKDF key derivation and message MACs (
crypto). - Small, promise-based job serialiser to avoid concurrent state races (
queue_job).
The library is implemented in JavaScript and ships TypeScript declarations.
Features
- Forward secrecy via Double Ratchet chains.
- Asynchronous session setup using Signed PreKeys and (optional) one-time PreKeys.
- Compact wire format using protobuf (
WhisperMessage,PreKeyWhisperMessage). - Works with Node's
cryptowhen available, with a fallback tocurve25519-js. - Ships
index.d.tsfor TypeScript consumers.
Installation
Install with your preferred package manager:
npm install @ecync/libsignal
# or
yarn add @ecync/libsignal
# or
pnpm add @ecync/libsignalRecommended: Node.js 14+.
Quick Start
Below is a minimal example showing how to prepare keys, create a session to a remote device, and encrypt/decrypt.
const {
keyhelper,
ProtocolAddress,
SessionBuilder,
SessionCipher
} = require('@ecync/libsignal');
// For a complete working example, see example.js// 1) Your long-term identity + registration id
const ourIdentity = keyhelper.generateIdentityKeyPair();
const ourRegistrationId = keyhelper.generateRegistrationId();
// 2) Your Signed PreKey and optional one-time PreKey to publish server-side
const signed = keyhelper.generateSignedPreKey(ourIdentity, /*signedKeyId*/ 1);
const preKey = keyhelper.generatePreKey(/*keyId*/ 1001);
// 3) Minimal storage implementation (see Storage Interface below)
const storage = createInMemoryStorage({ ourIdentity, ourRegistrationId, preKey, signed });
// 4) Remote addressing (user id + device id)
const addr = new ProtocolAddress('alice', 1);
// 5) Remote device bundle (typically fetched from your server)
const remoteBundle = {
registrationId: 2222,
identityKey: /* Buffer */ Buffer.from('...', 'base64'),
signedPreKey: {
keyId: 1,
publicKey: Buffer.from('...', 'base64'),
signature: Buffer.from('...', 'base64'),
},
// Optional one-time preKey
preKey: {
keyId: 10001,
publicKey: Buffer.from('...', 'base64'),
},
};
// 6) Establish an outgoing session
const builder = new SessionBuilder(storage, addr);
await builder.initOutgoing(remoteBundle);
// 7) Encrypt a message
const cipher = new SessionCipher(storage, addr);
const { type, body, registrationId } = await cipher.encrypt(Buffer.from('hello'));
// Send { type, body } to the remote. type: 3 => PreKey message (first), 1 => normal.
// 8) Decrypt a message
// If first message from remote used a PreKey bundle:
// const plaintext = await cipher.decryptPreKeyWhisperMessage(remoteBodyBuffer)
// Otherwise:
// const plaintext = await cipher.decryptWhisperMessage(remoteBodyBuffer)Storage Interface
You provide persistent storage for identity, PreKeys, and session state. The library calls the following async methods on your storage object:
loadSession(id: string): Promise<SessionRecord | undefined | null>: Load a previously stored session record for a fully-qualified address (e.g.,"alice.1").storeSession(id: string, record: SessionRecord): Promise<void>: Persist a session record.isTrustedIdentity(identifier: string, identityKey: Buffer): Promise<boolean>: Return whetheridentityKeyis currently trusted foridentifier.loadPreKey(id: number): Promise<{ privKey: Buffer; pubKey: Buffer } | undefined>: Load one-time PreKey pair by id.removePreKey(id: number): Promise<void>: Remove a consumed PreKey.loadSignedPreKey(id: number): Promise<{ privKey: Buffer; pubKey: Buffer } | undefined>: Load signed PreKey pair by id.getOurRegistrationId(): Promise<number>: Return our local registration id.getOurIdentity(): Promise<{ privKey: Buffer; pubKey: Buffer } | { privKey: Buffer; pubKey: Buffer }>: Return our identity key pair.
Minimal in-memory example for demos/tests:
function createInMemoryStorage({ ourIdentity, ourRegistrationId, preKey, signed }) {
const sessions = new Map();
const preKeys = new Map([[preKey.keyId, preKey.keyPair]]);
const signedPreKeys = new Map([[signed.keyId, signed.keyPair]]);
return {
async loadSession(id) { return sessions.get(id) || null; },
async storeSession(id, record) { sessions.set(id, record); },
async isTrustedIdentity(/*id, identityKey*/) { return true; },
async loadPreKey(id) { return preKeys.get(id); },
async removePreKey(id) { preKeys.delete(id); },
async loadSignedPreKey(id) { return signedPreKeys.get(id); },
async getOurRegistrationId() { return ourRegistrationId; },
async getOurIdentity() { return ourIdentity; },
};
}API Surface
keyhelpergenerateIdentityKeyPair()→{ pubKey: Buffer, privKey: Buffer }generateRegistrationId()→numbergenerateSignedPreKey(identityKeyPair, signedKeyId)→{ keyId, keyPair, signature }generatePreKey(keyId)→{ keyId, keyPair }
ProtocolAddressnew ProtocolAddress(id: string, deviceId: number)toString()→"<id>.<deviceId>"
SessionBuilderconstructor(storage, remoteAddress)initOutgoing(deviceBundle)→ Initializes a session using a remote bundle
SessionCipherconstructor(storage, remoteAddress)encrypt(plaintext: Buffer)→{ type: 1|3, body: Buffer, registrationId: number }decryptWhisperMessage(body: Buffer)→BufferdecryptPreKeyWhisperMessage(body: Buffer)→Buffer
SessionRecord- Serialization helpers for persisting session state
errorsUntrustedIdentityKeyError,SessionError,MessageCounterError,PreKeyError
Protobufs
The wire messages are defined in protos/WhisperTextProtocol.proto and compiled to src/WhisperTextProtocol.js via protobufjs.
- Regenerate (requires
protobufjs-cli):
npx pbjs -t static-module -w commonjs -o ./src/WhisperTextProtocol.js ./protos/WhisperTextProtocol.protoAlternatively, run the helper script in a Unix-like shell:
./generate-proto.shTypeScript
Type definitions are provided via index.d.ts. Import using standard CJS/ESM interop and let your tooling infer types.
Security Notes
- Always verify and pin remote identity keys via your
isTrustedIdentitypolicy. - Persist session state atomically to avoid message key reuse after crashes.
- One-time PreKeys must be deleted after consumption (
removePreKey).
License
GPL-3.0-only. See https://www.gnu.org/licenses/gpl-3.0
- Copyright 2015-2016 Open Whisper Systems
- Copyright 2017-2018 Forsta Inc
