You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/src/lib/inspector/report.ts

286 lines
9.8 KiB

// The page model of one inspection: every value the inspector shows, built
// from the library's Inspection with no DOM and no clock, so that it can be
// tested without a browser.
import {
canonicalJSON,
chainHashHex,
compactDateKey,
DKC_PRELUDE_SIZE,
type ErrorCode,
type Extension,
type ExtensionRegistry,
Read capsule format 2 of spec v0.9 Syncs testdata with datekeys-go at spec-v0.9 (7e2d83c) and moves the reader to the DateKeys Protocol Specification v0.9. Both capsule formats are read; a format 1 capsule keeps the verdict v0.8.2 gave it. - framing: the VERSION of the prelude is the capsule format, 1 or 2 (Prelude.format, FORMAT_1, FORMAT_2, isFormat). - control: decodeControl and encodeControl take the format; schema version 2 adds payload_length (8 bytes, at most L_MAX) and padding (1 or 2). - padding.ts: the rules bloque256 and reforzado of spec §29.1, exact up to L_MAX with BigInt bit lengths and ceil roundings, and the length of PAYLOAD_AGE. - open: exactly 16 stanzas in INNER_ACCESS_AGE of format 2 (step 12), P at step 16, and at step 17 a plaintext of exactly P bytes whose padding is zero; only the first L bytes are delivered, never the padding. Step 17 is recorded when it passes, and step 18 gives the bytes of content, as the reference does. Opened reports the format, L and, in format 2, the rule and P. - inspect: the JSON view carries format, as datekeys inspect -json. - The page shows the format, warns about format 1, and gives the padding rule and P once a format 2 capsule opens. Tests: the twelve fixtures, the 125 mutation cases through open from memory and from a Blob, the 4380 differential cases, padding.json, the format 2 CBOR vectors, and padding.test.ts against a BigInt statement of §29.1. The error texts of the 125 corpus cases were compared with capsule.Open at spec-v0.9. ibe-vectors.json gains the seven format 2 fixtures from scripts/ibe-go-vectors.go; its frozen values are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
type Format,
formatRFC3339,
type Inspection,
inspectView,
type InspectView,
policyName,
STANZA_TLOCK,
type StanzaInfo,
toHex,
Phase 2, step 8: the open action of /inspect After steps 1 to 8, a valid capsule whose date has passed on the device clock can be opened in the page: steps 9 to 18 of spec section 63 with open, loaded on demand with a dynamic import (opener.ts), so noble and age-encryption stay out of the first load of every page. - The release is supplied directly by the person (spec 63, step 10): drand's JSON answer or the bare signature, pasted after opening the drand URL the page links to, or the release in the record of an official fixture. The page never fetches it and reads only its round and signature (spec 11, 13). The CSP is unchanged. - time_and_key credentials: a .dkk (readAccessKey reads at most 12 bytes + 16 MiB + 1) or age identities, one per line. - The plaintext of the person's own file goes to a temporary OPFS file (tempfile.ts), committed only after step 18 (spec 56), offered for download and deleted on request, with another capsule, on pagehide and, if left over, on the next visit. One directory and one Web Lock per tab keep other tabs' clean-up away from files in use. Without OPFS, or when the browser refuses it, capsules up to 64 MiB open in memory. An opening in progress stops when another capsule is loaded. - opening.ts builds the page model of steps 9 to 18 as the reference records them; fixtures show their plaintext and compare its SHA-256 with their record. - licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts), the license of every package in the client bundle, Vite's and rolldown's runtime code, and the site's own license. check-build now fails if a notice is missing, or if a page loads noble, @scure/base or age-encryption with its first load. - The home page no longer says that the page never asks for keys. Checked in the browser on the production build: the time_only, time_and_key_portable (with its .dkk) and time_and_key_recipients (with a pasted identity) fixtures open with the SHA-256 of their records; a tampered signature fails at step 10 and a tampered STREAM chunk at step 17, with no download and no file left; an own file opens to OPFS, downloads without a CSP violation and is deleted with its lock; a left over directory goes on the next visit; no request leaves the origin. An adversarial review (four dimensions, each finding checked by a refuter) confirmed 15 findings, all fixed here. 2611 tests; coverage 100 % of the new modules, now a threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
type Unusable,
} from '../dkc/index.ts';
import { cborView, type Diagnostic } from './diagnostic.ts';
import { cliJSON, displayText, instantToEpochMs, INSPECT_STEPS, printableText, stepGloss, stepName } from './format.ts';
export type StepState = 'ok' | 'failed' | 'not-run';
/** One of the eight steps, as the CLI reports it, or not run. */
export interface StepRow {
readonly step: number;
/** The CLI name, for example "header validation". */
readonly name: string;
/** What the step checks, in Spanish. */
readonly gloss: string;
readonly state: StepState;
/** The pass detail or the error message; undefined when not run. */
readonly detail?: string;
/** The normative code of a failed step. */
readonly code?: ErrorCode;
}
/** A value read from the capsule next to the one the pinned profile expects. */
export interface Comparison {
readonly label: string;
readonly found: string | undefined;
readonly expected: string;
readonly match: boolean;
}
/** One PUBLIC_HEADER extension, with every view of its data. */
export interface ExtensionRow {
readonly critical: boolean;
/** extension_id as shown: as it is, or quoted and escaped. */
readonly id: string;
/** The identifier holds non-printable characters and is shown escaped. */
readonly idEscaped: boolean;
readonly version: number;
/** Whether this reader implements the extension. */
readonly known: boolean;
/** The data error of a known noncritical extension the reader cannot use. */
readonly unusable?: string;
/** Data length in bytes; 0 when the extension carries no data. */
readonly length: number;
/** Data in lowercase hexadecimal. */
readonly hex: string;
/** The data as text when it is valid printable UTF-8. */
readonly text?: string;
/** Informative diagnostic notation when the data is CBOR of the §58 profile. */
readonly cbor?: Diagnostic;
}
export interface CapsuleFacts {
readonly capsuleId: string;
readonly accessPolicy: string;
/** The compact dk1_ DateKey. */
readonly dateKey: string;
/** Its decoded payload, the canonical JSON of spec §18. */
readonly dateKeyJSON: string;
/** The network field of the DateKey: the profile_id. */
readonly network: string;
readonly round: number;
}
export interface ProfileFacts {
readonly id: string;
readonly provider: string;
readonly network: string;
readonly chainHash: string;
readonly period: number;
readonly genesis: string;
readonly scheme: string;
}
export interface PreludeFacts {
/** The 16 prelude bytes in hexadecimal. */
readonly hex: string;
readonly magic: string;
Read capsule format 2 of spec v0.9 Syncs testdata with datekeys-go at spec-v0.9 (7e2d83c) and moves the reader to the DateKeys Protocol Specification v0.9. Both capsule formats are read; a format 1 capsule keeps the verdict v0.8.2 gave it. - framing: the VERSION of the prelude is the capsule format, 1 or 2 (Prelude.format, FORMAT_1, FORMAT_2, isFormat). - control: decodeControl and encodeControl take the format; schema version 2 adds payload_length (8 bytes, at most L_MAX) and padding (1 or 2). - padding.ts: the rules bloque256 and reforzado of spec §29.1, exact up to L_MAX with BigInt bit lengths and ceil roundings, and the length of PAYLOAD_AGE. - open: exactly 16 stanzas in INNER_ACCESS_AGE of format 2 (step 12), P at step 16, and at step 17 a plaintext of exactly P bytes whose padding is zero; only the first L bytes are delivered, never the padding. Step 17 is recorded when it passes, and step 18 gives the bytes of content, as the reference does. Opened reports the format, L and, in format 2, the rule and P. - inspect: the JSON view carries format, as datekeys inspect -json. - The page shows the format, warns about format 1, and gives the padding rule and P once a format 2 capsule opens. Tests: the twelve fixtures, the 125 mutation cases through open from memory and from a Blob, the 4380 differential cases, padding.json, the format 2 CBOR vectors, and padding.test.ts against a BigInt statement of §29.1. The error texts of the 125 corpus cases were compared with capsule.Open at spec-v0.9. ibe-vectors.json gains the seven format 2 fixtures from scripts/ibe-go-vectors.go; its frozen values are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
/**
* VERSION, the capsule format (spec §22): format 1 does not hide the
* number of credentials nor the exact length of the content (§55.2, §70).
*/
readonly format: Format;
readonly flags: string;
readonly reserved: string;
readonly publicHeaderLen: number;
readonly sealedControlLen: number;
/** Where PAYLOAD_AGE starts. */
readonly payloadOffset: number;
/** Bytes of PAYLOAD_AGE, when the file reaches its start. */
readonly payloadLength?: number;
}
export interface Report {
readonly fileName: string;
/** Size of the whole file. */
readonly size: number;
/** Bytes read for the inspection (see inspectedLength). */
readonly readLength: number;
readonly valid: boolean;
readonly failure?: { readonly step: number; readonly code: ErrorCode; readonly message: string };
/** Always the eight steps, in order. */
readonly steps: readonly StepRow[];
/** The view of `datekeys inspect -json`. */
readonly view: InspectView;
/** Its exact JSON text. */
readonly json: string;
readonly capsule?: CapsuleFacts;
readonly profile?: ProfileFacts;
/** The effective round time, once step 7 passed. */
readonly unlock?: { readonly rfc3339: string; readonly epochMs: number | undefined };
readonly prelude?: PreludeFacts;
readonly outerStanzas?: readonly StanzaInfo[];
readonly payloadStanzas?: readonly StanzaInfo[];
/** The tlock stanza arguments against the pinned profile. */
readonly tlock?: readonly Comparison[];
/** Critical extensions first, then noncritical; undefined until PUBLIC_HEADER decoded. */
readonly extensions?: readonly ExtensionRow[];
/**
* The public note of the header (spec §24.1): text of the creator that
* nobody has checked, or `unusable` when the header holds a note that
* breaks the rules of text, which is not shown. Undefined without a note.
*/
readonly note?: { readonly text: string } | { readonly unusable: true };
}
export interface ReportInput {
readonly fileName: string;
/** The bytes that were inspected: the file or its inspected prefix. */
readonly bytes: Uint8Array;
/** Size of the whole file. */
readonly size: number;
readonly inspection: Inspection;
/** The extension registry given to the inspection, if any. */
readonly extensions?: ExtensionRegistry;
}
const hex8 = (v: number): string => `0x${v.toString(16).padStart(2, '0')}`;
export function buildReport(input: ReportInput): Report {
const { inspection: r, bytes, size } = input;
const view = inspectView(r, input.fileName);
const out: {
-readonly [K in keyof Report]: Report[K];
} = {
fileName: input.fileName,
size,
readLength: bytes.length,
valid: r.error === undefined,
steps: steps(r),
view,
json: cliJSON(view),
};
if (r.error !== undefined) {
const last = r.checks.at(-1)!;
out.failure = { step: last.step, code: r.error.code, message: r.error.message };
}
if (r.prelude !== undefined && r.payloadOffset !== undefined) {
const p: { -readonly [K in keyof PreludeFacts]: PreludeFacts[K] } = {
hex: toHex(bytes.subarray(0, DKC_PRELUDE_SIZE)),
magic: 'DKC1',
Read capsule format 2 of spec v0.9 Syncs testdata with datekeys-go at spec-v0.9 (7e2d83c) and moves the reader to the DateKeys Protocol Specification v0.9. Both capsule formats are read; a format 1 capsule keeps the verdict v0.8.2 gave it. - framing: the VERSION of the prelude is the capsule format, 1 or 2 (Prelude.format, FORMAT_1, FORMAT_2, isFormat). - control: decodeControl and encodeControl take the format; schema version 2 adds payload_length (8 bytes, at most L_MAX) and padding (1 or 2). - padding.ts: the rules bloque256 and reforzado of spec §29.1, exact up to L_MAX with BigInt bit lengths and ceil roundings, and the length of PAYLOAD_AGE. - open: exactly 16 stanzas in INNER_ACCESS_AGE of format 2 (step 12), P at step 16, and at step 17 a plaintext of exactly P bytes whose padding is zero; only the first L bytes are delivered, never the padding. Step 17 is recorded when it passes, and step 18 gives the bytes of content, as the reference does. Opened reports the format, L and, in format 2, the rule and P. - inspect: the JSON view carries format, as datekeys inspect -json. - The page shows the format, warns about format 1, and gives the padding rule and P once a format 2 capsule opens. Tests: the twelve fixtures, the 125 mutation cases through open from memory and from a Blob, the 4380 differential cases, padding.json, the format 2 CBOR vectors, and padding.test.ts against a BigInt statement of §29.1. The error texts of the 125 corpus cases were compared with capsule.Open at spec-v0.9. ibe-vectors.json gains the seven format 2 fixtures from scripts/ibe-go-vectors.go; its frozen values are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
format: r.prelude.format,
flags: hex8(bytes[5]!),
reserved: `0x${toHex(bytes.subarray(6, 8))}`,
publicHeaderLen: r.prelude.publicHeaderLen,
sealedControlLen: r.prelude.sealedControlLen,
payloadOffset: r.payloadOffset,
};
if (size >= r.payloadOffset) p.payloadLength = size - r.payloadOffset;
out.prelude = p;
}
const h = r.header;
if (h !== undefined) {
out.capsule = {
capsuleId: toHex(h.capsuleId),
accessPolicy: policyName(h.policy),
dateKey: compactDateKey(h.dateKey),
dateKeyJSON: canonicalJSON(h.dateKey) ?? '',
network: h.dateKey.profileId,
round: h.dateKey.round,
};
out.extensions = [
Phase 2, step 8: the open action of /inspect After steps 1 to 8, a valid capsule whose date has passed on the device clock can be opened in the page: steps 9 to 18 of spec section 63 with open, loaded on demand with a dynamic import (opener.ts), so noble and age-encryption stay out of the first load of every page. - The release is supplied directly by the person (spec 63, step 10): drand's JSON answer or the bare signature, pasted after opening the drand URL the page links to, or the release in the record of an official fixture. The page never fetches it and reads only its round and signature (spec 11, 13). The CSP is unchanged. - time_and_key credentials: a .dkk (readAccessKey reads at most 12 bytes + 16 MiB + 1) or age identities, one per line. - The plaintext of the person's own file goes to a temporary OPFS file (tempfile.ts), committed only after step 18 (spec 56), offered for download and deleted on request, with another capsule, on pagehide and, if left over, on the next visit. One directory and one Web Lock per tab keep other tabs' clean-up away from files in use. Without OPFS, or when the browser refuses it, capsules up to 64 MiB open in memory. An opening in progress stops when another capsule is loaded. - opening.ts builds the page model of steps 9 to 18 as the reference records them; fixtures show their plaintext and compare its SHA-256 with their record. - licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts), the license of every package in the client bundle, Vite's and rolldown's runtime code, and the site's own license. check-build now fails if a notice is missing, or if a page loads noble, @scure/base or age-encryption with its first load. - The home page no longer says that the page never asks for keys. Checked in the browser on the production build: the time_only, time_and_key_portable (with its .dkk) and time_and_key_recipients (with a pasted identity) fixtures open with the SHA-256 of their records; a tampered signature fails at step 10 and a tampered STREAM chunk at step 17, with no download and no file left; an own file opens to OPFS, downloads without a CSP violation and is deleted with its lock; a left over directory goes on the next visit; no request leaves the origin. An adversarial review (four dimensions, each finding checked by a refuter) confirmed 15 findings, all fixed here. 2611 tests; coverage 100 % of the new modules, now a threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
...h.critical.map((e) => extensionRow(e, true, r.unusableExtensions, input.extensions)),
...h.noncritical.map((e) => extensionRow(e, false, r.unusableExtensions, input.extensions)),
];
}
const p = r.profile;
if (p !== undefined) {
out.profile = {
id: p.id,
provider: p.provider,
network: p.network,
chainHash: chainHashHex(p),
period: p.period,
genesis: formatRFC3339({ seconds: p.genesisTime, nanos: 0 }),
scheme: p.scheme,
};
}
if (r.unlockAt !== undefined) {
out.unlock = { rfc3339: formatRFC3339(r.unlockAt), epochMs: instantToEpochMs(r.unlockAt) };
}
if (r.outerStanzas !== undefined) out.outerStanzas = r.outerStanzas;
if (r.payloadStanzas !== undefined) out.payloadStanzas = r.payloadStanzas;
if (r.outerStanzas !== undefined && h !== undefined && p !== undefined) {
const t = r.outerStanzas.find((s) => s.type === STANZA_TLOCK);
if (t !== undefined) {
out.tlock = [
compare('Argumentos', String(t.args.length), '2'),
compare('Ronda', t.args[0], String(h.dateKey.round)),
compare('Cadena (chain hash)', t.args[1], chainHashHex(p)),
];
}
}
if (r.publicNote !== undefined) out.note = { text: r.publicNote };
else if (r.unusableNote === true) out.note = { unusable: true };
return out;
}
function compare(label: string, found: string | undefined, expected: string): Comparison {
return { label, found, expected, match: found === expected };
}
function steps(r: Inspection): StepRow[] {
return INSPECT_STEPS.map((step): StepRow => {
const c = r.checks.find((x) => x.step === step);
const base = { step, name: stepName(step), gloss: stepGloss(step) };
if (c === undefined) return { ...base, state: 'not-run' };
if (c.ok) return c.detail === undefined ? { ...base, state: 'ok' } : { ...base, state: 'ok', detail: c.detail };
const failed: { -readonly [K in keyof StepRow]: StepRow[K] } = { ...base, state: 'failed' };
if (c.detail !== undefined) failed.detail = c.detail;
if (r.error !== undefined) failed.code = r.error.code;
return failed;
});
}
Phase 2, step 8: the open action of /inspect After steps 1 to 8, a valid capsule whose date has passed on the device clock can be opened in the page: steps 9 to 18 of spec section 63 with open, loaded on demand with a dynamic import (opener.ts), so noble and age-encryption stay out of the first load of every page. - The release is supplied directly by the person (spec 63, step 10): drand's JSON answer or the bare signature, pasted after opening the drand URL the page links to, or the release in the record of an official fixture. The page never fetches it and reads only its round and signature (spec 11, 13). The CSP is unchanged. - time_and_key credentials: a .dkk (readAccessKey reads at most 12 bytes + 16 MiB + 1) or age identities, one per line. - The plaintext of the person's own file goes to a temporary OPFS file (tempfile.ts), committed only after step 18 (spec 56), offered for download and deleted on request, with another capsule, on pagehide and, if left over, on the next visit. One directory and one Web Lock per tab keep other tabs' clean-up away from files in use. Without OPFS, or when the browser refuses it, capsules up to 64 MiB open in memory. An opening in progress stops when another capsule is loaded. - opening.ts builds the page model of steps 9 to 18 as the reference records them; fixtures show their plaintext and compare its SHA-256 with their record. - licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts), the license of every package in the client bundle, Vite's and rolldown's runtime code, and the site's own license. check-build now fails if a notice is missing, or if a page loads noble, @scure/base or age-encryption with its first load. - The home page no longer says that the page never asks for keys. Checked in the browser on the production build: the time_only, time_and_key_portable (with its .dkk) and time_and_key_recipients (with a pasted identity) fixtures open with the SHA-256 of their records; a tampered signature fails at step 10 and a tampered STREAM chunk at step 17, with no download and no file left; an own file opens to OPFS, downloads without a CSP violation and is deleted with its lock; a left over directory goes on the next visit; no request leaves the origin. An adversarial review (four dimensions, each finding checked by a refuter) confirmed 15 findings, all fixed here. 2611 tests; coverage 100 % of the new modules, now a threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
/**
* The row of one extension of PUBLIC_HEADER, CONTROL_CBOR or a .dkk, given
* the known noncritical extensions of that object that the registry rejects.
*/
export function extensionRow(e: Extension, critical: boolean, unusable: readonly Unusable[], reg: ExtensionRegistry | undefined): ExtensionRow {
const id = displayText(e.id);
const row: { -readonly [K in keyof ExtensionRow]: ExtensionRow[K] } = {
critical,
id: id.text,
idEscaped: id.escaped,
version: e.version,
known: reg?.known(e.id, e.version) ?? false,
length: e.data?.length ?? 0,
hex: e.data === undefined ? '' : toHex(e.data),
};
Phase 2, step 8: the open action of /inspect After steps 1 to 8, a valid capsule whose date has passed on the device clock can be opened in the page: steps 9 to 18 of spec section 63 with open, loaded on demand with a dynamic import (opener.ts), so noble and age-encryption stay out of the first load of every page. - The release is supplied directly by the person (spec 63, step 10): drand's JSON answer or the bare signature, pasted after opening the drand URL the page links to, or the release in the record of an official fixture. The page never fetches it and reads only its round and signature (spec 11, 13). The CSP is unchanged. - time_and_key credentials: a .dkk (readAccessKey reads at most 12 bytes + 16 MiB + 1) or age identities, one per line. - The plaintext of the person's own file goes to a temporary OPFS file (tempfile.ts), committed only after step 18 (spec 56), offered for download and deleted on request, with another capsule, on pagehide and, if left over, on the next visit. One directory and one Web Lock per tab keep other tabs' clean-up away from files in use. Without OPFS, or when the browser refuses it, capsules up to 64 MiB open in memory. An opening in progress stops when another capsule is loaded. - opening.ts builds the page model of steps 9 to 18 as the reference records them; fixtures show their plaintext and compare its SHA-256 with their record. - licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts), the license of every package in the client bundle, Vite's and rolldown's runtime code, and the site's own license. check-build now fails if a notice is missing, or if a page loads noble, @scure/base or age-encryption with its first load. - The home page no longer says that the page never asks for keys. Checked in the browser on the production build: the time_only, time_and_key_portable (with its .dkk) and time_and_key_recipients (with a pasted identity) fixtures open with the SHA-256 of their records; a tampered signature fails at step 10 and a tampered STREAM chunk at step 17, with no download and no file left; an own file opens to OPFS, downloads without a CSP violation and is deleted with its lock; a left over directory goes on the next visit; no request leaves the origin. An adversarial review (four dimensions, each finding checked by a refuter) confirmed 15 findings, all fixed here. 2611 tests; coverage 100 % of the new modules, now a threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
const u = critical ? undefined : unusable.find((x) => x.id === e.id && x.version === e.version);
if (u !== undefined) row.unusable = u.error.message;
if (e.data !== undefined) {
const text = printableText(e.data);
if (text !== undefined) row.text = text;
const cbor = cborView(e.data);
if (cbor !== undefined) row.cbor = cbor;
}
return row;
}

Powered by TurnKey Linux.