@me2em/crypto
    Preparing search index...

    Function initiateX3DH

    • INITIATOR side of X3DH.

      Establishes a shared secret with an OFFLINE recipient using their published PreKeyBundle. The recipient completes the same computation later using their private pre-keys.

      CANONICAL DH ORDER (Signal spec §3.3) — DO NOT REORDER

      DH1 = DH(IK_A, SPK_B) // initiator identity × recipient signed prekey DH2 = DH(EK_A, IK_B) // ephemeral × recipient identity DH3 = DH(EK_A, SPK_B) // ephemeral × recipient signed prekey DH4 = DH(EK_A, OPK_B) // ephemeral × recipient one-time prekey

      All keys are converted Ed25519→X25519 (Montgomery) where needed. DH4 uses the recipient's OPK public key with the initiator's EPHEMERAL private key — zero-fallback is not allowed (enforces forward secrecy).

      SK = HKDF(DH1 || DH2 || DH3 || DH4, salt=zero, info=X3DH_INFO)

      Parameters

      • ourIdentity: IdentityKeys

        Initiator's Ed25519 identity key pair.

      • theirBundle: PreKeyBundle

        Recipient's PreKeyBundle (MUST be verified via verifySignedPreKey BEFORE calling this function).

      Returns Promise<X3DHInitiationResult>

      X3DHInitiationResult with shared secret and ephemeral pubkey. The shared secret is the CALLER's responsibility — wipe it after use (see wrapKeyForRecipient for the pattern).

      'VERIFICATION_FAILED' if SPK signature invalid, 'INVALID_ARGUMENT' if bundle lacks OTK.

      Memory hygiene: every DH intermediate, the IKM, the derived Montgomery copy of the identity key and the ephemeral private key are wiped on ALL exit paths (success or throw) — a failed DH against a hostile bundle must not leak live key bytes.

      const bundle = await fetchPreKeyBundle(bobId);
      verifySignedPreKey(bundle, bobIdentityPub);
      const result = await initiateX3DH(aliceIdentity, bundle);
      // send result.ephemeralPublicKey to Bob in first message