Something went wrong. Try again.
An AT Protocol Personal Data Server written in JavaScript pdsjs.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153// @pdsjs/spaces/commit - permissioned repo commits with deniable signatures.
import { LtHash } from './lthash.js';import { bytesEqual, COMMIT_VERSION, computeMac, encodeCommitCtx,} from './mac.js';import { formatSetHashElement } from './path.js';
/** * @typedef {Object} SignedCommit * @property {number} ver * @property {Uint8Array} hash * @property {Uint8Array} ikm * @property {Uint8Array} sig * @property {Uint8Array} mac * @property {string} rev */
export class RepoCommit { /** @param {LtHash} [setHash] */ constructor(setHash = new LtHash()) { this.setHash = setHash; }
/** @param {Uint8Array|null|undefined} state */ static fromState(state) { return new RepoCommit(new LtHash(state ?? null)); }
/** @param {Iterable<{collection: string, rkey: string, cid: string}>} records */ static fromRecords(records) { const commit = new RepoCommit(); for (const { collection, rkey, cid } of records) { commit.add(collection, rkey, cid); } return commit; }
/** * Fold in every record an index describes, to compare against a commit. * @param {Record<string, string>} index - "{collection}/{rkey}" to CID */ static fromIndex(index) { const commit = new RepoCommit(); for (const [path, cid] of Object.entries(index)) { commit.setHash.add(`${path}/${cid}`); } return commit; }
/** * @param {string} collection * @param {string} rkey * @param {string} cid * @returns {this} */ add(collection, rkey, cid) { this.setHash.add(formatSetHashElement(collection, rkey, cid)); return this; }
/** * @param {string} collection * @param {string} rkey * @param {string} cid * @returns {this} */ remove(collection, rkey, cid) { this.setHash.remove(formatSetHashElement(collection, rkey, cid)); return this; }
/** @param {{collection: string, rkey: string, cid: string|null, prev: string|null}} op */ applyOp(op) { if (op.prev) this.remove(op.collection, op.rkey, op.prev); if (op.cid) this.add(op.collection, op.rkey, op.cid); return this; }
/** * @param {Iterable<{collection: string, rkey: string, cid: string|null, prev: string|null}>} ops * @returns {this} */ applyOps(ops) { for (const op of ops) this.applyOp(op); return this; }
state() { return this.setHash.state(); }
/** * Whether this repo's contents match a signed commit. Verify the commit first — * on its own this says nothing about authenticity. * @param {SignedCommit} commit */ async matches(commit) { return bytesEqual(await this.setHash.digest(), commit.hash); }
/** * Sign a commit over the current contents. * * The signature covers only the ctx, never the digest, so a leaked commit * proves nothing about what the author wrote. The digest is bound to the ctx * by a symmetric MAC instead: readers get integrity, third parties get * nothing. A fresh ikm per commit means each reader receives a distinct one. * * @param {{space: string, author: string, rev: string}} ctx * @param {{sign: (bytes: Uint8Array) => Promise<Uint8Array>}} signer * @returns {Promise<SignedCommit>} */ async sign(ctx, signer) { const hash = await this.setHash.digest(); const ikm = crypto.getRandomValues(new Uint8Array(32)); const ctxBytes = encodeCommitCtx(ctx, ikm); return { ver: COMMIT_VERSION, hash, ikm, mac: await computeMac(ikm, ctxBytes, hash), sig: await signer.sign(ctxBytes), rev: ctx.rev, }; }}
/** * Verify a commit's signature (authenticity) and MAC (integrity). Once this * passes, `hash` is trusted as the author's claim about their repo, which is * what makes RepoCommit#matches meaningful. * * @param {SignedCommit} commit * @param {{space: string, author: string, rev: string}} ctx * @param {string} didKey * @param {import('@pdsjs/core/ports').SignatureVerifierPort} verifier * @returns {Promise<boolean>} */export async function verifyCommit(commit, ctx, didKey, verifier) { if (commit.ver !== COMMIT_VERSION) return false; if (commit.rev !== ctx.rev) return false;
const ctxBytes = encodeCommitCtx(ctx, commit.ikm); const mac = await computeMac(commit.ikm, ctxBytes, commit.hash); if (!bytesEqual(mac, commit.mac)) return false;
return verifier.verify(didKey, ctxBytes, commit.sig);}