UNPKG

ts-mls

Version:

[![CI](https://github.com/LukaJCB/ts-mls/actions/workflows/ci.yml/badge.svg)](https://github.com/LukaJCB/ts-mls/actions/workflows/ci.yml) [![npm version](https://badge.fury.io/js/ts-mls.svg)](https://badge.fury.io/js/ts-mls) [![Coverage Status](https://co

137 lines 7.13 kB
import { expandWithLabel, deriveTreeSecret } from "./crypto/kdf.js"; import { InternalError, ValidationError } from "./mlsError.js"; import { nodeWidth, root, right, isLeaf, left, leafToNodeIndex, toLeafIndex } from "./treemath.js"; import { updateArray } from "./util/array.js"; import { repeatAsync } from "./util/repeat.js"; function scaffoldSecretTree(leafWidth, encryptionSecret, kdf) { const tree = new Array(nodeWidth(leafWidth)); const rootIndex = root(leafWidth); const parentInhabited = updateArray(tree, rootIndex, encryptionSecret); return deriveChildren(parentInhabited, rootIndex, kdf); } export async function createSecretTree(leafWidth, encryptionSecret, kdf) { const tree = await scaffoldSecretTree(leafWidth, encryptionSecret, kdf); return await Promise.all(tree.map(async (secret) => { const application = await createRatchetRoot(secret, "application", kdf); const handshake = await createRatchetRoot(secret, "handshake", kdf); return { handshake, application }; })); } async function deriveChildren(tree, nodeIndex, kdf) { if (isLeaf(nodeIndex)) return tree; const l = left(nodeIndex); const r = right(nodeIndex); const parentSecret = tree[nodeIndex]; if (parentSecret === undefined) throw new InternalError("Bad node index for secret tree"); const leftSecret = await expandWithLabel(parentSecret, "tree", new TextEncoder().encode("left"), kdf.size, kdf); const rightSecret = await expandWithLabel(parentSecret, "tree", new TextEncoder().encode("right"), kdf.size, kdf); const currentTree = updateArray(updateArray(tree, l, leftSecret), r, rightSecret); return deriveChildren(await deriveChildren(currentTree, l, kdf), r, kdf); } export async function deriveNonce(secret, generation, cs) { return await deriveTreeSecret(secret, "nonce", generation, cs.hpke.nonceLength, cs.kdf); } export async function deriveKey(secret, generation, cs) { return await deriveTreeSecret(secret, "key", generation, cs.hpke.keyLength, cs.kdf); } export async function ratchetUntil(current, desiredGen, config, kdf) { const generationDifference = desiredGen - current.generation; if (generationDifference > config.maximumForwardRatchetSteps) throw new ValidationError("Desired generation too far in the future"); return await repeatAsync(async (s) => { const nextSecret = await deriveTreeSecret(s.secret, "secret", s.generation, kdf.size, kdf); return { secret: nextSecret, generation: s.generation + 1, unusedGenerations: updateUnusedGenerations(s, config.retainKeysForGenerations), }; }, current, generationDifference); } function updateUnusedGenerations(s, retainGenerationsMax) { const withNew = { ...s.unusedGenerations, [s.generation]: s.secret }; const generations = Object.keys(withNew); const result = generations.length >= retainGenerationsMax ? removeOldGenerations(withNew, retainGenerationsMax) : withNew; return result; } function removeOldGenerations(historicalReceiverData, max) { const sortedGenerations = Object.keys(historicalReceiverData) .map(Number) .sort((a, b) => (a < b ? -1 : 1)); return Object.fromEntries(sortedGenerations.slice(-max).map((generation) => [generation, historicalReceiverData[generation]])); } export async function derivePrivateMessageNonce(secret, generation, reuseGuard, cs) { const nonce = await deriveNonce(secret, generation, cs); if (nonce.length >= 4 && reuseGuard.length >= 4) { for (let i = 0; i < 4; i++) { nonce[i] ^= reuseGuard[i]; } } else throw new ValidationError("Reuse guard or nonce incorrect length"); return nonce; } export async function ratchetToGeneration(tree, senderData, contentType, config, cs) { const index = leafToNodeIndex(toLeafIndex(senderData.leafIndex)); const node = tree[index]; if (node === undefined) throw new InternalError("Bad node index for secret tree"); const ratchet = ratchetForContentType(node, contentType); if (ratchet.generation > senderData.generation) { const desired = ratchet.unusedGenerations[senderData.generation]; if (desired !== undefined) { const { [senderData.generation]: _, ...removedDesiredGen } = ratchet.unusedGenerations; const ratchetState = { ...ratchet, unusedGenerations: removedDesiredGen }; return await createRatchetResultWithSecret(node, index, desired, senderData.generation, senderData.reuseGuard, tree, contentType, cs, ratchetState); } throw new ValidationError("Desired gen in the past"); } const currentSecret = await ratchetUntil(ratchetForContentType(node, contentType), senderData.generation, config, cs.kdf); return createRatchetResult(node, index, currentSecret, senderData.reuseGuard, tree, contentType, cs); } export async function consumeRatchet(tree, index, contentType, cs) { const node = tree[index]; if (node === undefined) throw new InternalError("Bad node index for secret tree"); const currentSecret = ratchetForContentType(node, contentType); const reuseGuard = cs.rng.randomBytes(4); return createRatchetResult(node, index, currentSecret, reuseGuard, tree, contentType, cs); } async function createRatchetResult(node, index, currentSecret, reuseGuard, tree, contentType, cs) { const nextSecret = await deriveTreeSecret(currentSecret.secret, "secret", currentSecret.generation, cs.kdf.size, cs.kdf); const ratchetState = { ...currentSecret, secret: nextSecret, generation: currentSecret.generation + 1 }; return await createRatchetResultWithSecret(node, index, currentSecret.secret, currentSecret.generation, reuseGuard, tree, contentType, cs, ratchetState); } async function createRatchetResultWithSecret(node, index, secret, generation, reuseGuard, tree, contentType, cs, ratchetState) { const { nonce, key } = await createKeyAndNonce(secret, generation, reuseGuard, cs); const newNode = contentType === "application" ? { ...node, application: ratchetState } : { ...node, handshake: ratchetState }; const newTree = updateArray(tree, index, newNode); return { generation: generation, reuseGuard, nonce, key, newTree, }; } async function createKeyAndNonce(secret, generation, reuseGuard, cs) { const key = await deriveKey(secret, generation, cs); const nonce = await derivePrivateMessageNonce(secret, generation, reuseGuard, cs); return { nonce, key }; } function ratchetForContentType(node, contentType) { switch (contentType) { case "application": return node.application; case "proposal": return node.handshake; case "commit": return node.handshake; } } async function createRatchetRoot(node, label, kdf) { const secret = await expandWithLabel(node, label, new Uint8Array(), kdf.size, kdf); return { secret: secret, generation: 0, unusedGenerations: {} }; } //# sourceMappingURL=secretTree.js.map