The @wireapp/core-crypto package ships two entry points:
@wireapp/core-crypto/browser — WASM build for browsers and browser-compatible runtimes.
@wireapp/core-crypto/native — Native N-API build for Node.js and Bun.
This guide covers the browser / WASM path. The native path uses the same API surface but omits the WASM initialisation step.
Installation
npm install @wireapp/core-crypto
yarn add @wireapp/core-crypto
bun add @wireapp/core-crypto
Setup
Initialise the WASM module
Before calling any CoreCrypto API you must load the WASM binary. The
package includes the .wasm file at
@wireapp/core-crypto/out/browser/autogenerated/wasm-bindgen/index_bg.wasm.
Copy that file to a location your web server can serve, then call
initWasmModule.import { initWasmModule } from "@wireapp/core-crypto/browser";
// Pass the directory that serves `index_bg.wasm`.
// Omit the argument and the module looks for `index_bg.wasm` at the root.
await initWasmModule("/assets/core-crypto/");
initWasmModule must be called exactly once per page load, before any
other import from this package is used. In non-browser runtimes (Bun,
Node.js) the function reads the file from disk using fs/promises and
no server path is required.
Open a database
CoreCrypto stores cryptographic material in an encrypted SQLite database.
In browsers the database is backed by IndexedDB via the absurd-sql
layer; the location string becomes the IndexedDB store name.import {
Database,
DatabaseKey,
} from "@wireapp/core-crypto/browser";
// Generate or restore a 32-byte database key.
const rawKey = crypto.getRandomValues(new Uint8Array(32));
const databaseKey = new DatabaseKey(rawKey.buffer);
const database = await Database.open(
"my-app-keystore", // store name (IndexedDB in browsers)
databaseKey
);
IndexedDB storage is origin-scoped and persists across page reloads.
You are responsible for deriving and safely storing the DatabaseKey
so that the database can be reopened. A mismatched key will cause
Database.open to throw.
Create a CoreCrypto instance
import { CoreCrypto } from "@wireapp/core-crypto/browser";
const coreCrypto = CoreCrypto.new(database);
Initialise MLS inside a transaction
All state-mutating operations go through a transaction. If the
callback throws, every change made inside it is rolled back.import {
ClientId,
Credential,
Ciphersuite,
} from "@wireapp/core-crypto/browser";
const encoder = new TextEncoder();
const clientId = new ClientId(
encoder.encode("[email protected]/device-1").buffer
);
await coreCrypto.newTransaction(async (ctx) => {
// Attach the MLS transport before initialising.
await ctx.mlsInit(clientId, myMlsTransport);
// Add a credential so you can generate key packages.
await ctx.addCredential(
Credential.basic(Ciphersuite.Mls128Dhkemx25519Aes128gcmSha256Ed25519, clientId)
);
});
Implementing MlsTransport
CoreCrypto calls your transport implementation whenever it needs to deliver a
commit bundle or a plain MLS message to the delivery service.
import type {
MlsTransport,
CommitBundle,
MlsTransportResponse,
HistorySecret,
MlsTransportData,
} from "@wireapp/core-crypto/browser";
import { MlsTransportResponse as Resp } from "@wireapp/core-crypto/browser";
const myMlsTransport: MlsTransport = {
async sendCommitBundle(commitBundle: CommitBundle): Promise<MlsTransportResponse> {
// Send commit + welcome + group info to your delivery service.
// Return Success, or a RetryAfter / Abort variant on failure.
await deliveryService.postCommit(commitBundle);
return Resp.Success.new();
},
async sendMessage(mlsMessage: ArrayBuffer): Promise<MlsTransportResponse> {
await deliveryService.postMessage(mlsMessage);
return Resp.Success.new();
},
async prepareForTransport(historySecret: HistorySecret): Promise<MlsTransportData> {
// Encode data for the history client (only required when history sharing is enabled).
return historySecret.clientId.copyBytes();
},
};
Working with MLS groups
Creating a conversation
import { ConversationId } from "@wireapp/core-crypto/browser";
const conversationId = new ConversationId(
new TextEncoder().encode("room-42").buffer
);
await coreCrypto.newTransaction(async (ctx) => {
// Retrieve a credential ref to use for this conversation.
const [credentialRef] = await ctx.getCredentials();
await ctx.createConversation(conversationId, credentialRef!);
});
Adding members
To invite another client, obtain their key package first, then call
addClientsToConversation. The resulting CommitBundle is delivered to the
transport automatically.
await coreCrypto.newTransaction(async (ctx) => {
// bobKeyPackage is a Keypackage received from the server.
await ctx.addClientsToConversation(conversationId, [bobKeyPackage]);
});
The invited client processes the welcome:
await bobCoreCrypto.newTransaction(async (ctx) => {
// welcomeMessage is the Welcome bytes from the commit bundle.
const welcomeBundle = await ctx.processWelcomeMessage(welcomeMessage);
console.log("Joined conversation", welcomeBundle.id);
});
Encrypting and decrypting messages
const plaintext = new TextEncoder().encode("Hello, MLS!").buffer;
// Sender encrypts.
const ciphertext = await coreCrypto.newTransaction(async (ctx) =>
ctx.encryptMessage(conversationId, plaintext)
);
// Receiver decrypts.
const decrypted = await bobCoreCrypto.newTransaction(async (ctx) =>
ctx.decryptMessage(conversationId, ciphertext)
);
if (decrypted.message !== undefined) {
const text = new TextDecoder().decode(decrypted.message);
console.log(text); // "Hello, MLS!"
}
Generating key packages
Before another client can add you to a group they need one of your key
packages. Generate them inside a transaction and upload them to your key
distribution service.
const keyPackage = await coreCrypto.newTransaction(async (ctx) => {
const [credentialRef] = await ctx.getCredentials();
return ctx.generateKeypackage(credentialRef!, undefined /* no explicit lifetime */);
});
// Serialise for upload.
const keyPackageBytes = await keyPackage.serialize();
TypeScript-specific notes
CoreCrypto vs CoreCryptoContext
| Class | What it is |
|---|
CoreCrypto | Long-lived handle to the encrypted database. Acquired via CoreCrypto.new(database). |
CoreCryptoContext | Short-lived transaction context. Created by newTransaction and passed to your callback. |
All MLS and Proteus operations are methods on CoreCryptoContext. The
CoreCrypto instance itself exposes only newTransaction and a handful of
non-mutating helpers such as getClientIds and conversationEpoch.
Async / await pattern
Every method that crosses the Rust boundary is async. Chain calls inside a
single newTransaction callback to keep them atomic:
// Good: atomic.
await coreCrypto.newTransaction(async (ctx) => {
await ctx.mlsInit(clientId, transport);
await ctx.addCredential(credential);
});
// Risky: two separate transactions — not atomic.
await coreCrypto.newTransaction((ctx) => ctx.mlsInit(clientId, transport));
await coreCrypto.newTransaction((ctx) => ctx.addCredential(credential));
Numeric types
Rust u64 values (e.g. epoch numbers, validity timestamps) arrive as
bigint in TypeScript. Use BigInt arithmetic and avoid mixing them with
number literals without an explicit cast.
Browser storage requirements
The WASM build stores data via IndexedDB. Browsers that block third-party
storage or run in private/incognito mode may have limited or ephemeral
IndexedDB access. Test your storage behaviour in the browser environments
you target.
Building from source
See the general requirements for Rust and WASM
toolchain setup. Then:
# Install the correct wasm-bindgen-cli version.
wasm_bindgen_version="$(
cargo metadata --format-version 1 |
jq -r '.packages[] | select (.name == "wasm-bindgen") | .version'
)"
cargo install wasm-bindgen-cli --version $wasm_bindgen_version
# Build the TypeScript package.
make ts
# Build and run tests in a headless browser.
make ts-test
The built package lands in crypto-ffi/bindings/js/out/.