@me2em/crypto
    Preparing search index...

    Function establishChannel

    • Establishes a channel from an X3DH shared secret.

      ───────────────────────────────────────────────────────────── Key hierarchy ─────────────────────────────────────────────────────────────

      X3DH sharedSecret (32B, raw) └─ HKDF(sharedSecret, salt=channelSalt, info='…/channel-root/v1') ├─ rootKeyMaterial (32B) — channel-local secret └─ channelId = SHA-256(sharedSecret ‖ salt) — stable across rotations

      Why derive (not use X3DH secret directly)?

      • The X3DH secret is SHARED history; deriving a channel-local key allows multiple channels from one X3DH (different salts) and isolates channels from each other.

      Key representation:

      • rootKeyMaterial exists as raw Uint8Array in ChannelInternals (accessible only via the module-private WeakMap).
      • rootKey is a non-extractable AES-GCM CryptoKey derived from rootKeyMaterial for AEAD operations.
      • Both representations coexist during the channel lifetime; rootKeyMaterial is only wiped on rotateChannel.

      Parameters

      • sharedSecret: Uint8Array

        32B from initiateX3DH/completeX3DH.

      • channelSalt: string

        Application-provided salt (e.g. chatId). MUST be identical on both sides. Does not need to be secret.

      • role: "initiator" | "recipient" = 'initiator'

        Direction role: 'initiator' or 'recipient'. Defaults to 'initiator'. Affects nothing cryptographically (X3DH is symmetric), but useful for application-side audit.

      Returns Promise<Channel>

      A Channel with epoch=0 and hidden internals. The object is brand-validated (D1) and frozen (D2) — all fields readonly.

      If sharedSecret length is not 32 bytes.

      const x3dh = await initiateX3DH(myIdentity, theirBundle);
      const channel = await establishChannel(x3dh.sharedSecret, 'chat-42');