Recipient identity (private needed).
Recipient's SPK (private needed).
The consumed OTK (private needed). Required.
Initiator identity public (32B Ed25519).
From the first message (32B X25519).
The same 32-byte shared secret as the initiator computed.
The shared secret is the CALLER's responsibility — wipe it after
use (see unwrapKeyForMe for the pattern).
RECIPIENT side of X3DH: completes the same computation.
DH1 = DH(SPK_B_priv, IK_A_pub) // recipient SPK × initiator identity DH2 = DH(IK_B_priv, EK_A_pub) // recipient identity × initiator ephemeral DH3 = DH(SPK_B_priv, EK_A_pub) // recipient SPK × initiator ephemeral DH4 = DH(OTK_B_priv, EK_A_pub) // recipient OTK × initiator ephemeral
→ HKDF of the same 4 secrets in the same order → same 32B.
ourOneTimePreKey is REQUIRED — the initiator MUST consume an OTK (zero-fallback prohibited on both sides). Passing undefined would produce a different shared secret, breaking the protocol.
⚠️ OTK CONSUMED: this function wipes
ourOneTimePreKey.keyPair.privateKeyIN PLACE after the DH that uses it (session-bound ephemeral-key model). Reusing the wiped buffer derives a corrupted secret SILENTLY — no error is thrown. Pass a copy if retention is needed. The identity and SPK private keys are NOT touched (they are reused across sessions).