@substrate-system/webcrypto-x3dh
    Preparing search index...

    @substrate-system/webcrypto-x3dh

    webcrypto x3dh

    tests types module semantic versioning install size GZip size license

    X3DH for the browser. This is a typeScript implementation of X3DH, as described in Going Bark: A Furry's Guide to End-to-End Encryption.

    This uses the Web Crypto API, so it works in the browser.

    X3DH (Extended Triple Diffie-Hellman) is a secure key exchange protocol for end-to-end encrypted communication. It allows two parties to establish a shared secret for encrypted messaging, even when one party is offline.

    This library handles key exchange only. It returns a shared secret that you can use with a ratcheting protocol (like Double Ratchet) for ongoing message encryption. No session state is stored — this does key exchange only.

    The standard X3DH handshake usually includes three exchanges:

    1. Alice’s ephemeral key + Bob’s identity key
    2. Alice’s ephemeral key + Bob’s signed prekey
    3. (Optionally) Alice’s identity key + Bob’s signed prekey

    These three Diffie–Hellman results are then concatenated and passed through a KDF (key derivation function) to produce a single strong shared secret.

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

    npm i -S @substrate-system/webcrypto-x3dh
    

    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
    • Node.js - Uses the webcrypto API in Node
    • Web Workers - Background processing

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

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

    See Signal docs - 3.1. Overview

    X3DH has three phases:

    1. Bob publishes his identity key and prekeys to a server.
    2. Alice fetches a "prekey bundle" from the server, and uses it to send an initial message to Bob.
    3. Bob receives and processes Alice's initial message.

    Use @substrate-system/keys for identity key management.

    Alice wants to start a conversation with Bob. Bob has already published some public keys to the server.

    Every user has an identity key (IK) — a long-term, static ed25519 keypair.

    At this point Bob should have published an identity key, a signed pre-key (SPK), and a one-time pre-key (OPK).

    Alice must have an identity key and an ephemeral key (EK), which is created fresh for each session.

    1. Alice fetches Bob's identity key, signed pre-key, and possibly one-time pre-key
    2. Alice computes 4 Diffie-Hellman exchanges:
    3. IK_A x SPK_B
    4. EK_A x IK_B
    5. EK_A x SPK_B
    6. EK_A x OPK_B
    7. Those results are combined via a KDF to produce a shared secret.
    8. Alice sends her first encrypted message to Bob, including EK_A, her identity info, and the used prekey identifiers
    9. Bob does the same DH calculations and derives the same shared secret.

    When all is said and done, both parties have the same shared secret key material.

    • The ephemeral keys give us forward secrecy, meaning that if long-term keys are compromised, previous sessions remain secure.
    • The pre-keys are signed, which prevents impersonation attacks.

    Since messages are authenticated via ephemeral DH rather than digital signatures on each message, a third party can’t cryptographically prove you authored a message.

    One-time pre-keys prevent an attacker from re-using an initial handshake to trick you into deriving the same session key again.

    import { EccKeys } from '@substrate-system/keys/ecc'
    import { X3DH } from '@substrate-system/webcrypto-x3dh'

    // 1. Create identity keys with @substrate-system/keys
    const aliceKeys = await EccKeys.create()
    await aliceKeys.persist()

    // 2. Generate X25519 pre-keys
    const preKeyPair = await X3DH.prekeys()

    // 3. Create X3DH keys instance
    // - aliceKeys has privateWriteKey and publicWriteKey properties
    // - preKeyPair has privateKey and publicKey properties
    const x3dhKeys = X3DH.X3DHKeys(aliceKeys, preKeyPair)

    // 4. Initialize X3DH with keys and identity string
    const x3dh = new X3DH(x3dhKeys, aliceKeys.DID)

    // 5. Generate one-time keys for key exchange
    const oneTimeKeyBundle = await x3dh.generateOneTimeKeys(10)

    // Upload oneTimeKeyBundle to your server so others can retrieve them
    // during key exchange

    // 6. Perform key exchange with another user
    // First, fetch the recipient's public keys from your server
    const recipientKeys = await fetch('/api/keys/recipient-did').then(r => r.json())
    // recipientKeys has shape: {
    // IdentityKey,
    // SignedPreKey: { Signature, PreKey },
    // OneTimeKey?
    // }

    const result = await x3dh.initSend('recipient-did', recipientKeys)

    // Send the handshake to the recipient (via your server or direct messaging),
    // so they can derive the same secret.
    await sendToRecipient(result.handshakeData)

    // X3DH is now complete. Use the shared secret to initialize your
    // ratcheting protocol (e.g., Double Ratchet)
    const doubleRatchet = new DoubleRatchet(result.sharedSecret)

    The other side of the conversation above.

    import { EccKeys } from '@substrate-system/keys/ecc'
    import { X3DH } from '@substrate-system/webcrypto-x3dh'

    // 1. Create identity keys with @substrate-system/keys
    const bobKeys = await EccKeys.create()
    await bobKeys.persist()

    // 2. Generate X25519 pre-keys
    const preKeyPair = await X3DH.prekeys()

    // 3. Create X3DH keys instance
    const x3dhKeys = X3DH.X3DHKeys(bobKeys, preKeyPair)

    // 4. Initialize X3DH with keys and identity string
    const x3dh = new X3DH(x3dhKeys, bobKeys.DID)

    // 5. Generate one-time keys for key exchange
    const oneTimeKeyBundle = await x3dh.generateOneTimeKeys(10)

    // Upload oneTimeKeyBundle to your server so others can retrieve them
    // during key exchange

    // 6. Receive handshake data from Alice
    const handshakeData = await receiveFromSender() // Get handshake from Alice

    const result = await x3dh.initReceive(handshakeData)

    // X3DH is now complete. Both sides have the same shared secret.
    //
    // result.sharedSecret is a Uint8Array - the same value Alice has
    // result.senderIdentity is Alice's DID

    // Use the shared secret to initialize your ratcheting protocol
    // (e.g., Double Ratchet)
    const doubleRatchet = new DoubleRatchet(result.sharedSecret)
    class X3DH {
    static X3DHKeys (
    idKeys:{
    privateWriteKey:Ed25519SecretKey,
    publicWriteKey:Ed25519PublicKey
    },
    preKeypair:{
    privateKey:X25519SecretKey,
    publicKey:X25519PublicKey
    }
    ):X3DHKeys
    }
    type X3DHKeys = {
    identitySecret:Ed25519SecretKey
    identityPublic:Ed25519PublicKey
    preKeySecret:X25519SecretKey
    preKeyPublic:X25519PublicKey
    }
    const x3dh = new X3DH(
    x3dhKeys, // X3DHKeys (required)
    identityString, // string/DID (required)
    keyDerivationFunction // KeyDerivationFunction (optional)
    )

    Once your X3DH object has been created, you can perform key exchanges:

    // Generate one-time keys for others to use
    const oneTimeKeyBundle = await x3dh.generateOneTimeKeys(10)
    // Upload oneTimeKeyBundle to your server so others can retrieve them
    // during key exchange

    // Initiate communication (sender side)
    // First, fetch the recipient's public keys from your server
    const recipientKeys = await fetch('/api/keys/recipient-did-string')
    .then(r => r.json())

    const result = await x3dh.initSend('recipient-did-string', recipientKeys)

    // result.sharedSecret is a Uint8Array you use to initialize your
    // ratcheting protocol
    // result.handshakeData is sent to the recipient

    The recipientKeys parameter should be an object containing the recipient's identity key, signed pre-key, and optional one-time key.

    See the definition of the InitServerInfo type in src/index.ts:

    type InitServerInfo = {
    IdentityKey:string;
    SignedPreKey:{
    Signature:string;
    PreKey:string;
    };
    OneTimeKey?:string;
    };

    Derive the same secret value as the sender.

    // Receive handshake (recipient side)
    const result = await x3dh.initReceive(handshakeData)

    // X3DH is now complete. Both sides have the same shared secret.
    // result.sharedSecret is a Uint8Array - the same value the sender has
    // result.senderIdentity is the sender's DID

    // Use the shared secret to initialize your ratcheting protocol
    // (e.g., Double Ratchet)
    const doubleRatchet = new DoubleRatchet(result.sharedSecret)
    Note


    This library only performs the X3DH key exchange. For ongoing message encryption, you need to implement or use a ratcheting protocol like Double Ratchet with the shared secret returned from initSend() and initReceive().

    This library does not implement Gossamer integration for identity key management.

    type X3DHKeys = {
    identitySecret: Ed25519SecretKey
    identityPublic: Ed25519PublicKey
    preKeySecret: X25519SecretKey
    preKeyPublic: X25519PublicKey
    }

    type InitServerInfo = {
    IdentityKey: string;
    SignedPreKey: {
    Signature: string;
    PreKey: string;
    };
    OneTimeKey?: string;
    }

    type InitSenderInfo = {
    Sender: string,
    IdentityKey: string,
    PreKey: string,
    EphemeralKey: string,
    OneTimeKey?: string
    }

    type InitSendResult = {
    sharedSecret: Uint8Array
    handshakeData: InitSenderInfo
    }

    type InitReceiveResult = {
    sharedSecret: Uint8Array
    senderIdentity: string
    }
    • X3DH - Main class for X3DH key exchange operations

    Main class for performing X3DH key exchanges.

    Static Methods:

    X3DH.prekeys():Promise<CryptoKeyPair>
    

    Generate an X25519 key pair for use as pre-keys in X3DH.

    Returns: A promise resolving to a CryptoKeyPair with privateKey and publicKey.

    Example:

    const preKeyPair = await X3DH.prekeys()
    
    X3DH.X3DHKeys(
    idKeys:{
    privateWriteKey:Ed25519SecretKey,
    publicWriteKey:Ed25519PublicKey
    },
    preKeypair:{
    privateKey:X25519SecretKey,
    publicKey:X25519PublicKey
    }
    ):X3DHKeys

    Helper function to construct an X3DHKeys object from identity keys and pre-keys.

    Parameters:

    • idKeys - Object with privateWriteKey and publicWriteKey (Ed25519 keys from @substrate-system/keys)
    • preKeypair - Object with privateKey and publicKey (X25519 keys from X3DH.prekeys())

    Returns: X3DHKeys object for use in X3DH constructor

    Example:

    const x3dhKeys = X3DH.X3DHKeys(aliceKeys, preKeyPair)
    

    Constructor:

    new X3DH(
    keys:{
    identitySecret:Ed25519SecretKey
    identityPublic:Ed25519PublicKey
    preKeySecret:X25519SecretKey
    preKeyPublic:X25519PublicKey
    },
    identityString:string,
    kdf?:KeyDerivationFunction
    )

    Creates a new X3DH instance.

    Parameters:

    • keys: X3DHKeys (required) - Identity and pre-keys for this participant
    • identityString: string (required) - Unique identifier (typically a DID) for this participant
    • kdf?: KeyDerivationFunction (optional) - Custom key derivation function (defaults to blakeKdf)

    Example:

    const x3dh = new X3DH(x3dhKeys, aliceKeys.DID)
    

    Instance Methods:

    generateOneTimeKeys(numKeys?:number):Promise<{
    signature:string
    bundle:string[]
    }>

    Generates and signs a bundle of one-time keys for use in X3DH handshakes. Stores them locally for later retrieval during initReceive().

    Parameters:

    • numKeys?:number (default: 100) - Number of one-time keys to generate

    Returns: Promise resolving to SignedBundle with:

    • signature:string - Hex-encoded signature over the bundle
    • bundle:string[] - Array of hex-encoded public keys

    Example:

    const bundle = await x3dh.generateOneTimeKeys(10)
    // Upload bundle to server for distribution

    Initiates X3DH key exchange as the sender. Performs X3DH handshake and returns the shared secret.

    initSend(
    recipientIdentity:string,
    recipientKeys:{
    IdentityKey:string
    SignedPreKey:{
    Signature:string
    PreKey:string
    }
    OneTimeKey?:string
    }
    ):Promise<{
    sharedSecret: Uint8Array
    handshakeData: InitSenderInfo
    }>

    Parameters:

    • recipientIdentity:string - DID or identifier of the recipient
    • recipientKeys:InitServerInfo - Object containing recipient's public keys

    Returns: Promise resolving to InitSendResult containing:

    • sharedSecret: Uint8Array - The derived shared secret from X3DH
    • handshakeData: InitSenderInfo - Handshake data to send to recipient

    Example:

    // Fetch recipient's keys from your server
    const recipientKeys = await fetch('/api/keys/did:example:bob').then(r => r.json())

    const result = await x3dh.initSend('did:example:bob', recipientKeys)

    // result.sharedSecret is a Uint8Array to use for your ratcheting protocol
    // Send result.handshakeData to the recipient

    Receive and process an initial handshake message from a sender. Returns the shared secret and sender's identity.

    initReceive(req:{
    Sender:string
    IdentityKey:string
    PreKey:string
    EphemeralKey:string
    OneTimeKey?:string
    }):Promise<{
    sharedSecret: Uint8Array
    senderIdentity: string
    }>

    Parameters:

    • req:InitSenderInfo - Handshake data received from sender

    Returns: Promise resolving to InitReceiveResult containing:

    • sharedSecret: Uint8Array - The derived shared secret from X3DH
    • senderIdentity: string - The sender's DID

    Example:

    const result = await x3dh.initReceive(handshakeData)
    console.log(`Received from ${result.senderIdentity}`)
    // Use result.sharedSecret to initialize your ratcheting protocol

    Sign an X25519 pre-key with an Ed25519 identity key. Use to create signed pre-key bundles.

    signPreKey(signingKey:Ed25519SecretKey, preKey:X25519PublicKey):Promise<string>
    

    Parameters:

    • signingKey: Ed25519SecretKey - Identity secret key for signing
    • preKey: X25519PublicKey - Pre-key public key to sign

    Returns: hex-encoded signature

    Update the identity string for this X3DH instance.

    setIdentityString(id:string):void
    

    Parameters:

    • id:string - New identity string (DID)

    Don't use it in production until version 1.0.0 has been tagged. The API can break at any moment until that happens.

    However, feel free to test and play with it.