@substrate-system/x3dh

x3dh

tests types module semantic versioning install size GZip size License: ISC

TypeScript implementation of X3DH, as described in Going Bark: A Furry's Guide to End-to-End Encryption.

This is a fork of soatok/rawr-x3dh. Thanks @soatok for working in public.

This library implements the Extended Triple Diffie-Hellman key exchange, with a few minor tweaks:

  1. Identity keys are Ed25519 public keys, not X25519 public keys. See this for an explanation.
  2. Encryption/decryption and KDF implementations are pluggable (assuming you implement the interface I provide), so you aren't married to HKDF or a particular cipher. (Although I recommend hard-coding it to your application!)

This library is designed to work across different JavaScript environments:

  • Browser environments - Works with the Web Crypto API and handles Ed25519 key format differences
  • Node.js - Uses @substrate-system/one-webcrypto for compatibility
  • Web Workers - Full support for background processing
  • React Native - Compatible with React Native's crypto implementations

The library now handles Ed25519 key format differences between browsers and Node.js automatically:

  • Automatically detects and supports both raw (32-byte) and structured (SPKI/PKCS8) Ed25519 key formats
  • Exports keys in the format most compatible with the current environment
  • Fallback mechanisms ensure keys work across different Web Crypto API implementations

The DefaultIdentityKeyManager no longer depends on Node.js filesystem operations. Instead:

  • Use exportIdentityKeypair() to get key data for storage
  • Use loadIdentityKeypair(storedData) to restore keys from your preferred storage method
  • Implement your own storage strategy (localStorage, AsyncStorage, database, etc.)
npm i -S @substrate-system/x3dh

First, import the X3DH class from the module.

import { X3DH } from '@substrate-system/x3dh'

const x3dh = new X3DH()

Note: You can pass some classes to the constructor to replace the algorithm implementations.

import { X3DH } from '@substrate-system/x3dh'

const x3dh = new X3DH(
sessionKeyManager, /* SessionKeyManagerInterface */
identityKeyManager, /* IdentityKeyManagerInterface */
symmetricEncryptionHandler, /* SymmetricEncryptionInterface */
keyDerivationFunction /* KeyDerivationFunction */
)

Once your X3DH object is instantiated, you will be able to initialize handshakes either as a sender or as a recipient. Then you will be able to encrypt additional messages on either side.

const firstEncrypted = await x3dh.initSend(
'recipient@server2',
serverApiCallFunc,
firstMessage
);

The serverApiCallFunc parameter should be a function that sends a request to the server to obtain the identity key, signed pre-key, and optional one-time key for the handshake.

See the definition of the InitClientFunction type in src/index.ts.

Once this has completed, you can call encryptNext() multiple times to append messages to send.

const nextEncrypted = await x3dh.encryptNext(
'recipient@server2',
'This is a follow-up message UwU'
);

On the other side, your communication partner will use the following feature.

const [sender, firstMessage] = await x3dh.initRecv(senderInfo);
const nextMessage = await x3dh.decryptNext(sender, nextEncrypted);

Note: initRecv() will always return the sender identity (a string) and the message (a Buffer that can be converted to a string). The sender identity should be usable for decryptNext() calls.

However, that doesn't mean it's trustworthy! This library only implements the X3DH pattern. It doesn't implement the Gossamer integration.

Don't use it in production until version 1.0.0 has been tagged. The API can break at any moment until that happens (especially if I decide I hate the default key management classes I wrote).

However, feel free to test and play with it.