Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

mls-lazy — mls-lazy.js

The shared-secret (N-party) confidentiality plugin: a binary ratchet tree keyed to the sorted member set, giving O(log N) epoch-key distribution per commit, plus a per-sender HKDF message ratchet inside each epoch. Suite: mls-chacha-v1 (ChaCha20-Poly1305, 12-byte nonce, RFC 9420). Used by group. Normative spec: mls-lazy.

import {
  prepareCommit, consumeCommit, wireEnvelope, parseWireEnvelope,
  encryptMessage, decryptMessage, deriveSenderMessageKey,
} from '@enc-protocol/core/mls-lazy.js'

Epoch key agreement

prepareCommit(opts)

prepareCommit({ sortedMembers, committerPubHex, prevEpochN?, prevTreeState?, newMembers? })
  → { newEpochSecret: Uint8Array, newTreeState, envelope }

Run by the committer. Generates a fresh epoch root and wraps it to each member along the tree copath (reusing unchanged subtree keys from prevTreeState when present). envelope is the commit to broadcast; newTreeState is cached for the next commit.

consumeCommit(opts)

consumeCommit({ sortedMembers, myPubHex, identityPriv, prevTreeState?, envelope,
                prevHighestN?, expectedCommitter? })
  → { newEpochSecret: Uint8Array, newTreeState }

Run by each member. Unwraps the one path entry decryptable with the member's identity key and recovers the new epoch secret. Validates that envelope.n is strictly increasing and (optionally) that the committer matches.

wireEnvelope(result) / parseWireEnvelope(content)

Serialize a prepareCommit result to the wire { epoch: {...} } shape, and parse it back.

Per-epoch messaging

deriveSenderMessageKey(epochSecret, senderPubHex, senderSeq)

deriveSenderMessageKey(epochSecret: Uint8Array, senderPubHex: string, senderSeq: number)
Uint8Array(32)

The per-sender HKDF ratchet inside an epoch — a fresh key per (sender, seq).

encryptMessage(opts) / decryptMessage(opts)

encryptMessage({ epochSecret, epochN, senderPubHex, senderSeq, plaintext })
  → { epoch_n, sender_pub, sender_seq, ciphertext, nonce }
decryptMessage({ epochSecret, envelope }) → string | Uint8Array

The standalone per-sender ratchet is also exposed as @enc-protocol/core/group-ratchet.js (groupRatchetMessageKey / groupEncrypt / groupDecrypt).

Tree helpers

Pure tree math is exported for tooling: paddedLeafCount, totalNodeCount, treeDepth, leafNodeId, nodeIdToLeafIdx, parentNode, siblingNode, directPath, copath, subtreeLeafIndices, lca, sortMembers, memberLeafIdx.

Example

import {
  sortMembers, prepareCommit, consumeCommit, wireEnvelope, parseWireEnvelope,
  encryptMessage, decryptMessage,
} from '@enc-protocol/core/mls-lazy.js'
 
const members = sortMembers([alicePubHex, bobPubHex, carolPubHex])
 
// alice commits a fresh epoch and broadcasts the envelope
const { newEpochSecret, envelope } =
  prepareCommit({ sortedMembers: members, committerPubHex: alicePubHex })
const wire = wireEnvelope({ envelope })
 
// bob consumes it and recovers the same epoch secret
const bob = consumeCommit({
  sortedMembers: members, myPubHex: bobPubHex, identityPriv: bobPriv,
  envelope: parseWireEnvelope(wire),
})
 
// alice sends a message in the epoch; bob decrypts it
const m = encryptMessage({
  epochSecret: newEpochSecret, epochN: envelope.n,
  senderPubHex: alicePubHex, senderSeq: 0, plaintext: 'gm all',
})
console.log(decryptMessage({ epochSecret: bob.newEpochSecret, envelope: m })) // 'gm all'

See also