| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Note: this library has not yet undergone a formal third-party security audit. Review the code before relying on it, and please report any suspected vulnerability privately as described in SECURITY.md.
libomemo.js is a TypeScript implementation of the OMEMO Multi-End Message and Object Encryption protocol for XMPP. It provides ratcheting forward secrecy for synchronous and asynchronous messaging environments, enabling secure multi-device encrypted communication.
This library supports both version 0.3.0, which is what most XMPP clients support today, and the latest version 0.9.1 (also known as OMEMO2 or NEWMEMO).
The two versions are identified by their XML namespaces:
The version is chosen per device when constructing a SessionBuilder/SessionCipher — see Selecting the OMEMO version.
This library is crypto-only: it implements the X3DH key agreement and Double Ratchet, and produces/consumes the per-device ratchet wire format. Building XMPP stanzas, PEP bundles/device lists, and the XEP-0420 SCE payload encryption are the responsibility of the consumer (e.g. Converse).
This library started as a fork of libsignal-protocol-javascript by Open Whisper Systems and has been modernized, ported to TypeScript and adapted for the XMPP OMEMO specification. NodeJS support has also been added.
The OMEMO 2 (urn:xmpp:omemo:2) support reuses Curve25519↔Ed25519 field operations from libomemo-c (the C library used by Dino); see Acknowledgements.
npm install libomemo.jsOr include the UMD build directly in your webpage:
<script src="dist/libomemo.umd.cjs"></script>This library requires a modern JavaScript environment with support for:
These are available in all modern browsers and Node.js 15+.
// ES Modules
import { KeyHelper, SessionBuilder, SessionCipher, OMEMOAddress } from "libomemo.js";
// CommonJS
const { KeyHelper, SessionBuilder, SessionCipher, OMEMOAddress } = require("libomemo.js");
// Browser (UMD)
const { KeyHelper, SessionBuilder, SessionCipher, OMEMOAddress } = libomemo;Generate identity keys, registration ID, and PreKeys at install time:
const registrationId = KeyHelper.generateRegistrationId();
// Store registrationId somewhere durable and safe.
const identityKeyPair = await KeyHelper.generateIdentityKeyPair();
// Store identityKeyPair somewhere durable and safe.
const preKey = await KeyHelper.generatePreKey(keyId);
store.storePreKey(preKey.keyId, preKey.keyPair);
const signedPreKey = await KeyHelper.generateSignedPreKey(identityKeyPair, keyId, version);
store.storeSignedPreKey(signedPreKey.keyId, signedPreKey.keyPair);
// Register preKeys and signedPreKey with the XMPP serverImplement a storage interface for managing keys and session state (see src/session/store.ts for an example), then establish sessions:
const store = new MyOMEMOProtocolStore();
const address = new OMEMOAddress(recipientId, deviceId);
// The OMEMO version is required (no default); it is the protocol's XML namespace:
// "eu.siacs.conversations.axolotl" (XEP-0384 v0.3.0) or "urn:xmpp:omemo:2".
const sessionBuilder = new SessionBuilder(store, address, "urn:xmpp:omemo:2");
// Process a PreKey bundle from the server
try {
await sessionBuilder.processPreKey({
registrationId: <Number>,
identityKey: <ArrayBuffer>,
signedPreKey: {
keyId: <Number>,
publicKey: <ArrayBuffer>,
signature: <ArrayBuffer>
},
preKey: {
keyId: <Number>,
publicKey: <ArrayBuffer>
}
});
// Session established — ready to encrypt
} catch (error) {
// Handle identity key conflict
}const sessionCipher = new SessionCipher(store, address, "urn:xmpp:omemo:2");
const ciphertext = await sessionCipher.encrypt("Hello world");
// ciphertext -> { type: <Number>, body: <string>, kex?: <boolean> }
// For omemo:2, `kex` indicates whether `body` is an OMEMOKeyExchange (true)
// or a plain OMEMOAuthenticatedMessage (false).Both decrypt methods resolve to a DecryptResult:
const sessionCipher = new SessionCipher(store, address, "urn:xmpp:omemo:2");
// Decrypt a PreKey/key-exchange message (establishes session if needed)
try {
const { plaintext } = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
} catch (error) {
// Handle identity key conflict
}
// Decrypt a regular message using existing session
const { plaintext, ratchet } = await sessionCipher.decryptWhisperMessage(ciphertext);
// `ratchet.counter` (message index in the sender's chain) and `ratchet.key`
// (the sender's 33-byte 0x05-prefixed ratchet public key) let you implement
// protocol rules such as the OMEMO heartbeat.OMEMO eu.siacs.conversations.axolotl and urn:xmpp:omemo:2 are distinct wire protocols with separate sessions, bundles, and PEP nodes. Which one to use is decided per recipient device, based on the version(s) that device advertises (i.e. which device-list PEP node it publishes to). Pass that version to every SessionBuilder/SessionCipher for that device. There is intentionally no default — passing the wrong version fails loudly rather than silently producing an undecryptable message.
For omemo:2, the identity key is published in its Ed25519 form. Derive it from your Curve25519 identity key when building your bundle:
import { curvePubKeyToEd25519PubKey } from "libomemo.js";
const ik = await curvePubKeyToEd25519PubKey(identityKeyPair.pubKey); // 32-byte Ed25519This matches the encoding used by libomemo-c (and thus interoperating clients such as Dino): the Ed25519 identity key is derived from the public key with the Edwards sign bit forced to zero.
A peer's omemo:2 bundle/key-exchange carries that same Ed25519 identity key; pass it through unchanged as identityKey and the library converts it to Curve25519 internally for the key agreement.
Key generation utilities for OMEMO protocol setup.
| Method | Description |
|---|---|
| generateRegistrationId() | Generate a unique registration ID |
| generateIdentityKeyPair() | Generate an identity key pair |
| generatePreKey(keyId) | Generate an unsigned PreKey |
| generateSignedPreKey(identityKeyPair, keyId, version) | Generate a signed PreKey (version is the OMEMO namespace) |
Handles session establishment with remote recipients.
| Method | Description |
|---|---|
| processPreKey(preKeyBundle) | Build a session from a PreKey bundle |
Encrypts and decrypts messages for established sessions.
| Method | Description |
|---|---|
| encrypt(plaintext) | Encrypt a message |
| decryptPreKeyWhisperMessage(ciphertext) | Decrypt and establish session |
| decryptWhisperMessage(ciphertext) | Decrypt using existing session |
Represents a recipient address (JID + device ID tuple).
const address = new OMEMOAddress(recipientId, deviceId);Low-level cryptographic functions for advanced use cases:
getRandomBytes, encrypt, decrypt, sign, hash, HKDF, verifyMAC, createKeyPair, ECDHE, Ed25519Sign, Ed25519Verify
The Curve25519 operations (key generation, ECDH, signing/verification, and the OMEMO 2 IdentityKey conversions) can be moved off the main thread. Call startWorker(url) with the bundled worker script; every subsequent OMEMO operation then runs in the worker. stopWorker() reverts to the main thread.
import { startWorker, stopWorker, KeyHelper } from "libomemo.js";
startWorker("/path/to/libomemo-worker.js"); // serve dist/libomemo-worker.js
const identityKeyPair = await KeyHelper.generateIdentityKeyPair(); // runs in the workerIf the worker cannot be loaded (wrong URL, blocked by CSP, script error) or a call does not get a reply within the timeout (default 10s, set with startWorker(url, { timeout }), 0 disables it), the call logs an error and falls back to the main-thread WebAssembly, so crypto keeps working. Errors the worker reports for a specific operation (an invalid signature, for example) are propagated unchanged and do not trigger a fallback.
Whether operations are currently offloaded is observable. In the default build a fallback means private key operations have moved onto the main thread, which some applications will want to surface or act on:
startWorker("/path/to/libomemo-worker.js", {
onStatusChange: ({ offloaded, error }) => {
if (!offloaded) console.warn("OMEMO crypto is on the main thread", error);
},
});The callback is edge-triggered. It fires when offloading starts or stops, not per operation, and not for stopWorker().
Important: The worker script runs inside the trust boundary and it receives raw private keys. Serve it as a trusted, same-origin script under your own CSP, and do not build its URL from remote or user-supplied input.
For apps that always run a worker, libomemo.js/worker-client is a second build with the same API but no embedded wasm, so the worker holds the only copy:
import { startWorker, KeyHelper } from "libomemo.js/worker-client";
startWorker("/path/to/libomemo-worker.js"); // required: this build has no local wasm
const identityKeyPair = await KeyHelper.generateIdentityKeyPair();Any operation attempted before a worker is started (or while the worker is unavailable) rejects with a clear error, since there is no main-thread fallback.
This build is ESM-only and intended for the browser; under Node, use the default build.
# Install dependencies
npm install
# Compile native Curve25519 code (requires Emscripten)
npm run compile
# Build TypeScript distribution
npm run dist
# Full build (compile + dist)
npm run build
# Watch mode for development
npm run dev# Run all tests (Node.js + Headless Chrome)
npm test
# Run tests in Chrome browser
npm run test:browser
# Run tests in headless Chrome only
npm run test:headless
# Run Node.js tests only
npm run test:nodeContributions are welcome! Please follow these guidelines:
Please ensure all new functionality includes tests and follows existing code conventions.
Thank you to the NLNet Foundation for sponsoring work on this library.
Large Language Models (DeepSeek 4 Pro, Qwen 3.7 max and Claude Opus 4.8) were used to assist with tasks such as writing code, editing text and research.
Where AI was not used: no cryptographic primitive or protocol logic was designed or invented by an LLM. The cryptographic core derives from established, widely-reviewed sources:
AI assistance was limited to porting, tests, tooling and documentation.
Correctness is checked independently of any AI:
Any LLM-generated content is carefully and manually reviewed, as would any 3rd party contribution. A human maintainer remains responsible for every line regardless of how it was drafted.
| Back | FazBrowse Home | New Git URL |