ts-mls
Version:
[](https://github.com/LukaJCB/ts-mls/actions/workflows/ci.yml) [](https://badge.fury.io/js/ts-mls) [ {
const { state, pskIndex = makePskIndex(state, {}), cipherSuite } = context;
const { wireAsPublicMessage = false, extraProposals = [], ratchetTreeExtension = false, authenticatedData = new Uint8Array(), groupInfoExtensions = [], } = options ?? {};
checkCanSendHandshakeMessages(state);
const wireformat = wireAsPublicMessage ? "mls_public_message" : "mls_private_message";
const allProposals = bundleAllProposals(state, extraProposals);
const res = await applyProposals(state, allProposals, toLeafIndex(state.privatePath.leafIndex), pskIndex, true, cipherSuite);
if (res.additionalResult.kind === "externalCommit")
throw new UsageError("Cannot create externalCommit as a member");
const suspendedPendingReinit = res.additionalResult.kind === "reinit" ? res.additionalResult.reinit : undefined;
const [tree, updatePath, pathSecrets, newPrivateKey] = res.needsUpdatePath
? await createUpdatePath(res.tree, toLeafIndex(state.privatePath.leafIndex), state.groupContext, state.signaturePrivateKey, cipherSuite)
: [res.tree, undefined, [], undefined];
const updatedExtensions = res.additionalResult.kind === "memberCommit" && res.additionalResult.extensions.length > 0
? res.additionalResult.extensions
: state.groupContext.extensions;
const groupContextWithExtensions = { ...state.groupContext, extensions: updatedExtensions };
const privateKeys = mergePrivateKeyPaths(newPrivateKey !== undefined
? updateLeafKey(state.privatePath, await cipherSuite.hpke.exportPrivateKey(newPrivateKey))
: state.privatePath, await toPrivateKeyPath(pathToPathSecrets(pathSecrets), state.privatePath.leafIndex, cipherSuite));
const lastPathSecret = pathSecrets.at(-1);
const commitSecret = lastPathSecret === undefined
? new Uint8Array(cipherSuite.kdf.size)
: await deriveSecret(lastPathSecret.secret, "path", cipherSuite.kdf);
const { signature, framedContent } = await createContentCommitSignature(state.groupContext, wireformat, { proposals: allProposals, path: updatePath }, { senderType: "member", leafIndex: state.privatePath.leafIndex }, authenticatedData, state.signaturePrivateKey, cipherSuite.signature);
const treeHash = await treeHashRoot(tree, cipherSuite.hash);
const updatedGroupContext = await nextEpochContext(groupContextWithExtensions, wireformat, framedContent, signature, treeHash, state.confirmationTag, cipherSuite.hash);
const epochSecrets = await initializeEpoch(state.keySchedule.initSecret, commitSecret, updatedGroupContext, res.pskSecret, cipherSuite.kdf);
const confirmationTag = await createConfirmationTag(epochSecrets.keySchedule.confirmationKey, updatedGroupContext.confirmedTranscriptHash, cipherSuite.hash);
const authData = {
contentType: framedContent.contentType,
signature,
confirmationTag,
};
const [commit] = await protectCommit(wireAsPublicMessage, state, authenticatedData, framedContent, authData, cipherSuite);
const welcome = await createWelcome(ratchetTreeExtension, updatedGroupContext, confirmationTag, state, tree, cipherSuite, epochSecrets, res, pathSecrets, groupInfoExtensions);
const groupActiveState = res.selfRemoved
? { kind: "removedFromGroup" }
: suspendedPendingReinit !== undefined
? { kind: "suspendedPendingReinit", reinit: suspendedPendingReinit }
: { kind: "active" };
const newState = {
groupContext: updatedGroupContext,
ratchetTree: tree,
secretTree: await createSecretTree(leafWidth(tree.length), epochSecrets.keySchedule.encryptionSecret, cipherSuite.kdf),
keySchedule: epochSecrets.keySchedule,
privatePath: privateKeys,
unappliedProposals: {},
historicalReceiverData: addHistoricalReceiverData(state),
confirmationTag,
signaturePrivateKey: state.signaturePrivateKey,
groupActiveState,
clientConfig: state.clientConfig,
};
return { newState, welcome, commit };
}
function bundleAllProposals(state, extraProposals) {
const refs = Object.keys(state.unappliedProposals).map((p) => ({
proposalOrRefType: "reference",
reference: base64ToBytes(p),
}));
const proposals = extraProposals.map((p) => ({ proposalOrRefType: "proposal", proposal: p }));
return [...refs, ...proposals];
}
async function createWelcome(ratchetTreeExtension, groupContext, confirmationTag, state, tree, cs, epochSecrets, res, pathSecrets, extensions) {
const groupInfo = ratchetTreeExtension
? await createGroupInfoWithRatchetTree(groupContext, confirmationTag, state, tree, extensions, cs)
: await createGroupInfo(groupContext, confirmationTag, state, extensions, cs);
const encryptedGroupInfo = await encryptGroupInfo(groupInfo, epochSecrets.welcomeSecret, cs);
const encryptedGroupSecrets = res.additionalResult.kind === "memberCommit"
? await Promise.all(res.additionalResult.addedLeafNodes.map(([leafNodeIndex, keyPackage]) => {
return createEncryptedGroupSecrets(tree, leafNodeIndex, state, pathSecrets, cs, keyPackage, encryptedGroupInfo, epochSecrets, res);
}))
: [];
return encryptedGroupSecrets.length > 0
? {
cipherSuite: groupContext.cipherSuite,
secrets: encryptedGroupSecrets,
encryptedGroupInfo,
}
: undefined;
}
async function createEncryptedGroupSecrets(tree, leafNodeIndex, state, pathSecrets, cs, keyPackage, encryptedGroupInfo, epochSecrets, res) {
const nodeIndex = firstCommonAncestor(tree, leafNodeIndex, toLeafIndex(state.privatePath.leafIndex));
const pathSecret = pathSecrets.find((ps) => ps.nodeIndex === nodeIndex);
const pk = await cs.hpke.importPublicKey(keyPackage.initKey);
const egs = await encryptGroupSecrets(pk, encryptedGroupInfo, { joinerSecret: epochSecrets.joinerSecret, pathSecret: pathSecret?.secret, psks: res.pskIds }, cs.hpke);
const ref = await makeKeyPackageRef(keyPackage, cs.hash);
return { newMember: ref, encryptedGroupSecrets: { kemOutput: egs.enc, ciphertext: egs.ct } };
}
export async function createGroupInfo(groupContext, confirmationTag, state, extensions, cs) {
const groupInfoTbs = {
groupContext: groupContext,
extensions: extensions,
confirmationTag,
signer: state.privatePath.leafIndex,
};
return signGroupInfo(groupInfoTbs, state.signaturePrivateKey, cs.signature);
}
export async function createGroupInfoWithRatchetTree(groupContext, confirmationTag, state, tree, extensions, cs) {
const encodedTree = encodeRatchetTree(tree);
const gi = await createGroupInfo(groupContext, confirmationTag, state, [...extensions, { extensionType: "ratchet_tree", extensionData: encodedTree }], cs);
return gi;
}
export async function createGroupInfoWithExternalPub(state, extensions, cs) {
const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret);
const externalPub = await cs.hpke.exportPublicKey(externalKeyPair.publicKey);
const gi = await createGroupInfo(state.groupContext, state.confirmationTag, state, [...extensions, { extensionType: "external_pub", extensionData: externalPub }], cs);
return gi;
}
export async function createGroupInfoWithExternalPubAndRatchetTree(state, extensions, cs) {
const encodedTree = encodeRatchetTree(state.ratchetTree);
const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret);
const externalPub = await cs.hpke.exportPublicKey(externalKeyPair.publicKey);
const gi = await createGroupInfo(state.groupContext, state.confirmationTag, state, [
...extensions,
{ extensionType: "external_pub", extensionData: externalPub },
{ extensionType: "ratchet_tree", extensionData: encodedTree },
], cs);
return gi;
}
async function protectCommit(publicMessage, state, authenticatedData, content, authData, cs) {
const wireformat = publicMessage ? "mls_public_message" : "mls_private_message";
const authenticatedContent = {
wireformat,
content,
auth: authData,
};
if (publicMessage) {
const msg = await protectPublicMessage(state.keySchedule.membershipKey, state.groupContext, authenticatedContent, cs);
return [{ version: "mls10", wireformat: "mls_public_message", publicMessage: msg }, state.secretTree];
}
else {
const res = await protect(state.keySchedule.senderDataSecret, authenticatedData, state.groupContext, state.secretTree, { ...content, auth: authData }, state.privatePath.leafIndex, state.clientConfig.paddingConfig, cs);
return [{ version: "mls10", wireformat: "mls_private_message", privateMessage: res.privateMessage }, res.tree];
}
}
export async function applyUpdatePathSecret(tree, privatePath, senderLeafIndex, gc, path, excludeNodes, cs) {
const { nodeIndex: ancestorNodeIndex, resolution, updateNode, } = firstMatchAncestor(tree, toLeafIndex(privatePath.leafIndex), senderLeafIndex, path);
for (const [i, nodeIndex] of filterNewLeaves(resolution, excludeNodes).entries()) {
if (privatePath.privateKeys[nodeIndex] !== undefined) {
const key = await cs.hpke.importPrivateKey(privatePath.privateKeys[nodeIndex]);
const ct = updateNode.encryptedPathSecret[i];
const pathSecret = await decryptWithLabel(key, "UpdatePathNode", encodeGroupContext(gc), ct.kemOutput, ct.ciphertext, cs.hpke);
return { nodeIndex: ancestorNodeIndex, pathSecret };
}
}
throw new InternalError("No overlap between provided private keys and update path");
}
export async function joinGroupExternal(groupInfo, keyPackage, privateKeys, resync, cs, tree, clientConfig = defaultClientConfig, authenticatedData = new Uint8Array()) {
const externalPub = groupInfo.extensions.find((ex) => ex.extensionType === "external_pub");
if (externalPub === undefined)
throw new UsageError("Could not find external_pub extension");
const allExtensionsSupported = extensionsSupportedByCapabilities(groupInfo.groupContext.extensions, keyPackage.leafNode.capabilities);
if (!allExtensionsSupported)
throw new UsageError("client does not support every extension in the GroupContext");
const { enc, secret: initSecret } = await exportSecret(externalPub.extensionData, cs);
const ratchetTree = ratchetTreeFromExtension(groupInfo) ?? tree;
if (ratchetTree === undefined)
throw new UsageError("No RatchetTree passed and no ratchet_tree extension");
throwIfDefined(await validateRatchetTree(ratchetTree, groupInfo.groupContext, clientConfig.lifetimeConfig, clientConfig.authService, groupInfo.groupContext.treeHash, cs));
const signaturePublicKey = getSignaturePublicKeyFromLeafIndex(ratchetTree, toLeafIndex(groupInfo.signer));
const signerCredential = getCredentialFromLeafIndex(ratchetTree, toLeafIndex(groupInfo.signer));
const credentialVerified = await clientConfig.authService.validateCredential(signerCredential, signaturePublicKey);
if (!credentialVerified)
throw new ValidationError("Could not validate credential");
const groupInfoSignatureVerified = await verifyGroupInfoSignature(groupInfo, signaturePublicKey, cs.signature);
if (!groupInfoSignatureVerified)
throw new CryptoVerificationError("Could not verify groupInfo Signature");
const formerLeafIndex = resync
? nodeToLeafIndex(toNodeIndex(ratchetTree.findIndex((n) => {
if (n !== undefined && n.nodeType === "leaf") {
return clientConfig.keyPackageEqualityConfig.compareKeyPackageToLeafNode(keyPackage, n.leaf);
}
return false;
})))
: undefined;
const updatedTree = formerLeafIndex !== undefined ? removeLeafNode(ratchetTree, formerLeafIndex) : ratchetTree;
const [treeWithNewLeafNode, newLeafNodeIndex] = addLeafNode(updatedTree, keyPackage.leafNode);
const [newTree, updatePath, pathSecrets, newPrivateKey] = await createUpdatePath(treeWithNewLeafNode, nodeToLeafIndex(newLeafNodeIndex), groupInfo.groupContext, privateKeys.signaturePrivateKey, cs);
const privateKeyPath = updateLeafKey(await toPrivateKeyPath(pathToPathSecrets(pathSecrets), nodeToLeafIndex(newLeafNodeIndex), cs), await cs.hpke.exportPrivateKey(newPrivateKey));
const lastPathSecret = pathSecrets.at(-1);
const commitSecret = lastPathSecret === undefined
? new Uint8Array(cs.kdf.size)
: await deriveSecret(lastPathSecret.secret, "path", cs.kdf);
const externalInitProposal = {
proposalType: "external_init",
externalInit: { kemOutput: enc },
};
const proposals = formerLeafIndex !== undefined
? [{ proposalType: "remove", remove: { removed: formerLeafIndex } }, externalInitProposal]
: [externalInitProposal];
const pskSecret = new Uint8Array(cs.kdf.size);
const { signature, framedContent } = await createContentCommitSignature(groupInfo.groupContext, "mls_public_message", { proposals: proposals.map((p) => ({ proposalOrRefType: "proposal", proposal: p })), path: updatePath }, {
senderType: "new_member_commit",
}, authenticatedData, privateKeys.signaturePrivateKey, cs.signature);
const treeHash = await treeHashRoot(newTree, cs.hash);
const groupContext = await nextEpochContext(groupInfo.groupContext, "mls_public_message", framedContent, signature, treeHash, groupInfo.confirmationTag, cs.hash);
const epochSecrets = await initializeEpoch(initSecret, commitSecret, groupContext, pskSecret, cs.kdf);
const confirmationTag = await createConfirmationTag(epochSecrets.keySchedule.confirmationKey, groupContext.confirmedTranscriptHash, cs.hash);
const state = {
ratchetTree: newTree,
groupContext: groupContext,
secretTree: await createSecretTree(leafWidth(newTree.length), epochSecrets.keySchedule.encryptionSecret, cs.kdf),
privatePath: privateKeyPath,
confirmationTag,
historicalReceiverData: new Map(),
signaturePrivateKey: privateKeys.signaturePrivateKey,
keySchedule: epochSecrets.keySchedule,
unappliedProposals: {},
groupActiveState: { kind: "active" },
clientConfig,
};
const authenticatedContent = {
content: framedContent,
auth: { signature, confirmationTag, contentType: "commit" },
wireformat: "mls_public_message",
};
const msg = await protectPublicMessage(epochSecrets.keySchedule.membershipKey, groupContext, authenticatedContent, cs);
return { publicMessage: msg, newState: state };
}
export function filterNewLeaves(resolution, excludeNodes) {
const set = new Set(excludeNodes);
return resolution.filter((i) => !set.has(i));
}
//# sourceMappingURL=createCommit.js.map