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 legacy version is
eu.siacs.conversations.axolotl - and the current one is
urn:xmpp:omemo:2.
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.
- Features
- Installation
- Requirements
- Quick Start
- API Reference
- Building from Source
- Testing
- Contributing
- Acknowledgements
- License
- Double Ratchet Protocol — Forward secrecy and post-compromise security
- Multi-device support — Encrypt messages for multiple devices simultaneously
- PreKey management — Asynchronous session establishment via PreKey bundles
- TypeScript native — Full type definitions included
- Browser & Node.js compatible — ESM, UMD, and CommonJS support
- Curve25519 — High-performance elliptic curve cryptography (compiled via Emscripten)
npm install libomemo.jsOr include the UMD build directly in your webpage:
<script src="dist/libomemo.umd.js"></script>This library requires a modern JavaScript environment with support for:
ArrayBufferTypedArrayPromiseWebCryptowith:- AES-CBC
- HMAC SHA-256
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).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 = await sessionCipher.decryptWhisperMessage(ciphertext);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
- Node.js 18+
- Emscripten (for compiling native Curve25519 code)
# 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:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests (
npm test) and linting (npm run lint) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure all new functionality includes tests and follows existing code conventions.
- libsignal-protocol-javascript by Open Whisper Systems — the original codebase this library was forked from.
- libomemo-c — the OMEMO 2 support reuses its
Curve25519↔Ed25519 field operations (
fe_montx_to_edy/fe_edy_to_montxand the supportingfe_*/ge_*files undernative/ed25519/additions/), and the conversion wrappers innative/ed25519/additions/omemo_convert.cfollow its approach so the on-wire identity-key encoding interoperates with libomemo-c-based clients such as Dino.
Copyright 2015-2018 Open Whisper Systems
Copyright 2022-2026 JC Brand
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html