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

260 lines 10.9 kB
import { contramapEncoder, contramapEncoders } from "./codec/tlsEncoder.js"; import { flatMapDecoder, mapDecoder } from "./codec/tlsDecoder.js"; import { decodeVarLenType, encodeVarLenType } from "./codec/variableLength.js"; import { decodeNodeType, encodeNodeType } from "./nodeType.js"; import { decodeOptional, encodeOptional } from "./codec/optional.js"; import { encodeParentNode, decodeParentNode } from "./parentNode.js"; import { copath, directPath, isLeaf, leafToNodeIndex, leafWidth, left, nodeToLeafIndex, parent, right, root, toLeafIndex, toNodeIndex, } from "./treemath.js"; import { encodeLeafNode, decodeLeafNode } from "./leafNode.js"; import { constantTimeEqual } from "./util/constantTimeCompare.js"; import { InternalError, ValidationError } from "./mlsError.js"; export const encodeNode = (node) => { switch (node.nodeType) { case "parent": return contramapEncoders([encodeNodeType, encodeParentNode], (n) => [n.nodeType, n.parent])(node); case "leaf": return contramapEncoders([encodeNodeType, encodeLeafNode], (n) => [n.nodeType, n.leaf])(node); } }; export const decodeNode = flatMapDecoder(decodeNodeType, (nodeType) => { switch (nodeType) { case "parent": return mapDecoder(decodeParentNode, (parent) => ({ nodeType, parent, })); case "leaf": return mapDecoder(decodeLeafNode, (leaf) => ({ nodeType, leaf, })); } }); export function getHpkePublicKey(n) { switch (n.nodeType) { case "parent": return n.parent.hpkePublicKey; case "leaf": return n.leaf.hpkePublicKey; } } export function extendRatchetTree(tree) { const lastIndex = tree.length - 1; if (tree[lastIndex] === undefined) { throw new InternalError("The last node in the ratchet tree must be non-blank."); } // Compute the smallest full binary tree size >= current length const neededSize = nextFullBinaryTreeSize(tree.length); // Fill with `undefined` until tree has the needed size const copy = tree.slice(); while (copy.length < neededSize) { copy.push(undefined); } return copy; } // Compute the smallest 2^(d + 1) - 1 >= n function nextFullBinaryTreeSize(n) { let d = 0; while ((1 << (d + 1)) - 1 < n) { d++; } return (1 << (d + 1)) - 1; } /** * If the tree has 2d leaves, then it has 2d+1 - 1 nodes. * The ratchet_tree vector logically has this number of entries, but the sender MUST NOT include blank nodes after the last non-blank node. * The receiver MUST check that the last node in ratchet_tree is non-blank, and then extend the tree to the right until it has a length of the form 2d+1 - 1, adding the minimum number of blank values possible. * (Obviously, this may be done "virtually", by synthesizing blank nodes when required, as opposed to actually changing the structure in memory.) */ export function stripBlankNodes(tree) { let lastNonBlank = tree.length - 1; while (lastNonBlank >= 0 && tree[lastNonBlank] === undefined) { lastNonBlank--; } return tree.slice(0, lastNonBlank + 1); } export const encodeRatchetTree = contramapEncoder(encodeVarLenType(encodeOptional(encodeNode)), stripBlankNodes); export const decodeRatchetTree = mapDecoder(decodeVarLenType(decodeOptional(decodeNode)), extendRatchetTree); export function findBlankLeafNodeIndex(tree) { const nodeIndex = tree.findIndex((node, nodeIndex) => node === undefined && isLeaf(toNodeIndex(nodeIndex))); if (nodeIndex < 0) return undefined; else return toNodeIndex(nodeIndex); } export function findBlankLeafNodeIndexOrExtend(tree) { const blankLeaf = findBlankLeafNodeIndex(tree); return blankLeaf === undefined ? toNodeIndex(tree.length + 1) : blankLeaf; } export function extendTree(tree, leafNode) { const newRoot = undefined; const insertedNodeIndex = toNodeIndex(tree.length + 1); const newTree = [ ...tree, newRoot, { nodeType: "leaf", leaf: leafNode }, ...new Array(tree.length - 1), ]; return [newTree, insertedNodeIndex]; } export function addLeafNode(tree, leafNode) { const blankLeaf = findBlankLeafNodeIndex(tree); if (blankLeaf === undefined) { return extendTree(tree, leafNode); } const insertedLeafIndex = nodeToLeafIndex(blankLeaf); const dp = directPath(blankLeaf, leafWidth(tree.length)); const copy = tree.slice(); for (const nodeIndex of dp) { const node = tree[nodeIndex]; if (node !== undefined) { const parentNode = node; const updated = { nodeType: "parent", parent: { ...parentNode.parent, unmergedLeaves: [...parentNode.parent.unmergedLeaves, insertedLeafIndex] }, }; copy[nodeIndex] = updated; } } copy[blankLeaf] = { nodeType: "leaf", leaf: leafNode }; return [copy, blankLeaf]; } export function updateLeafNode(tree, leafNode, leafIndex) { const leafNodeIndex = leafToNodeIndex(leafIndex); const pathToBlank = directPath(leafNodeIndex, leafWidth(tree.length)); const copy = tree.slice(); for (const nodeIndex of pathToBlank) { const node = tree[nodeIndex]; if (node !== undefined) { copy[nodeIndex] = undefined; } } copy[leafNodeIndex] = { nodeType: "leaf", leaf: leafNode }; return copy; } export function removeLeafNode(tree, removedLeafIndex) { const leafNodeIndex = leafToNodeIndex(removedLeafIndex); const pathToBlank = directPath(leafNodeIndex, leafWidth(tree.length)); const copy = tree.slice(); for (const nodeIndex of pathToBlank) { const node = tree[nodeIndex]; if (node !== undefined) { copy[nodeIndex] = undefined; } } copy[leafNodeIndex] = undefined; return condenseRatchetTreeAfterRemove(copy); } /** * When the right subtree of the tree no longer has any non-blank nodes, it can be safely removed */ function condenseRatchetTreeAfterRemove(tree) { return extendRatchetTree(stripBlankNodes(tree)); } export function resolution(tree, nodeIndex) { const node = tree[nodeIndex]; if (node === undefined) { if (isLeaf(nodeIndex)) { return []; } const l = left(nodeIndex); const r = right(nodeIndex); const leftRes = resolution(tree, l); const rightRes = resolution(tree, r); return [...leftRes, ...rightRes]; } if (isLeaf(nodeIndex)) { return [nodeIndex]; } const unmerged = node.nodeType === "parent" ? node.parent.unmergedLeaves : []; return [nodeIndex, ...unmerged.map((u) => leafToNodeIndex(toLeafIndex(u)))]; } export function filteredDirectPath(leafIndex, tree) { const leafNodeIndex = leafToNodeIndex(leafIndex); const leafWidth = nodeToLeafIndex(toNodeIndex(tree.length)); const cp = copath(leafNodeIndex, leafWidth); // the filtered direct path of a leaf node L is the node's direct path, // with any node removed whose child on the copath of L has an empty resolution return directPath(leafNodeIndex, leafWidth).filter((_nodeIndex, n) => resolution(tree, cp[n]).length !== 0); } export function filteredDirectPathAndCopathResolution(leafIndex, tree) { const leafNodeIndex = leafToNodeIndex(leafIndex); const lWidth = leafWidth(tree.length); const cp = copath(leafNodeIndex, lWidth); // the filtered direct path of a leaf node L is the node's direct path, // with any node removed whose child on the copath of L has an empty resolution return directPath(leafNodeIndex, lWidth).reduce((acc, cur, n) => { const r = resolution(tree, cp[n]); if (r.length === 0) return acc; else return [...acc, { nodeIndex: cur, resolution: r }]; }, []); } export function removeLeaves(tree, leafIndices) { const copy = tree.slice(); function shouldBeRemoved(leafIndex) { return leafIndices.find((x) => leafIndex === x) !== undefined; } for (const [i, n] of tree.entries()) { if (n !== undefined) { const nodeIndex = toNodeIndex(i); if (isLeaf(nodeIndex) && shouldBeRemoved(nodeToLeafIndex(nodeIndex))) { copy[i] = undefined; } else if (n.nodeType === "parent") { copy[i] = { ...n, parent: { ...n.parent, unmergedLeaves: n.parent.unmergedLeaves.filter((l) => !shouldBeRemoved(l)) }, }; } } } return condenseRatchetTreeAfterRemove(copy); } export function traverseToRoot(tree, leafIndex, f) { const rootIndex = root(leafWidth(tree.length)); let currentIndex = leafToNodeIndex(leafIndex); while (currentIndex != rootIndex) { currentIndex = parent(currentIndex, leafWidth(tree.length)); const currentNode = tree[currentIndex]; if (currentNode !== undefined) { if (currentNode.nodeType === "leaf") { throw new InternalError("Expected parent node"); } const result = f(currentIndex, currentNode.parent); if (result !== undefined) { return [result, currentIndex]; } } } } export function findFirstNonBlankAncestor(tree, nodeIndex) { return (traverseToRoot(tree, nodeToLeafIndex(nodeIndex), (nodeIndex, _node) => nodeIndex)?.[0] ?? root(leafWidth(tree.length))); } export function findLeafIndex(tree, leaf) { const foundIndex = tree.findIndex((node, nodeIndex) => { if (isLeaf(toNodeIndex(nodeIndex)) && node !== undefined) { if (node.nodeType === "parent") throw new InternalError("Found parent node in leaf node position"); //todo is there a better (faster) comparison method? return constantTimeEqual(encodeLeafNode(node.leaf), encodeLeafNode(leaf)); } return false; }); return foundIndex === -1 ? undefined : nodeToLeafIndex(toNodeIndex(foundIndex)); } export function getCredentialFromLeafIndex(ratchetTree, leafIndex) { const senderLeafNode = ratchetTree[leafToNodeIndex(leafIndex)]; if (senderLeafNode === undefined || senderLeafNode.nodeType === "parent") throw new ValidationError("Unable to find leafnode for leafIndex"); return senderLeafNode.leaf.credential; } export function getSignaturePublicKeyFromLeafIndex(ratchetTree, leafIndex) { const leafNode = ratchetTree[leafToNodeIndex(leafIndex)]; if (leafNode === undefined || leafNode.nodeType === "parent") throw new ValidationError("Unable to find leafnode for leafIndex"); return leafNode.leaf.signaturePublicKey; } //# sourceMappingURL=ratchetTree.js.map