diff --git a/.changes/sha256-noble.json b/.changes/sha256-noble.json new file mode 100644 index 00000000..007a38f5 --- /dev/null +++ b/.changes/sha256-noble.json @@ -0,0 +1 @@ +{ "type": "internal", "reason": "The synchronous SHA-256 behind workspace, revision and proposal digests is now @noble/hashes (MIT) instead of a hand-written implementation whose origin could not be established. Every digest is byte-identical, which a baseline recorded before the change pins; Web Crypto is still used first where the page has it. Nothing a person sees or stores changes." } diff --git a/package-lock.json b/package-lock.json index 8961c0eb..6d1b83ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/ibm-plex-sans-thai": "^5.3.0", + "@noble/hashes": "2.4.0", "@xyflow/react": "^12.11.5", "intl-messageformat": "11.2.14", "react": "^19.2.8", @@ -1732,6 +1733,18 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@oxc-project/types": { "version": "0.147.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", diff --git a/package.json b/package.json index c6ec6860..9729107f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/ibm-plex-sans-thai": "^5.3.0", + "@noble/hashes": "2.4.0", "@xyflow/react": "^12.11.5", "intl-messageformat": "11.2.14", "react": "^19.2.8", diff --git a/scripts/check-share-crypto.mjs b/scripts/check-share-crypto.mjs index 9a100e08..55b35909 100644 --- a/scripts/check-share-crypto.mjs +++ b/scripts/check-share-crypto.mjs @@ -31,7 +31,11 @@ // `extractable` argument, and `deriveKey` asks for exactly one usage; // - there is exactly one `encrypt` call and one `decrypt` call. // -// And `package.json` names no cryptography library: Web Crypto only. +// And `package.json` names no cryptography library: Web Crypto only. The one +// exception (issue #301) is `@noble/hashes` 2.4.0 for the synchronous SHA-256 of +// `src/model/workspace.ts`, held to the conditions in +// `scripts/share-crypto-noble.mjs`: one exact version, nothing it depends on, +// one importer, one path, one name, and nothing of it reachable from the module. // // What it CANNOT see: a name assembled at run time, code in a dependency, and // whether the values passed at run time are the ones the format fixes - the @@ -43,6 +47,7 @@ import fs from 'node:fs' import path from 'node:path' import ts from 'typescript' import { fileURLToPath } from 'node:url' +import { checkLock, checkNobleUse, checkPackage } from './share-crypto-noble.mjs' const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const SRC = path.join(ROOT, 'src') @@ -53,7 +58,6 @@ const ITERATIONS = 600000 const FORMAT_ID = 'loop-share-protected/1' const KEY_OPS = new Set(['deriveKey', 'deriveBits', 'importKey', 'exportKey', 'generateKey', 'wrapKey', 'unwrapKey', 'encrypt', 'decrypt']) -const CRYPTO_LIBRARY = /(^|[-_/@.])(crypto|cryptojs|argon2?|scrypt|bcrypt(js)?|sodium|libsodium|nacl|tweetnacl|forge|sjcl|noble|aes|pbkdf2?|hash-wasm|openpgp|jose|webcrypto)([-_/@.]|$)/i const CODE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/ const TEST = /\.test\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/ @@ -151,9 +155,25 @@ else { const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')) const deps = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].flatMap((k) => Object.keys(pkg[k] ?? {})) -for (const d of deps) if (CRYPTO_LIBRARY.test(d)) problems.push(`package.json: '${d}' looks like a cryptography library - protected links use Web Crypto only`) +// issue #301 - no cryptography library, with ONE exception: `@noble/hashes` +// 2.4.0 for the synchronous SHA-256 in src/model/workspace.ts, under the +// conditions in scripts/share-crypto-noble.mjs (and its tests). Every source +// the repository has is scanned for it: product code, unit tests, end-to-end +// specs and scripts. +const lock = JSON.parse(fs.readFileSync(path.join(ROOT, 'package-lock.json'), 'utf8')) +const everySource = [] +const walkAll = (dir) => { + if (!fs.existsSync(dir)) return + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, e.name) + if (e.isDirectory()) walkAll(abs) + else if (CODE.test(e.name) && !/\.d\.(ts|mts|cts)$/.test(e.name)) everySource.push({ rel: path.relative(ROOT, abs).split(path.sep).join('/'), text: fs.readFileSync(abs, 'utf8') }) + } +} +for (const d of ['src', 'e2e', 'scripts']) walkAll(path.join(ROOT, d)) +problems.push(...checkPackage(pkg), ...checkLock(lock), ...checkNobleUse(everySource)) -console.log(` scanned ${files.length} product source files and ${deps.length} package names`) +console.log(` scanned ${files.length} product source files, ${everySource.length} sources for @noble imports, and ${deps.length} package names`) console.log(` module: ${MODULE} (deriveKey ${inModule.deriveKey}, importKey ${inModule.importKey}, encrypt ${inModule.encrypt}, decrypt ${inModule.decrypt}; iteration count written ${inModule.iterations}x, format identifier ${inModule.formatId}x)`) for (const p of problems) console.error(` FAIL ${p}`) if (violations.length) { diff --git a/scripts/share-crypto-noble.mjs b/scripts/share-crypto-noble.mjs new file mode 100644 index 00000000..30e54b92 --- /dev/null +++ b/scripts/share-crypto-noble.mjs @@ -0,0 +1,172 @@ +// Issue #301 — the ONE exception to "no cryptography library" in +// `check:share-crypto`: `@noble/hashes`, for a synchronous SHA-256 behind the +// workspace and revision digests. Protected share links stay Web Crypto only. +// +// Pure functions over facts the caller gathers (package.json, package-lock.json +// and the source texts), so the rule is tested on made-up inputs in +// `share-crypto-noble.test.mjs`. The exception holds only while ALL of this is +// true: +// +// - package.json names `@noble/hashes` in `dependencies` as exactly `2.4.0`, +// and no other package that looks like a cryptography library anywhere; +// - package-lock.json has exactly one `@noble/*` package, `@noble/hashes` at +// 2.4.0, and it depends on nothing; +// - exactly one source file imports from `@noble/`: `src/model/workspace.ts`, +// with `import { sha256 } from '@noble/hashes/sha2.js'` (an alias is fine), +// one name, no default or namespace import; no other product file, test, +// end-to-end spec or script imports, re-exports or `import()`s any +// `@noble/` path; +// - `src/model/shareProtected.ts`, and every module it imports, directly or +// through others, is neither `src/model/workspace.ts` nor +// `src/model/revision.ts` (the two that expose a hash that is not Web +// Crypto) and imports nothing from `@noble/`; and the module never names +// one of their hash functions. +import ts from 'typescript' + +export const NOBLE = '@noble/hashes' +export const NOBLE_VERSION = '2.4.0' +export const NOBLE_IMPORTER = 'src/model/workspace.ts' +export const NOBLE_SPECIFIER = '@noble/hashes/sha2.js' +export const NOBLE_NAME = 'sha256' +export const PROTECTED_MODULE = 'src/model/shareProtected.ts' +export const NON_WEBCRYPTO_HASH_MODULES = ['src/model/workspace.ts', 'src/model/revision.ts'] +export const NON_WEBCRYPTO_HASH_NAMES = ['sha256Js', 'sha256Hex', 'digestOfCanonical', 'fullContentDigest', 'semanticDigest', 'nobleSha256'] + +export const CRYPTO_LIBRARY = /(^|[-_/@.])(crypto|cryptojs|argon2?|scrypt|bcrypt(js)?|sodium|libsodium|nacl|tweetnacl|forge|sjcl|noble|aes|pbkdf2?|hash-wasm|openpgp|jose|webcrypto)([-_/@.]|$)/i + +const KIND = { ts: ts.ScriptKind.TS, mts: ts.ScriptKind.TS, cts: ts.ScriptKind.TS, tsx: ts.ScriptKind.TSX, js: ts.ScriptKind.JS, mjs: ts.ScriptKind.JS, cjs: ts.ScriptKind.JS, jsx: ts.ScriptKind.JSX } + +/** package.json: the dependency fields, and the one allowed entry */ +export function checkPackage(pkg) { + const problems = [] + const fields = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] + for (const field of fields) { + for (const [name, range] of Object.entries(pkg?.[field] ?? {})) { + if (!CRYPTO_LIBRARY.test(name)) continue + if (name === NOBLE && field === 'dependencies' && range === NOBLE_VERSION) continue + if (name === NOBLE) problems.push(`package.json: '${NOBLE}' is allowed only in dependencies as exactly ${NOBLE_VERSION} (found ${field}: '${range}')`) + else problems.push(`package.json: '${name}' looks like a cryptography library - protected links use Web Crypto only`) + } + } + return problems +} + +/** package-lock.json (lockfile v2/v3 `packages`): one @noble package, 2.4.0, no dependencies */ +export function checkLock(lock) { + const problems = [] + const packages = lock?.packages + if (!packages || typeof packages !== 'object') return ['package-lock.json: no `packages` map - cannot verify the @noble/hashes exception'] + const noble = Object.keys(packages).filter((k) => /(^|\/)node_modules\/@noble\//.test(k)) + if (noble.length !== 1 || noble[0] !== 'node_modules/' + NOBLE) problems.push(`package-lock.json: expected exactly one @noble package, node_modules/${NOBLE} (found ${noble.length ? noble.join(', ') : 'none'})`) + const entry = packages['node_modules/' + NOBLE] + if (entry) { + if (entry.version !== NOBLE_VERSION) problems.push(`package-lock.json: ${NOBLE} is ${entry.version}, not ${NOBLE_VERSION}`) + for (const f of ['dependencies', 'optionalDependencies', 'peerDependencies']) { + if (entry[f] && Object.keys(entry[f]).length) problems.push(`package-lock.json: ${NOBLE} has ${f} (${Object.keys(entry[f]).join(', ')}) - it must depend on nothing`) + } + } + const root = packages['']?.dependencies?.[NOBLE] + if (root !== undefined && root !== NOBLE_VERSION) problems.push(`package-lock.json: the root names ${NOBLE} as '${root}', not ${NOBLE_VERSION}`) + return problems +} + +/** every module specifier in a source text, with what it imports */ +export function moduleReferences(rel, text) { + const ext = rel.split('.').pop() + const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, KIND[ext] ?? ts.ScriptKind.TS) + if (sf.parseDiagnostics?.length) return { parsed: false, refs: [], identifiers: [] } + const refs = [] + const identifiers = [] + const line = (n) => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1 + const visit = (n) => { + if (ts.isImportDeclaration(n) && ts.isStringLiteralLike(n.moduleSpecifier)) { + const c = n.importClause + const named = c?.namedBindings && ts.isNamedImports(c.namedBindings) ? c.namedBindings.elements.map((e) => (e.propertyName ?? e.name).text) : [] + refs.push({ kind: 'import', specifier: n.moduleSpecifier.text, line: line(n), defaultImport: Boolean(c?.name), namespaceImport: Boolean(c?.namedBindings && ts.isNamespaceImport(c.namedBindings)), named, sideEffectOnly: !c }) + } else if (ts.isExportDeclaration(n) && n.moduleSpecifier && ts.isStringLiteralLike(n.moduleSpecifier)) { + refs.push({ kind: 'export', specifier: n.moduleSpecifier.text, line: line(n) }) + } else if (ts.isCallExpression(n) && n.arguments[0] && ts.isStringLiteralLike(n.arguments[0]) && (n.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(n.expression) && n.expression.text === 'require'))) { + refs.push({ kind: 'dynamic', specifier: n.arguments[0].text, line: line(n) }) + } else if (ts.isImportTypeNode?.(n) && ts.isLiteralTypeNode(n.argument) && ts.isStringLiteralLike(n.argument.literal)) { + refs.push({ kind: 'type', specifier: n.argument.literal.text, line: line(n) }) + } + if (ts.isIdentifier(n)) identifiers.push({ text: n.text, line: line(n) }) + ts.forEachChild(n, visit) + } + visit(sf) + return { parsed: true, refs, identifiers } +} + +/** resolve a relative specifier against `rel` to one of the known source paths */ +function resolveRelative(rel, specifier, known) { + if (!specifier.startsWith('.')) return null + const dir = rel.split('/').slice(0, -1) + for (const part of specifier.split('/')) { + if (part === '.' || part === '') continue + if (part === '..') dir.pop() + else dir.push(part) + } + const base = dir.join('/') + const stem = base.replace(/\.(js|mjs|cjs|jsx)$/, '') + for (const c of [base, stem + '.ts', stem + '.tsx', stem + '.mts', stem + '.js', stem + '.mjs', base + '/index.ts', base + '/index.tsx']) if (known.has(c)) return c + return null +} + +/** + * `sources`: every source file the caller can see - product code, unit tests, + * end-to-end specs and scripts - as `{ rel, text }` with `/`-separated paths. + * Returns the problems; empty means the exception holds. + */ +export function checkNobleUse(sources) { + const problems = [] + const parsed = new Map() + for (const { rel, text } of sources) { + const r = moduleReferences(rel, text) + if (!r.parsed) problems.push(`${rel}: did not parse cleanly - refusing to report it as clean`) + parsed.set(rel, r) + } + + let allowed = 0 + for (const [rel, r] of parsed) { + for (const ref of r.refs) { + if (!ref.specifier.startsWith('@noble/')) continue + const ok = + rel === NOBLE_IMPORTER && + ref.kind === 'import' && + ref.specifier === NOBLE_SPECIFIER && + !ref.defaultImport && + !ref.namespaceImport && + ref.named.length === 1 && + ref.named[0] === NOBLE_NAME + if (ok) allowed++ + else if (rel !== NOBLE_IMPORTER) problems.push(`${rel}:${ref.line} imports '${ref.specifier}' - only ${NOBLE_IMPORTER} may use ${NOBLE}`) + else problems.push(`${rel}:${ref.line} '${ref.specifier}' as ${ref.kind}${ref.named?.length ? ' {' + ref.named.join(', ') + '}' : ''} - only \`import { ${NOBLE_NAME} } from '${NOBLE_SPECIFIER}'\` is allowed`) + } + } + if (allowed > 1) problems.push(`${NOBLE_IMPORTER}: ${NOBLE_SPECIFIER} is imported ${allowed} times - once`) + + // the protected module and everything it reaches + if (!parsed.has(PROTECTED_MODULE)) problems.push(`${PROTECTED_MODULE}: not among the sources - cannot check what it reaches`) + else { + const known = new Set(parsed.keys()) + const seen = new Set([PROTECTED_MODULE]) + const queue = [[PROTECTED_MODULE, [PROTECTED_MODULE]]] + while (queue.length) { + const [rel, chain] = queue.shift() + for (const ref of parsed.get(rel)?.refs ?? []) { + if (ref.specifier.startsWith('@noble/')) problems.push(`${chain.join(' -> ')}: imports '${ref.specifier}' - protected links use Web Crypto only`) + const next = resolveRelative(rel, ref.specifier, known) + if (!next) continue + if (NON_WEBCRYPTO_HASH_MODULES.includes(next)) problems.push(`${[...chain, next].join(' -> ')}: reaches a hash that is not Web Crypto - protected links use Web Crypto only`) + if (!seen.has(next)) { + seen.add(next) + queue.push([next, [...chain, next]]) + } + } + } + for (const id of parsed.get(PROTECTED_MODULE).identifiers) { + if (NON_WEBCRYPTO_HASH_NAMES.includes(id.text)) problems.push(`${PROTECTED_MODULE}:${id.line} names ${id.text} - protected links use Web Crypto only`) + } + } + return problems +} diff --git a/scripts/share-crypto-noble.test.mjs b/scripts/share-crypto-noble.test.mjs new file mode 100644 index 00000000..b9c72cde --- /dev/null +++ b/scripts/share-crypto-noble.test.mjs @@ -0,0 +1,126 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { checkLock, checkNobleUse, checkPackage } from './share-crypto-noble.mjs' + +// Issue #301 — the one exception to "no cryptography library", on made-up +// facts. Each counter-example must fail; the real repository must pass. + +const GOOD_IMPORT = "import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js'\nexport const h = (b) => nobleSha256(b)\n" +const PROTECTED = "import { base64urlEncode } from './share'\nexport const seal = () => base64urlEncode(new Uint8Array(1))\n" +const SHARE = 'export const base64urlEncode = (b) => String(b.length)\n' + +/** a repository that holds the exception: override any file by path */ +function sources(over = {}) { + const base = { + 'src/model/workspace.ts': GOOD_IMPORT, + 'src/model/revision.ts': "import { h } from './workspace'\nexport const d = (b) => h(b)\n", + 'src/model/shareProtected.ts': PROTECTED, + 'src/model/share.ts': SHARE, + 'src/model/other.ts': 'export const x = 1\n', + 'src/model/other.test.ts': "import { x } from './other'\nexport const y = x\n", + 'e2e/some.spec.ts': 'export const z = 1\n', + } + return Object.entries({ ...base, ...over }).map(([rel, text]) => ({ rel, text })) +} +const PKG = (deps = { '@noble/hashes': '2.4.0', react: '^19.0.0' }, dev = {}) => ({ dependencies: deps, devDependencies: dev }) +const LOCK = (extra = {}, noble = { version: '2.4.0', license: 'MIT' }) => ({ + packages: { '': { dependencies: { '@noble/hashes': '2.4.0' } }, 'node_modules/react': { version: '19.0.0' }, 'node_modules/@noble/hashes': noble, ...extra }, +}) + +describe('the exception holds as written', () => { + it('package, lockfile and sources pass', () => { + expect(checkPackage(PKG())).toEqual([]) + expect(checkLock(LOCK())).toEqual([]) + expect(checkNobleUse(sources())).toEqual([]) + }) + + // the real repository is checked by `npm run check:share-crypto` itself, + // which runs these three functions over every source, the package file and + // the lockfile (and runs in CI's checks job); it is not repeated here + it('the real package files pass', () => { + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + expect(checkPackage(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')))).toEqual([]) + expect(checkLock(JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8')))).toEqual([]) + }) +}) + +describe('package.json', () => { + it.each([['2.4.1'], ['^2.4.0'], ['~2.4.0'], ['2.x'], ['latest']])('another version (%s) fails', (v) => { + expect(checkPackage(PKG({ '@noble/hashes': v })).join()).toMatch(/exactly 2\.4\.0/) + }) + it('as a devDependency it fails', () => { + expect(checkPackage(PKG({}, { '@noble/hashes': '2.4.0' })).join()).toMatch(/only in dependencies/) + }) + it.each([['@noble/ciphers'], ['@noble/curves'], ['tweetnacl'], ['crypto-js'], ['node-forge']])('another cryptography library (%s) still fails', (name) => { + expect(checkPackage(PKG({ '@noble/hashes': '2.4.0', [name]: '1.0.0' })).join()).toMatch(/looks like a cryptography library/) + }) +}) + +describe('package-lock.json', () => { + it('another version fails', () => { + expect(checkLock(LOCK({}, { version: '2.4.1' })).join()).toMatch(/not 2\.4\.0/) + }) + it('a dependency of its own fails', () => { + expect(checkLock(LOCK({}, { version: '2.4.0', dependencies: { 'some-dep': '1.0.0' } })).join()).toMatch(/must depend on nothing/) + }) + it('a second @noble package fails', () => { + expect(checkLock(LOCK({ 'node_modules/@noble/curves': { version: '2.0.0' } })).join()).toMatch(/exactly one @noble package/) + }) + it('a nested copy fails', () => { + expect(checkLock(LOCK({ 'node_modules/x/node_modules/@noble/hashes': { version: '1.8.0' } })).join()).toMatch(/exactly one @noble package/) + }) + it('the root asking for another version fails', () => { + const lock = LOCK() + lock.packages[''].dependencies['@noble/hashes'] = '^2.4.0' + expect(checkLock(lock).join()).toMatch(/the root names/) + }) +}) + +describe('the one import', () => { + it.each([["'@noble/hashes/sha3.js'"], ["'@noble/hashes/utils.js'"], ["'@noble/hashes'"], ["'@noble/hashes/sha2'"]])('another noble path (%s) fails', (p) => { + const text = `import { sha256 } from ${p}\n` + expect(checkNobleUse(sources({ 'src/model/workspace.ts': text })).join()).toMatch(/only `import \{ sha256 \}/) + }) + it.each([ + ['another name as well', "import { sha256, sha512 } from '@noble/hashes/sha2.js'\n"], + ['another name instead', "import { sha224 } from '@noble/hashes/sha2.js'\n"], + ['a namespace import', "import * as noble from '@noble/hashes/sha2.js'\n"], + ['a default import', "import noble from '@noble/hashes/sha2.js'\n"], + ['a re-export', "export { sha256 } from '@noble/hashes/sha2.js'\n"], + ['a dynamic import', "export const m = import('@noble/hashes/sha2.js')\n"], + ])('%s fails', (_label, text) => { + expect(checkNobleUse(sources({ 'src/model/workspace.ts': text })).length).toBeGreaterThan(0) + }) + it('importing it twice fails', () => { + expect(checkNobleUse(sources({ 'src/model/workspace.ts': GOOD_IMPORT + "import { sha256 as again } from '@noble/hashes/sha2.js'\n" })).join()).toMatch(/imported 2 times/) + }) + it.each([['src/model/other.ts'], ['src/components/Thing.tsx'], ['src/model/other.test.ts'], ['e2e/some.spec.ts'], ['scripts/tool.mjs']])('an import from %s fails', (rel) => { + expect(checkNobleUse(sources({ [rel]: "import { sha256 } from '@noble/hashes/sha2.js'\n" })).join()).toMatch(/only src\/model\/workspace\.ts may use/) + }) +}) + +describe('protected links stay Web Crypto only', () => { + it('a direct noble import in the module fails', () => { + const text = PROTECTED + "import { sha256 } from '@noble/hashes/sha2.js'\n" + expect(checkNobleUse(sources({ 'src/model/shareProtected.ts': text })).join()).toMatch(/shareProtected\.ts.*Web Crypto only/) + }) + it('importing the workspace hash wrapper fails', () => { + const text = PROTECTED + "import { sha256Js } from './workspace'\nexport const k = sha256Js(new Uint8Array(1))\n" + const out = checkNobleUse(sources({ 'src/model/shareProtected.ts': text })).join('\n') + expect(out).toMatch(/shareProtected\.ts -> src\/model\/workspace\.ts: reaches a hash that is not Web Crypto/) + expect(out).toMatch(/names sha256Js/) + }) + it('reaching it through another module fails', () => { + const share = "import { sha256Hex } from './workspace'\nexport const base64urlEncode = (b) => sha256Hex(b)\n" + expect(checkNobleUse(sources({ 'src/model/share.ts': share })).join()).toMatch(/shareProtected\.ts -> src\/model\/share\.ts -> src\/model\/workspace\.ts/) + }) + it('reaching the revision digests fails', () => { + const text = PROTECTED + "import { digestOfCanonical } from './revision'\n" + expect(checkNobleUse(sources({ 'src/model/shareProtected.ts': text })).join()).toMatch(/revision\.ts: reaches a hash/) + }) + it('a module that does not parse is never reported as clean', () => { + expect(checkNobleUse(sources({ 'src/model/shareProtected.ts': 'import { from \n' })).join()).toMatch(/did not parse cleanly/) + }) +}) diff --git a/src/model/revision.ts b/src/model/revision.ts index d0b53dc7..d05bf5ec 100644 --- a/src/model/revision.ts +++ b/src/model/revision.ts @@ -574,8 +574,9 @@ export function canonicalJson(x: CanonicalContent): string { } /** §R4.4 — `fullContentDigest` = SHA-256 (lowercase hex) of the UTF-8 bytes of - * `canonicalJson(canonicalContent(doc))`. Web Crypto where present, pure-JS - * fallback elsewhere (shared with `loop-workspace/1`). */ + * `canonicalJson(canonicalContent(doc))`. Web Crypto where present, the + * synchronous `sha256Js` (`@noble/hashes`) elsewhere (shared with + * `loop-workspace/1`). */ export async function fullContentDigest( doc: { nodes: LoopNode[] @@ -593,7 +594,8 @@ export async function fullContentDigest( return sha256Hex(utf8Bytes(canonicalJson(canonicalContent(doc, { modelVersion })))) } -/** synchronous digest of an already-built `CanonicalContent` (pure-JS SHA-256). +/** synchronous digest of an already-built `CanonicalContent` (`sha256Js`, the + * synchronous SHA-256 of `@noble/hashes`). * Used where the caller has the projection in hand and wants no `await`. */ export function digestOfCanonical(c: CanonicalContent): string { return sha256Js(utf8Bytes(canonicalJson(c))) diff --git a/src/model/sha256Baseline.fixture.ts b/src/model/sha256Baseline.fixture.ts new file mode 100644 index 00000000..4c5bddce --- /dev/null +++ b/src/model/sha256Baseline.fixture.ts @@ -0,0 +1,1260 @@ +// SHA-256 outputs of the pure-JS `sha256Js` as it shipped on main 44af9a4, +// recorded BEFORE it was replaced by `@noble/hashes` (issue #301). The +// replacement must give the same bytes for every one of them. Generated +// data; do not edit by hand. + +/** the deterministic input of a vector: `len` bytes from a fixed LCG seeded with `seed` */ +export function baselineBytes(len: number, seed: number): Uint8Array { + const b = new Uint8Array(len) + let x = seed >>> 0 + for (let i = 0; i < len; i++) { + x = (Math.imul(x, 1664525) + 1013904223) >>> 0 + b[i] = x >>> 24 + } + return b +} + +export const SHA256_VECTORS: readonly { len: number; seed: number; hex: string }[] = [ + { + "len": 0, + "seed": 1, + "hex": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "len": 1, + "seed": 2, + "hex": "dabd3aff769f07eb2965401eb029974ebba3407afd02b26ddb564ea5f8efae72" + }, + { + "len": 2, + "seed": 3, + "hex": "da569f54ddcd3ca7fce75cd0f34eedbc2a678552a9beade80f6b870c9a61e394" + }, + { + "len": 3, + "seed": 4, + "hex": "8c808a42f2c0c4c29dfb4e59c0053d5fb83ee9a52e1460cc9e9ff767ef9ec9ee" + }, + { + "len": 4, + "seed": 5, + "hex": "a75ee86f7df2eacefc7246b557e5854f1e33d393c625a913a1935493d57971de" + }, + { + "len": 5, + "seed": 6, + "hex": "e995aabaa013c7cc66cec552486d26e3b5cd1bd7525a26757c09f824c9eaae25" + }, + { + "len": 6, + "seed": 7, + "hex": "e5e4276dcb04f21dc33ac93bda106dde9ef36065a3b17f2d749f486db104e2b4" + }, + { + "len": 7, + "seed": 8, + "hex": "74a4a388e5d84fb98c031a61ea09d5ce589cfd489ad32bced08358fd3e3ba619" + }, + { + "len": 8, + "seed": 9, + "hex": "1c7fb01367e51cb60a0486ad10ae283b7cd4cee89b1430187fdc765d961fd0f0" + }, + { + "len": 9, + "seed": 10, + "hex": "44f994aa2cbb474920513d90ae2a709c17eba4e91d738033f97db89e42340be0" + }, + { + "len": 10, + "seed": 11, + "hex": "ff797c4023104acff268f7d156faa49c3daa1c12ead5a53b15fe4bb2e43d35f3" + }, + { + "len": 11, + "seed": 12, + "hex": "a85d3d87853f4d8419663a1a87d8c1feb3da239491b172a2c896fd236efd51c4" + }, + { + "len": 12, + "seed": 13, + "hex": "0b6950408cf6b6bba501acaa2de833ee14bc95814086735d3d804551c2ecb825" + }, + { + "len": 13, + "seed": 14, + "hex": "0a196781ea9d7df35ba16cee3cd8615ca08f84d93dc8f8b623bff1a864c43c37" + }, + { + "len": 14, + "seed": 15, + "hex": "c37f936a51e6e3456fd511d9761e1501f09da571408129f458e4463852cdfa36" + }, + { + "len": 15, + "seed": 16, + "hex": "29051d1c6bed5a21a25092fb5a86909fb893be05ede5eea7f1f1253d7fb3691e" + }, + { + "len": 16, + "seed": 17, + "hex": "224f83f0e0ac2adebc7630445d2462dde9cd5bcd49b9b2a213571d5bdbdd19e0" + }, + { + "len": 17, + "seed": 18, + "hex": "d94ac30df9f0046971aca837716e319c7ffc074c6c320221aa63a1ad478f22dd" + }, + { + "len": 18, + "seed": 19, + "hex": "a1aed5e910cdaffeb9754260f14894c360078e87dafb4833d685310c687a8342" + }, + { + "len": 19, + "seed": 20, + "hex": "53522a2528f783e06bcd6a5c04cd3c82c23f7861c39b4474d223ec36bdc1eb91" + }, + { + "len": 20, + "seed": 21, + "hex": "e4a9f8550166cba7447e2f32145c015595dece5d7367e690f95a11269398a7b2" + }, + { + "len": 21, + "seed": 22, + "hex": "cdbe860c74d744e71fe0f2958253a955fa4dd2f7c4bebdefdfe0322661501214" + }, + { + "len": 22, + "seed": 23, + "hex": "79697319b62cf7a84486aa8b277fd1f356a9b188b9f240131e92059d810a29a1" + }, + { + "len": 23, + "seed": 24, + "hex": "c7bdb08097a9338c2add48abb1541661fec32c548705b2098ebddb4f4346420a" + }, + { + "len": 24, + "seed": 25, + "hex": "69345b3786be3d0c577abbb10c5a7ac57f312d42fa99d384b55b745d42f7383a" + }, + { + "len": 25, + "seed": 26, + "hex": "fd9ab3d873c1c013a4312009754d7462d9f1657288cd9891f4b696d48a36e20f" + }, + { + "len": 26, + "seed": 27, + "hex": "ddf1e47515147523f0dc956bfd57d8db2763fe4bd8bb7cecc14d8cba56203ff3" + }, + { + "len": 27, + "seed": 28, + "hex": "4dd4b527be8f3790092d82135813e3fa82d41e5694dea3fad5d97ef3a038cae0" + }, + { + "len": 28, + "seed": 29, + "hex": "5bb465d5e786f6c23d905a58583c0249c0d6b74ba5aa632313f5f89756f46619" + }, + { + "len": 29, + "seed": 30, + "hex": "fb4228e47a624aeac37422551c54f859408435814337b3672943827ffb6ee7e7" + }, + { + "len": 30, + "seed": 31, + "hex": "b3cf32b2ff27bc03d0b0048fa39331f71f7a4dfff630b09c617b80aa25dd94b4" + }, + { + "len": 31, + "seed": 32, + "hex": "cb4cce0dfaf6ee9827c5b3932f61b988ef76e7c0be28c8099b383c5e59797d43" + }, + { + "len": 32, + "seed": 33, + "hex": "bf7d7747d82eed21b2eb9f159aec979b394ac328a830d66ee5dede5838cee625" + }, + { + "len": 33, + "seed": 34, + "hex": "e56d5f2ade851b8adf789b186be67d3c093a37096dcbc50c3b9bdcd6ce522de0" + }, + { + "len": 34, + "seed": 35, + "hex": "85d04b7b605badbeab49608cd412fe20bcc6db7aadf49763a58daa013aa44031" + }, + { + "len": 35, + "seed": 36, + "hex": "1b13b343ab83f2e6c0e4b16fdf7e7f2126cfb4bc1b8e04601a27d8dbdfdb8fbc" + }, + { + "len": 36, + "seed": 37, + "hex": "fea7412b78e402a536f6906347183134c8f1304b0f80c67195e3ca59136c24c9" + }, + { + "len": 37, + "seed": 38, + "hex": "0223138f201f3a670bb6bc06709cb9b8bb199c87e113e12d43ad36d1525cbf55" + }, + { + "len": 38, + "seed": 39, + "hex": "b4fbf5f7f51b22d9c516910f238ac92e67efa728b18d8672f1f9c79c4ee0d74c" + }, + { + "len": 39, + "seed": 40, + "hex": "15e148d1d71edd91e599f4aaaa0d756dbd5e7d6cde12f296d25f79d3df322816" + }, + { + "len": 40, + "seed": 41, + "hex": "e30b381f4aca7355bd08555dc70dc13404b229b65133c8506d29d614241e02e7" + }, + { + "len": 41, + "seed": 42, + "hex": "c0c414132890f2d0879ebd2e720ded87005edc95fba89c493f19d677d2435e24" + }, + { + "len": 42, + "seed": 43, + "hex": "436af5cd209b21f5c9ce7e6ec8f173e2ca462d57091531b5e9f80b89be6ed406" + }, + { + "len": 43, + "seed": 44, + "hex": "f57fcd078c7c59ad1e8bded5317f78ebcaefd067a7e08ed7059274f87c14b580" + }, + { + "len": 44, + "seed": 45, + "hex": "b4976303b5addc4727bb4361958c37795ea39de8ba6d15452e51ed4d694380e3" + }, + { + "len": 45, + "seed": 46, + "hex": "df3a032f24431363787624d866a71b382de9c1eb27e89b23ae1964474314332a" + }, + { + "len": 46, + "seed": 47, + "hex": "9ffd96b815e2a306a31e2438d614c317d1c4dad33c80fd3dc23671b3068e876b" + }, + { + "len": 47, + "seed": 48, + "hex": "496a9d596f7126452bbbcce3b26eb7c2793068e537c00c7d23eb0552dcf3c806" + }, + { + "len": 48, + "seed": 49, + "hex": "24b9e08867a1a643c70d96e7e63e5a2f74149a97c82ab21dce5d19135c611eff" + }, + { + "len": 49, + "seed": 50, + "hex": "c3b07a134959094dec7fa2d9447f2677d98d2c4912e8cbbc2649910539856ae3" + }, + { + "len": 50, + "seed": 51, + "hex": "5a06381ce563f98884e48133d46c5dfda15b904d4eb65699cc289a9fa3a0a93f" + }, + { + "len": 51, + "seed": 52, + "hex": "86480ff013cdd5b7d2cf1b44db6a4743f80e70d1ab09a47045a20cd9a3da35cb" + }, + { + "len": 52, + "seed": 53, + "hex": "37784c605c7551f44b430bea72a7009dbfd3779486b34be6361c87b5777202c4" + }, + { + "len": 53, + "seed": 54, + "hex": "ed13c3291067a53e38c390445e6f29c89c48c960516c40d545494762b0fc8ebe" + }, + { + "len": 54, + "seed": 55, + "hex": "806d26e32c290ce386540f7b1edf28f6deaa025890016034918d58e4f81e561a" + }, + { + "len": 55, + "seed": 56, + "hex": "f044de87773475849da05199198c86f4a6dc983f3f91527eda56049e0ab21146" + }, + { + "len": 56, + "seed": 57, + "hex": "4069255bff446e28f0a5bcd961b388732b3b58fb99cb49f101d8bec9047afc13" + }, + { + "len": 57, + "seed": 58, + "hex": "06bfb6b915d8829ddf1657aeaee958a6ddc5ddd478d5845ec4dbeb3a00b7692a" + }, + { + "len": 58, + "seed": 59, + "hex": "6570644b6c3c69c7232a3b9329d4b4615f86777985073df4b814c21829718972" + }, + { + "len": 59, + "seed": 60, + "hex": "2596dff20e42e6ba415be677b36e8084de3335eea2a3eb56e17dbf3cdc9805b2" + }, + { + "len": 60, + "seed": 61, + "hex": "bb0802e9b1077a91393f46370f96c348d8923d5dacdb9ef4d0049abd28ee0f04" + }, + { + "len": 61, + "seed": 62, + "hex": "6bd45d7e22c7101fc2d5d553a1e950f92b43072e5cca3fdb8020ce426812a712" + }, + { + "len": 62, + "seed": 63, + "hex": "0c7945a7c1457cad1b531fba88fc565c9e86e150b97c4b06d2884876a21f2d8c" + }, + { + "len": 63, + "seed": 64, + "hex": "3422c503299c94eafd8a5dcedb0549e1dc397c4e2a14af9d9711f7b91986058d" + }, + { + "len": 64, + "seed": 65, + "hex": "25949f8ce98798d11e8ae4e006acfc632e347eb054957ffcdd9b0052d4d7ccc4" + }, + { + "len": 65, + "seed": 66, + "hex": "1baed0b79a7134c2e95d49065de2b1bbd69050cca441d8bab07ff076d7f85f23" + }, + { + "len": 66, + "seed": 67, + "hex": "4e97049c816733865bd8038986001693b91cfb182fccf925fa1825cfba7f4462" + }, + { + "len": 67, + "seed": 68, + "hex": "cdc3a545280dfbb93fb47d687eaa48517ae81cdcdbc9345e012790089436f2e4" + }, + { + "len": 68, + "seed": 69, + "hex": "204a975edf0bb7eabf97b8f74b4c54e12497a7d6bab0fc1ffb4441d1f7ad6d84" + }, + { + "len": 69, + "seed": 70, + "hex": "f0d0e10a33cd6e5b2c57a1a05f70666d47c2fbbb46c9e35b9bffb49f5af79aa0" + }, + { + "len": 70, + "seed": 71, + "hex": "417da5e6958ff7a3b8cc01e90f359925ac1f0d7621f824477aceb6deb7d6ed8d" + }, + { + "len": 71, + "seed": 72, + "hex": "ea07bd1618dbe08485d682df10a36f34ab238e16954427be18b724ba8a56e010" + }, + { + "len": 72, + "seed": 73, + "hex": "7dd1c5e9dbeca00a9af3cd9509a0943a9d46ef9cb39de48107ce9c50ebc4d42c" + }, + { + "len": 73, + "seed": 74, + "hex": "581f6f0dbdbca755811b34001c4165a6a1b3c1663961d85669d5782f2dab1c86" + }, + { + "len": 74, + "seed": 75, + "hex": "aa6d438992daaf78586a660c4fac235b1e480502516b932ffae0b002150be517" + }, + { + "len": 75, + "seed": 76, + "hex": "11625e69216df77c2f57598f02aee2b3acd6bf28f8a353b95f6be71dcaa052ca" + }, + { + "len": 76, + "seed": 77, + "hex": "469e0a8b3f51860e4fe86cc0643a9281ca807354409b983a6ccbfd747cc4d849" + }, + { + "len": 77, + "seed": 78, + "hex": "302c511826089be5b019595677806caeab90b945d29b955307515154b2c7fc58" + }, + { + "len": 78, + "seed": 79, + "hex": "fe76041cb4f557cd359eba07da12a13272f0e31975d42daa2a7c4eb175ff6e41" + }, + { + "len": 79, + "seed": 80, + "hex": "1284a01b9c4a510bb7e40accd4c3a6eb421cb29ee066f3cea8fa2c02fd683800" + }, + { + "len": 80, + "seed": 81, + "hex": "5b34c45195561c10c9538ea315f22242de21792ff6a8ba7d8659ae405537ab22" + }, + { + "len": 81, + "seed": 82, + "hex": "573b548887ef4e4ce2a81c76999098581898c34e1e6c5994fda151126aa45770" + }, + { + "len": 82, + "seed": 83, + "hex": "673c5411b9083077e2412c99e34d10664e0be688007a453227b1a5313ae2adc1" + }, + { + "len": 83, + "seed": 84, + "hex": "3e1d6ed787cc4586e04eff7863529040d0f4bce4c75433ee6b4ac13359e55ed9" + }, + { + "len": 84, + "seed": 85, + "hex": "dad1618a16ed023d474916a78af1d5bbcce9211e8d410eff1c89e26222f48883" + }, + { + "len": 85, + "seed": 86, + "hex": "03cb5364c4efb17227e4378ab7d0e93e2cce92217cfeb17d2d7d6111b7c1dd66" + }, + { + "len": 86, + "seed": 87, + "hex": "597f6ed0f780c56fe55c0b62f7d8488e52c634e507e89144b98e2bd5b2836376" + }, + { + "len": 87, + "seed": 88, + "hex": "4d52f07ec21bb5a19392499a65fb1c3f3a986925fcc283e385fcded2bc86761b" + }, + { + "len": 88, + "seed": 89, + "hex": "6ff0782272091071ba253001c71ff0ea67eed0f993d2cdbe3284a8383e8af9b3" + }, + { + "len": 89, + "seed": 90, + "hex": "6baa7eb93997d23e7a2836ee295081e4e3e24c0008ac5cfdf1b4a8be3e4d6673" + }, + { + "len": 90, + "seed": 91, + "hex": "72d3c8db85ccc01b2d922413da9b34ada062a9c93db833f87ab12f753b540fb2" + }, + { + "len": 91, + "seed": 92, + "hex": "f9df70466c5065420a8247913422305875af1a05c91ef0a2a35693967206d162" + }, + { + "len": 92, + "seed": 93, + "hex": "475970a9f0403854b91922e778a6556498422a498de88923c0637072033c4a9c" + }, + { + "len": 93, + "seed": 94, + "hex": "f495e1192111f4ea2e83ea5dce7c44908fd4c645e2aa2e41a772fcc1773d7fed" + }, + { + "len": 94, + "seed": 95, + "hex": "3165da2453fb58d5938b245c9555c6ae3404f3af39aec69be569b89c8d14b20f" + }, + { + "len": 95, + "seed": 96, + "hex": "4f7e0d97d6f30b2b40c718550717ccc7f15f40d36ebbedd0b32367a148c71247" + }, + { + "len": 96, + "seed": 97, + "hex": "d09eed4d43b8c711dbe49407e1caa4fc5dbdee385e629e9d0510802f1a10b9b4" + }, + { + "len": 97, + "seed": 98, + "hex": "cc323d07ddce8998ce4483dfe2f29d5b0d8b273c8e958212d3f6b7b76286483a" + }, + { + "len": 98, + "seed": 99, + "hex": "c8e75f970d21d7ea8b6f9c6f6251f8396fe8113f9ff29b436c4ac7f3363a046b" + }, + { + "len": 99, + "seed": 100, + "hex": "0f05d7b719c097cf24a4efdab2d82f5cb4069302dec1ebcf5b2d5453da62fd68" + }, + { + "len": 100, + "seed": 101, + "hex": "fad13691e6ed6645aa7936a2c6ea52dd71d18486a7a8e1717e57ebdd12573f77" + }, + { + "len": 101, + "seed": 102, + "hex": "07ca6620268a7bb01bbad92aceb627858b3f413baaa784d11994733e442a9231" + }, + { + "len": 102, + "seed": 103, + "hex": "4ff4d0024023cff317ef938e0e3e9bb61b7856705894575227b4b4e71b8fc7aa" + }, + { + "len": 103, + "seed": 104, + "hex": "ce65bd87791a54e93b88f7e02bfd9043537beccd8107c16fc5d14a59521a5ecd" + }, + { + "len": 104, + "seed": 105, + "hex": "7f110ec51570b96fcb9bc396c56e68c53d7232f56ff0225c43827840300ec1f6" + }, + { + "len": 105, + "seed": 106, + "hex": "12bb5ea46928441a07dea842085478b539e105530ec78372ee8178e986cb480e" + }, + { + "len": 106, + "seed": 107, + "hex": "712e73ed460e06110cfffd08916fa59a80a7b0d452461ca9b74bdda80a4ecac0" + }, + { + "len": 107, + "seed": 108, + "hex": "11a1ae258e92212f083d88e1f6b9f93131c4fb91ab4835b44550cc66d0bcbe17" + }, + { + "len": 108, + "seed": 109, + "hex": "37d5643f93eb508aed9c208082b29da29fc066af209afa17af4a74a388d049de" + }, + { + "len": 109, + "seed": 110, + "hex": "0842cffb5d17c9a6703468954f76371caee89291958e27d880a288ec38cce291" + }, + { + "len": 110, + "seed": 111, + "hex": "4726354c5b176a254cf20941b49320d7eb0472b381c6f69b286f3a75b4edf4af" + }, + { + "len": 111, + "seed": 112, + "hex": "420d628294de9f839d1855535e922ba23411acd93b8cd43d74aa0bd8e0cc05f0" + }, + { + "len": 112, + "seed": 113, + "hex": "079079cd6ac094b16a63a00a4bd41f3e8d902a9fa900927434f227df60a1359b" + }, + { + "len": 113, + "seed": 114, + "hex": "d6b5de71fbdd203f55ed0e000f1896c9f219ad9f345ac2a80166bb546dcdc0a8" + }, + { + "len": 114, + "seed": 115, + "hex": "d23b75e64796d77a79811d8ddfafc1ec7dd587f81a0a2921e385d7df3a91cebd" + }, + { + "len": 115, + "seed": 116, + "hex": "c2c22cbb63aa17a6ea6ba162852475a5c265f3fb6c8e34b0f7de7deb50443768" + }, + { + "len": 116, + "seed": 117, + "hex": "fa188b2b88d1c839672f7daa5cd3196300412368efcc8e9f628071ee4051e273" + }, + { + "len": 117, + "seed": 118, + "hex": "902136c8ec41af581dfd7bf4e1d21a39e0081ce5788bf42012123c7fa68b509a" + }, + { + "len": 118, + "seed": 119, + "hex": "95ac3c3dad703d7a3f92bd1f09ac71d527cd6b8028b8df89da4554c3ae1c2823" + }, + { + "len": 119, + "seed": 120, + "hex": "cd4f973f21e01fbfc558f091d7bc4d9ae05928ed7e571b394def626d7fa7138b" + }, + { + "len": 120, + "seed": 121, + "hex": "a64ad0e780188f6442bad06f4202ef41fd3319ffa1431cefdd5faec5c7021ebe" + }, + { + "len": 121, + "seed": 122, + "hex": "f65e7df0a23d98d84814770e65f66ac707b13276a1523a5dee3b9629b6d4e0f3" + }, + { + "len": 122, + "seed": 123, + "hex": "83908858f2e254c0796a27092a6dab6c1533df2e9c1b947495ec37b2b29b25c8" + }, + { + "len": 123, + "seed": 124, + "hex": "3e48bc7cdbbe659b675dd41d2b7798888033b32c558b0c63f2a9afb95359f09a" + }, + { + "len": 124, + "seed": 125, + "hex": "4559b8f6dffe20d5d8875c1a2c1796d89e62326cc869829d2972d1e9883ad8f1" + }, + { + "len": 125, + "seed": 126, + "hex": "2756b5afb46abd311722d4e0d89f757ae0d6b92490a36bea7ca24c10d57b79d2" + }, + { + "len": 126, + "seed": 127, + "hex": "9b23c74f1b6f24095e375574439b5aad9ed465b933a2d1448dca6a4b9e42b834" + }, + { + "len": 127, + "seed": 128, + "hex": "5c67c09e1a5a97f83a2abb1a01d3256c8f55750c892e31921109e06b4262e5e4" + }, + { + "len": 128, + "seed": 129, + "hex": "aa3f2e6ed977c082c38c29229f026d813008a6c898d2cdcb4a610143557e0a70" + }, + { + "len": 129, + "seed": 130, + "hex": "2ce264308a0e80f93b8ee2008e5993e66886d7faff72bed405639045959fa5ad" + }, + { + "len": 130, + "seed": 131, + "hex": "4ace2135f0960badf3681a2e4140df4a17f32f7232ff57fa28e04e918b5463e8" + }, + { + "len": 131, + "seed": 132, + "hex": "cab3fb444bdf134e6668c0effd4d58c50d0edfa310b118978d80f5b7f7854b36" + }, + { + "len": 132, + "seed": 133, + "hex": "9b1b904dec36d38c1e19e13bf1f4d35a25525a03d76450ffa8f4011a6f34c6cd" + }, + { + "len": 133, + "seed": 134, + "hex": "e7a50752a7f8f67ed59875b72c6efd4bc5ad2750e4512287bdd7d7568ad91bff" + }, + { + "len": 134, + "seed": 135, + "hex": "94a9c89c522752858ae9eb29cbaaaa99b5a7e92e40a14ccd8ad1ea60ce17e09e" + }, + { + "len": 135, + "seed": 136, + "hex": "fc29bf45c7691541dfd50620981332b33cd6c5f38956a703ace14e5a96aaab03" + }, + { + "len": 136, + "seed": 137, + "hex": "f5b9d4d47afc52be15c94019e75f8f78ddb8c2257a06f7c63ad4310b54de1c06" + }, + { + "len": 137, + "seed": 138, + "hex": "cf24599dcf2b16d74e7de0627f41e15ba2904674c590cc8e775e2062b45649b2" + }, + { + "len": 138, + "seed": 139, + "hex": "d914dd0f1b9008feb1dec63c2351faae65351235f9ff09a0add3e5d38a13540f" + }, + { + "len": 139, + "seed": 140, + "hex": "2a7e25c70b2249489b2a37037becee4eee09bd4fd7510aead7541290f1a11ab6" + }, + { + "len": 140, + "seed": 141, + "hex": "978bf8188443a6e6d3086865077137922bc588fa6dfe815403f910b3765b0479" + }, + { + "len": 141, + "seed": 142, + "hex": "c6e1f494c50e19205651b4ceeb201dd8f5fa24eabe9078e79495b6fdcc2f398b" + }, + { + "len": 142, + "seed": 143, + "hex": "e2ed54c1f3084f49582887acdb79d47105cefe7561741941067cb93ca0570bc3" + }, + { + "len": 143, + "seed": 144, + "hex": "4fbfc9fc38870971a05111cb838238c9ba148966cf6a76da1947fc54041a6dbf" + }, + { + "len": 144, + "seed": 145, + "hex": "a1ca1ca554f8c1e1c3c56444b3103d87b3048b1a8abb2f368df85bc369139722" + }, + { + "len": 145, + "seed": 146, + "hex": "2cb4feda2f2bb8714840390c1b7aee0e113725fb1311901bdad732d001e3411f" + }, + { + "len": 146, + "seed": 147, + "hex": "e7b41553e30535413d463f88087258a3844e9881e847bc4440b396971eebfa1c" + }, + { + "len": 147, + "seed": 148, + "hex": "4543a0fac021dd758fc2461c2949cd092eecd6fa640e8f294903db6aa4e6f685" + }, + { + "len": 148, + "seed": 149, + "hex": "3e07cfaa676638603af67cbe21d2b756363ef9372a343ece4d76f33cf95f34f7" + }, + { + "len": 149, + "seed": 150, + "hex": "a96e4d5fc3953c7396c5656dabec4d6ab8cc57432b5ac81fca9b244bb4195d93" + }, + { + "len": 150, + "seed": 151, + "hex": "c25053259e854e4ec9945b6a74e47858cb4a8be857363994e1e9ea2ae9008eb7" + }, + { + "len": 151, + "seed": 152, + "hex": "e087121607d4ba549d464eb1e82a68c46e2111509a14bab0c53f952a0915e446" + }, + { + "len": 152, + "seed": 153, + "hex": "8b3f99b309ee35582e2fbafb6fa4c5ebeb1b9e3fb9f18e32516900e2e0253ba6" + }, + { + "len": 153, + "seed": 154, + "hex": "6ee96159649cd84c48d3d7cef1b43a6b79609c1dbcf0249f7e3cf0aec2bc6a5e" + }, + { + "len": 154, + "seed": 155, + "hex": "a1416f5bcb1aae212475654f25da37fad5aec9b378bcdd821975d072b63f7a65" + }, + { + "len": 155, + "seed": 156, + "hex": "8c2ae7ee0a1921cc45251edda4ee3a9e3c1142db81a5438df648fd947b8f848f" + }, + { + "len": 156, + "seed": 157, + "hex": "e50d43af7912147995bea563bb7edac52672d0d54a51e15f0d6578607d8747b8" + }, + { + "len": 157, + "seed": 158, + "hex": "e9f43382c04a299bfc8b9269b62b12a1077157be87569b3fd49a23500fd70702" + }, + { + "len": 158, + "seed": 159, + "hex": "d1a9f5f7471ac29a1bc822eabbc2102cf7ceb7a290900a7c9c33f4e3b0af8434" + }, + { + "len": 159, + "seed": 160, + "hex": "71c51a2439d3ed7fe724f9aa582558ae9d313d0e8bb6098d6232126a7a5a7db9" + }, + { + "len": 160, + "seed": 161, + "hex": "8d58d58bc8bccb2c23b19a0abb0d11ead15267a5e8cfba256f4e56bd2a6a76a7" + }, + { + "len": 161, + "seed": 162, + "hex": "9f1465fbfd03a22c176fd611cd9be5cbac70f8a43cfdd6934870853d19f379b6" + }, + { + "len": 162, + "seed": 163, + "hex": "a17ed15f28c197c9096655159446f4989f41e6d6e9be3652bff55d8ead7c0831" + }, + { + "len": 163, + "seed": 164, + "hex": "e098a43303f1d53ac7ea40b0a782f48e1e07eb8cb5242cac8559fc00529424c0" + }, + { + "len": 164, + "seed": 165, + "hex": "c372e2a09ab5bce91e67fd8bb367eebf38d5a1c3cd48de729cdad483b3ec9089" + }, + { + "len": 165, + "seed": 166, + "hex": "d210b3691d62119345d217d4352c632bc8f1f772885f17c4ff434d72fcad4e63" + }, + { + "len": 166, + "seed": 167, + "hex": "8a9833d3dec2250cfc28957a02f210e8263af482a86bf3aa11f2f44f267858d0" + }, + { + "len": 167, + "seed": 168, + "hex": "0d3a3a9a65ec57f2e59f166c1420d104f783d2c16aa1b5be6e90873191d772bd" + }, + { + "len": 168, + "seed": 169, + "hex": "48193c35c6167768083c1799822803e0cf6e4368a54d008bfcab35e73768c415" + }, + { + "len": 169, + "seed": 170, + "hex": "622181e6ae386b8003f50934a1bb7e04447844fc314cd70f25b660ea5e58d5cc" + }, + { + "len": 170, + "seed": 171, + "hex": "f4e78dfde467904ca2923e7ab0ca0a8581fb77705f968543bee29f2f1fc5b14b" + }, + { + "len": 171, + "seed": 172, + "hex": "c3a563e4658eae21100e7c9e448af2d4b6ff3e5b24a9fc28c287f015d210d288" + }, + { + "len": 172, + "seed": 173, + "hex": "e6ad069b9d53f537c36fd752420e7290b222dacf0f4460d5201549060a8ef8a9" + }, + { + "len": 173, + "seed": 174, + "hex": "a34178a31b392d28f0e7510ccc5c979c9a95d05a0c1073c526fcdc9b50aad0be" + }, + { + "len": 174, + "seed": 175, + "hex": "4f1f1f02a9bc0823505f47658f4aa9f9c9be2c917b3534b282f03f96a18628c9" + }, + { + "len": 175, + "seed": 176, + "hex": "86bab8facc6261dd55cdcba4c10d05dbac0ca15c2a7f9783657629326dfaf63a" + }, + { + "len": 176, + "seed": 177, + "hex": "fece28702781d07e1bf246a6f3917ede52d4b55b971cb9d9b50a88be3a0940c1" + }, + { + "len": 177, + "seed": 178, + "hex": "c77a1bf67fd92452a8629e9e602bc08ca5afe81c3a57192130701bd1f5289c80" + }, + { + "len": 178, + "seed": 179, + "hex": "8d1adb82b524a9d80937ca61c7f92a81c76dd5fa382cfef7735ab669a2c49f21" + }, + { + "len": 179, + "seed": 180, + "hex": "6995df2e41902b0524265e32f7c0915148b6a50a82f307b5cc7ee759712bc65a" + }, + { + "len": 180, + "seed": 181, + "hex": "927c84984b9ae3a050a3335df763a37bf574ee0795157f357eb02e72390dd656" + }, + { + "len": 181, + "seed": 182, + "hex": "f9f6f777aacecba62c29829305ec114135f131446773342d65d28daa507f82a1" + }, + { + "len": 182, + "seed": 183, + "hex": "80fb1f8af2aad6660ec63cd18d281adc06f45a13c85647e864c9db495475532c" + }, + { + "len": 183, + "seed": 184, + "hex": "86bbb956e5e12395d3b98b77862ab675a603077e013412f0c81f37c7f5f95872" + }, + { + "len": 184, + "seed": 185, + "hex": "1f016f5c66ca5a3ceed449b44d804de7f708e4d4ddff306a48d376d48ec15f21" + }, + { + "len": 185, + "seed": 186, + "hex": "381bd6aa9895b987ed8f37d65d9d1ca56f5cdc21a251ac22fcf3440ce6cd2373" + }, + { + "len": 186, + "seed": 187, + "hex": "be0fafdfbe5675c2ed4b33e440cd56b00f525c7ec1060aea577abf8b2f440842" + }, + { + "len": 187, + "seed": 188, + "hex": "c4c525872e3fe89f94c90e61e259b01a17143acfa98ea6ea9ffe254c31dc4429" + }, + { + "len": 188, + "seed": 189, + "hex": "ee46e10986d5e8002b4cdb0592c212df125a5fc26703ac73f8d163c6fc4f74ad" + }, + { + "len": 189, + "seed": 190, + "hex": "6b7c449c3c4462d51f8c0235d82bc1d43622767cfc8878a567c74872eec96856" + }, + { + "len": 190, + "seed": 191, + "hex": "d373e279a9a9cf091aaa65365fa8a72725bbcd8f3cca22af3a72d15537d92bd6" + }, + { + "len": 191, + "seed": 192, + "hex": "f148db0875cfd36dbc8041c2e5ec2e632f34b7becb4359d8c0e00f4cf6afce4c" + }, + { + "len": 192, + "seed": 193, + "hex": "9d855049eb39fef0cbc9bf94ae1dd5ccf7f245f4a11f68e2550c7edf602a81f1" + }, + { + "len": 193, + "seed": 194, + "hex": "cd0c289a38aaf3cf9972a0001d3b94a4d0415d10bc414b39e1784920c92c518f" + }, + { + "len": 194, + "seed": 195, + "hex": "b5c3e26e5f4f351f00b16625b3738a8b4356ac4ff8d08da787633f4d4cd75bfd" + }, + { + "len": 195, + "seed": 196, + "hex": "1cb69e639d584a5cd5337803d2c018c5b63b110b316ca849d3535730119ec0f5" + }, + { + "len": 196, + "seed": 197, + "hex": "3d010c756c67cd3dbec81721619537ff41a190e6b18bfb491aa40b08153d564b" + }, + { + "len": 197, + "seed": 198, + "hex": "9bfc485ff7ece41f15bdfe3d5828ed8e1aade5b02e505f8e8c5be8bb3b90304f" + }, + { + "len": 198, + "seed": 199, + "hex": "0c5d5eb0b41836a5f53517c082450143eac59cd79ed6c43b51ae51ce4f7cd34f" + }, + { + "len": 199, + "seed": 200, + "hex": "c6b50f5474ef8de46d23775e3c43e7b4fd7e75cca6c5f9ee0a3c8299ffda6b9c" + }, + { + "len": 200, + "seed": 201, + "hex": "0e768696d633d7fe577f37b3bc6c6fe4f69b58deba0bd385cb18ca84066c5226" + }, + { + "len": 1000, + "seed": 1001, + "hex": "7c5fe8aa3a3570468d239578fadc553214c1c43e88c427eecc1aad12499df9a1" + }, + { + "len": 4096, + "seed": 4097, + "hex": "12e8f7728691115fd4d908bbcfaa0458acfb6ef9bf275cacb3c2255066f95022" + }, + { + "len": 65536, + "seed": 65537, + "hex": "03a1c878361d454763c421f48df1d45e5305cbe1790ee714228c5f8a2de71226" + }, + { + "len": 1048576, + "seed": 1048577, + "hex": "64e194c2184733175d87f915d3ece2fbd3e68f7954bb959e6bd66c01a99b6b1f" + } +] + +export const SHA256_PUBLISHED: readonly { text: string; hex: string }[] = [ + { + "text": "", + "hex": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "text": "abc", + "hex": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + }, + { + "text": "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "hex": "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" + }, + { + "text": "'a' x 1000000", + "hex": "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0" + } +] + +export const EXAMPLE_GRAPH_DIGESTS: readonly { file: string; graph: boolean; modelVersion?: number; semanticDigest?: string; contentDigest?: string; fullContentDigest?: string }[] = [ + { + "file": "coffee-roastery.json", + "graph": true, + "modelVersion": 2, + "semanticDigest": "5b174d6a54bdcbf1c60e5651ec47031b4905b4cf99c0bb7305d2b875e22e113e", + "contentDigest": "a9d63f3b7562e88a7dc842a5966085839c870e79621ad8d70e60cbb8b9c82c87", + "fullContentDigest": "a9d63f3b7562e88a7dc842a5966085839c870e79621ad8d70e60cbb8b9c82c87" + }, + { + "file": "deadlock.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "4eae1a59299a8f7f54ec5ae07f87b4c5980bff829f578ad32077ce76dcca25f3", + "contentDigest": "d944dd4bb1bd26ed3a90ec2b3390d938d36e52b33516a69d097370b2208366cc", + "fullContentDigest": "d944dd4bb1bd26ed3a90ec2b3390d938d36e52b33516a69d097370b2208366cc" + }, + { + "file": "engine-b-verification.expected.json", + "graph": false + }, + { + "file": "engine-b-verification.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "8556468a2cf5f9e84991152c5dfa4abdb3f2473ca72e6d1eea26020ada41c908", + "contentDigest": "39471122530d9b004873fff59e3fa30dad2a6b8bca79e5b7eacc635b9a5dd07a", + "fullContentDigest": "39471122530d9b004873fff59e3fa30dad2a6b8bca79e5b7eacc635b9a5dd07a" + }, + { + "file": "equilibrium.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "1a929be61853177d97a81f0d68a18628919ae6f2581d5802b718d3bc5499fbb6", + "contentDigest": "10711fd761d4f739fe9a797b46275b44e1f85fef48c751238c55cc7e74ce4826", + "fullContentDigest": "10711fd761d4f739fe9a797b46275b44e1f85fef48c751238c55cc7e74ce4826" + }, + { + "file": "gacha-banner-zones.json", + "graph": true, + "modelVersion": 2, + "semanticDigest": "04fd52f0da05e87fe2e6ba9359546ebb8f3b652c742a9420d03e0eb89c9720e7", + "contentDigest": "699cfd4908353ca7d5c05e728780585c461d40854d28f1207abe5b72fb62503a", + "fullContentDigest": "699cfd4908353ca7d5c05e728780585c461d40854d28f1207abe5b72fb62503a" + }, + { + "file": "mmo-progression.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "2433dfdf4342924cdd9872fa87cc2b5bf33c0d7c5fa5196d016d97281cfcafaf", + "contentDigest": "d87e4307c6bce4361cbfcc6d1b40413773c0615633dec86be909a0c30651f746", + "fullContentDigest": "d87e4307c6bce4361cbfcc6d1b40413773c0615633dec86be909a0c30651f746" + }, + { + "file": "model-verification.expected.json", + "graph": false + }, + { + "file": "model-verification.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "3b3433867731ee7680d07f269512e2c1fa57cdcfba79d971b3f5d57e1cddfc9f", + "contentDigest": "5023c8ab7e80a6cf5c9966fa75adfc07d4991f3b723606da8a1724abeae01ce2", + "fullContentDigest": "5023c8ab7e80a6cf5c9966fa75adfc07d4991f3b723606da8a1724abeae01ce2" + }, + { + "file": "module-buffered-step.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "3c71687ec601d932d2ddacf14e686d330fb3dbc627641f11a9a98ffddd01ffee", + "contentDigest": "79ab1531c497075be0972c62a212519a4e23e770298cbf17a00e6a86767ffec2", + "fullContentDigest": "79ab1531c497075be0972c62a212519a4e23e770298cbf17a00e6a86767ffec2" + }, + { + "file": "module-reward-split.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "1e9d5ad8a18c3269a0ebe637a243fa05903a22e95447e82350984e93d95d732d", + "contentDigest": "0538f40b5ffd39a2e66f637afae1ca770aad781838a0d733bdb5f3755d79b017", + "fullContentDigest": "0538f40b5ffd39a2e66f637afae1ca770aad781838a0d733bdb5f3755d79b017" + }, + { + "file": "playback-choreography.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "b004b576b25a41c8da2becfb79e84c49b6e5e90ae28e6f6f6e5521e1096c129c", + "contentDigest": "47342738c43a8bddbfdfe07cde0e2cfa5df68b744000060732e40c988c3b5c97", + "fullContentDigest": "47342738c43a8bddbfdfe07cde0e2cfa5df68b744000060732e40c988c3b5c97" + }, + { + "file": "risky-factory.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "41a21a02a1bd936160cf5db3d3e5064b13b65c968c4ffefdbcef233fa9fd4641", + "contentDigest": "76f4a6a98749fbd93bc3ec2c22d8b3d7aa990b1e905fb8064aa4c8c0d83e792b", + "fullContentDigest": "76f4a6a98749fbd93bc3ec2c22d8b3d7aa990b1e905fb8064aa4c8c0d83e792b" + }, + { + "file": "state-verification.expected.json", + "graph": false + }, + { + "file": "state-verification.json", + "graph": true, + "modelVersion": 1, + "semanticDigest": "51adc0ac64a74c52daf87467905efaac40ee3a0e4d3af99c4d2b0f1bff0e9314", + "contentDigest": "bc2afa9ef52e5570e5da894fc4b7ef8cb83916b126a8949030f8b3f20d2284a3", + "fullContentDigest": "bc2afa9ef52e5570e5da894fc4b7ef8cb83916b126a8949030f8b3f20d2284a3" + } +] + +export const EXAMPLE_REVISION_READS: readonly Record[] = [ + { + "file": "revision/base.revision.json", + "ok": true, + "stage": null, + "storedContentDigest": "e490f996091cbb9e3875f83669e369240f882540421b141f6aa43db093da7617", + "storedBaseDigest": null, + "sideDigest": "e490f996091cbb9e3875f83669e369240f882540421b141f6aa43db093da7617", + "projectContentDigest": null, + "proposalBaseDigest": null, + "sideMatchesStored": true + }, + { + "file": "revision/proposal.clean.json", + "ok": true, + "stage": null, + "storedContentDigest": "cb6b4c226cdffe1a2009394a50e539c91057a88b522f3b426c904e372e9f577d", + "storedBaseDigest": "e490f996091cbb9e3875f83669e369240f882540421b141f6aa43db093da7617", + "sideDigest": "cb6b4c226cdffe1a2009394a50e539c91057a88b522f3b426c904e372e9f577d", + "projectContentDigest": null, + "proposalBaseDigest": "e490f996091cbb9e3875f83669e369240f882540421b141f6aa43db093da7617", + "sideMatchesStored": true + }, + { + "file": "revision/proposal.structural.json", + "ok": true, + "stage": null, + "storedContentDigest": "891f76715613fed6718f97cf2c41548e3f196b7d724c9baea6c7e6387379ee1a", + "storedBaseDigest": "e490f996091cbb9e3875f83669e369240f882540421b141f6aa43db093da7617", + "sideDigest": "891f76715613fed6718f97cf2c41548e3f196b7d724c9baea6c7e6387379ee1a", + "projectContentDigest": null, + "proposalBaseDigest": "e490f996091cbb9e3875f83669e369240f882540421b141f6aa43db093da7617", + "sideMatchesStored": true + }, + { + "file": "revision-legacy-v0.10.0/LP0.json", + "ok": true, + "stage": null, + "storedContentDigest": "1569a0853058e20fdb14997a197ec8908ee60db09335d73d35d20618e7790d41", + "storedBaseDigest": "76d9ad5c3679a3b8feabe090fda28a76fdc8c081165ec38fdab1282b20ef0906", + "sideDigest": "1569a0853058e20fdb14997a197ec8908ee60db09335d73d35d20618e7790d41", + "projectContentDigest": null, + "proposalBaseDigest": "76d9ad5c3679a3b8feabe090fda28a76fdc8c081165ec38fdab1282b20ef0906", + "sideMatchesStored": true + }, + { + "file": "revision-legacy-v0.10.0/LP1.json", + "ok": true, + "stage": null, + "storedContentDigest": "ed58f6fb4425479a47bf2cf1216b56f7d71dc259f0f979e77eb406e5d091c890", + "storedBaseDigest": "76d9ad5c3679a3b8feabe090fda28a76fdc8c081165ec38fdab1282b20ef0906", + "sideDigest": "ed58f6fb4425479a47bf2cf1216b56f7d71dc259f0f979e77eb406e5d091c890", + "projectContentDigest": null, + "proposalBaseDigest": "76d9ad5c3679a3b8feabe090fda28a76fdc8c081165ec38fdab1282b20ef0906", + "sideMatchesStored": true + }, + { + "file": "revision-legacy-v0.10.0/LR0.json", + "ok": true, + "stage": null, + "storedContentDigest": "1569a0853058e20fdb14997a197ec8908ee60db09335d73d35d20618e7790d41", + "storedBaseDigest": null, + "sideDigest": "1569a0853058e20fdb14997a197ec8908ee60db09335d73d35d20618e7790d41", + "projectContentDigest": null, + "proposalBaseDigest": null, + "sideMatchesStored": true + }, + { + "file": "revision-v3/proposal.malformed-base.json", + "ok": false, + "stage": "project", + "storedContentDigest": "5b67acd2cc6329303ffb64967b545457fccccbdf4d1e589a146916cbeb11a0c3", + "storedBaseDigest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "file": "revision-v3/proposal.malformed-proposed.json", + "ok": true, + "stage": null, + "storedContentDigest": "5b67acd2cc6329303ffb64967b545457fccccbdf4d1e589a146916cbeb11a0c3", + "storedBaseDigest": "b068ddc5c93123f5740148bb5cf428488584cbbca8ded04a94ab1b08a9e58572", + "sideDigest": "5b67acd2cc6329303ffb64967b545457fccccbdf4d1e589a146916cbeb11a0c3", + "projectContentDigest": null, + "proposalBaseDigest": "b068ddc5c93123f5740148bb5cf428488584cbbca8ded04a94ab1b08a9e58572", + "sideMatchesStored": true + } +] diff --git a/src/model/sha256Baseline.test.ts b/src/model/sha256Baseline.test.ts new file mode 100644 index 00000000..76cc3777 --- /dev/null +++ b/src/model/sha256Baseline.test.ts @@ -0,0 +1,101 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { deserialize } from './serialize' +import { canonicalContent, digestOfCanonical, fullContentDigest, readRevisionSideAndProject } from './revision' +import { semanticDigest, sha256Hex, sha256Js } from './workspace' +import { + EXAMPLE_GRAPH_DIGESTS, + EXAMPLE_REVISION_READS, + SHA256_PUBLISHED, + SHA256_VECTORS, + baselineBytes, +} from './sha256Baseline.fixture' + +// Issue #301 - the hand-written SHA-256 was replaced by `@noble/hashes`. The +// fixture holds what the replaced code computed on main 44af9a4, recorded +// before the change: hash vectors, the digests of every example diagram, and +// what the import layer's reader made of every example revision and proposal +// file. The replacement must give the same bytes for all of it, because those +// digests are stored in people's workspace, revision and proposal files. + +// the example files' exact text, the way an import reads them +const EXAMPLE_TEXT = import.meta.glob('../../examples/**/*.json', { query: '?raw', import: 'default', eager: true }) as Record +const read = (rel: string): string => { + const text = EXAMPLE_TEXT['../../examples/' + rel] + if (text === undefined) throw new Error('missing example ' + rel) + return text +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('SHA-256 after the replacement, against what the replaced code computed', () => { + it('has the vectors it is meant to have', () => { + expect(SHA256_VECTORS).toHaveLength(205) + expect(SHA256_PUBLISHED).toHaveLength(4) + expect(EXAMPLE_GRAPH_DIGESTS.filter((g) => g.graph)).toHaveLength(12) + expect(EXAMPLE_REVISION_READS).toHaveLength(8) + }) + + it('every hash vector, lengths 0 to 200 and up to 1 MiB, is byte-identical', () => { + for (const v of SHA256_VECTORS) expect(sha256Js(baselineBytes(v.len, v.seed)), `length ${v.len}`).toBe(v.hex) + }) + + it('the published vectors still hold', () => { + const text = (t: string) => (t.startsWith("'a' x ") ? 'a'.repeat(Number(t.slice(6))) : t) + for (const p of SHA256_PUBLISHED) expect(sha256Js(new TextEncoder().encode(text(p.text)))).toBe(p.hex) + }) + + it('Web Crypto and the synchronous path agree', async () => { + for (const v of SHA256_VECTORS.filter((x) => x.len % 17 === 0 || x.len > 200)) { + expect(await sha256Hex(baselineBytes(v.len, v.seed)), `length ${v.len}`).toBe(v.hex) + } + }) + + it('without Web Crypto, sha256Hex takes the synchronous path and gives the same bytes', async () => { + vi.stubGlobal('crypto', {}) + for (const v of SHA256_VECTORS.filter((x) => x.len % 23 === 0)) { + expect(await sha256Hex(baselineBytes(v.len, v.seed)), `length ${v.len}`).toBe(v.hex) + } + }) +}) + +describe('the digests of real documents are unchanged', () => { + for (const g of EXAMPLE_GRAPH_DIGESTS.filter((x) => x.graph)) { + it(`examples/${g.file}: semantic, content and full-content digests`, async () => { + const p = deserialize(read(g.file)) + const doc = { nodes: p.nodes, edges: p.edges, recommendedRunConfig: p.recommendedRunConfig, frames: p.frames, dataImports: p.dataImports } + expect(p.modelVersion).toBe(g.modelVersion) + expect(await semanticDigest({ nodes: p.nodes, edges: p.edges }, p.modelVersion)).toBe(g.semanticDigest) + expect(digestOfCanonical(canonicalContent(doc, { modelVersion: p.modelVersion }))).toBe(g.contentDigest) + expect(await fullContentDigest(doc, p.modelVersion)).toBe(g.fullContentDigest) + }) + } + + for (const r of EXAMPLE_REVISION_READS) { + it(`examples/${String(r.file)}: the import reader gives the same outcome`, () => { + const text = read(String(r.file)) + const obj = JSON.parse(text) as { project: { contentDigest?: string; base?: { contentDigest?: string } } } + const p = deserialize(text) + const out = readRevisionSideAndProject( + { nodes: p.nodes, edges: p.edges, recommendedRunConfig: p.recommendedRunConfig, frames: p.frames, dataImports: p.dataImports, rawDataImportSignal: p.hasRawDataImportSignal }, + obj.project, + p.modelVersion, + ) + const row: Record = { + file: r.file, + ok: out.ok, + stage: out.ok ? null : ((out as { stage?: string }).stage ?? null), + storedContentDigest: obj.project.contentDigest ?? null, + storedBaseDigest: obj.project.base?.contentDigest ?? null, + } + if (out.ok) { + row.sideDigest = digestOfCanonical(out.side.content) + row.projectContentDigest = out.project.contentDigest ?? null + row.proposalBaseDigest = out.proposalBase?.contentDigest ?? null + row.sideMatchesStored = row.sideDigest === row.storedContentDigest + } + expect(row).toEqual(r) + }) + } +}) diff --git a/src/model/workspace.ts b/src/model/workspace.ts index d4a60545..d38a794f 100644 --- a/src/model/workspace.ts +++ b/src/model/workspace.ts @@ -1,11 +1,12 @@ // Workspace Export / Import — pure format layer (SEMANTICS-W.md, loop-workspace/1). // // Slice A: constants, the canonical *semantic* graph digest (§W3.1 / §W11), a -// Web-Crypto-or-pure-JS SHA-256, a UTF-8 byte length. +// Web-Crypto-or-@noble/hashes SHA-256, a UTF-8 byte length. // Slice B: `buildWorkspacePayload` (assemble) and `readWorkspace` (the §W5 // defensive reader). Still store-free and UI-free — the store wiring lives in // `src/store/workspaceIO.ts`. +import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js' import { MAX_SERIES } from './limits' import type { LoopEdge, LoopNode } from './types' @@ -163,8 +164,9 @@ export function canonicalGraphString( /** * §W3.1 — SHA-256 (lowercase hex) of the canonical, id-sorted, engine-relevant * projection of the graph. Cross-verified against Web Crypto on standard vectors - * in the tests; the pure-JS path (`sha256Js`) is used where `crypto.subtle` is - * absent (some `file://` contexts). + * in the tests; the synchronous path (`sha256Js`, `@noble/hashes`) is used where + * `crypto.subtle` is absent (a non-secure context such as plain `http://` on a + * non-local host). */ export async function semanticDigest( graph: { nodes: LoopNode[]; edges: LoopEdge[] }, @@ -186,7 +188,7 @@ export function utf8ByteLength(s: string): number { const toHex = (bytes: Uint8Array): string => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') -/** Web Crypto when available, else the pure-JS fallback. */ +/** Web Crypto when available, else `sha256Js` (`@noble/hashes`). */ export async function sha256Hex(bytes: Uint8Array): Promise { const subtle = globalThis.crypto?.subtle if (subtle) { @@ -194,79 +196,18 @@ export async function sha256Hex(bytes: Uint8Array): Promise { const buf = await subtle.digest('SHA-256', bytes as unknown as ArrayBuffer) return toHex(new Uint8Array(buf)) } catch { - /* fall through to the pure-JS path */ + /* fall through to the synchronous path */ } } return sha256Js(bytes) } -/** FIPS 180-4 SHA-256 over `bytes` → lowercase hex. No Web Crypto dependency. */ +/** SHA-256 over `bytes` → lowercase hex, synchronously, with no Web Crypto + * dependency: `@noble/hashes` (issue #301 - the hand-written FIPS 180-4 code + * this replaced had no recorded origin). The synchronous revision digest + * (`digestOfCanonical`) and the fallback of `sha256Hex` both come here. */ export function sha256Js(bytes: Uint8Array): string { - // prettier-ignore - const K = new Uint32Array([ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, - ]) - const H = new Uint32Array([ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, - ]) - - const bitLen = bytes.length * 8 - const padded = new Uint8Array((((bytes.length + 8) >> 6) + 1) << 6) - padded.set(bytes) - padded[bytes.length] = 0x80 - const dv = new DataView(padded.buffer) - dv.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000), false) - dv.setUint32(padded.length - 4, bitLen >>> 0, false) - - const w = new Uint32Array(64) - const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n)) - - for (let off = 0; off < padded.length; off += 64) { - for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false) - for (let i = 16; i < 64; i++) { - const a = w[i - 15] - const b = w[i - 2] - const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3) - const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10) - w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0 - } - let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7] - for (let i = 0; i < 64; i++) { - const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25) - const ch = (e & f) ^ (~e & g) - const t1 = (h + S1 + ch + K[i] + w[i]) | 0 - const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22) - const maj = (a & b) ^ (a & c) ^ (b & c) - const t2 = (S0 + maj) | 0 - h = g - g = f - f = e - e = (d + t1) | 0 - d = c - c = b - b = a - a = (t1 + t2) | 0 - } - H[0] = (H[0] + a) | 0 - H[1] = (H[1] + b) | 0 - H[2] = (H[2] + c) | 0 - H[3] = (H[3] + d) | 0 - H[4] = (H[4] + e) | 0 - H[5] = (H[5] + f) | 0 - H[6] = (H[6] + g) | 0 - H[7] = (H[7] + h) | 0 - } - - let out = '' - for (let i = 0; i < 8; i++) out += (H[i] >>> 0).toString(16).padStart(8, '0') - return out + return toHex(nobleSha256(bytes)) } // ════════════════════════════════════════════════════════════════════════