libomemo.js
v2.0.2
Published
[](https://github.com/conversejs/libomemo.js/actions/workflows/karma-tests.yml) [](https://www.np
Readme
libomemo.js
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 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.
Table of Contents
- Features
- Installation
- Requirements
- Quick Start
- API Reference
- Building from Source
- Testing
- Contributing
- Acknowledgements
- LLM and GenAI usage
- License
Features
- 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)
Installation
npm install libomemo.jsOr include the UMD build directly in your webpage:
<script src="dist/libomemo.umd.js"></script>Requirements
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+.
Quick Start
Import
// 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;Setup
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 serverBuilding a Session
Implement 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
}Encrypting Messages
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).Decrypting Messages
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.Selecting the OMEMO version
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.
API Reference
KeyHelper
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) |
SessionBuilder
Handles session establishment with remote recipients.
| Method | Description |
| ----------------------------- | ------------------------------------ |
| processPreKey(preKeyBundle) | Build a session from a PreKey bundle |
SessionCipher
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 |
OMEMOAddress
Represents a recipient address (JID + device ID tuple).
const address = new OMEMOAddress(recipientId, deviceId);Crypto Utilities
Low-level cryptographic functions for advanced use cases:
getRandomBytes, encrypt, decrypt, sign, hash, HKDF, verifyMAC, createKeyPair, ECDHE, Ed25519Sign, Ed25519Verify
Building from Source
Prerequisites
- Node.js 18+
- Emscripten (for compiling native Curve25519 code)
Build Commands
# 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 devTesting
# 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:nodeContributing
Contributions 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.
Acknowledgements
Thank you to the NLNet Foundation for sponsoring work on this library.
- 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.
LLM and GenAI usage
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:
- libsignal for the Double Ratchet and X3DH
- libomemo-c for the OMEMO 2 Curve25519 & Ed25519 operations
AI assistance was limited to porting, tests, tooling and documentation.
Correctness is checked independently of any AI:
- the test suite runs on Node.js and in a real browser (via Playwright) against known-answer test vectors;
- the omemo:2 wire format is pinned by a cross-implementation interop vector generated by libomemo-c (the C library Dino uses): the suite decrypts libomemo-c's real ciphertexts and verifies its signatures, and the reverse direction (libomemo-c decrypting this library's output) is confirmed out of band.
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.
License
- Copyright 2015-2018 Open Whisper Systems
- Copyright 2022-2026 JC Brand
- Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
