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
< script lang = "ts" >
// The "abrir" action of the inspector (plan of phase 2, section 9): steps 9
// to 18 of spec §63 on a capsule that passed steps 1 to 8, with the release
// the person supplies directly, pasted from drand or taken from the record
// of an official fixture (decision 4: the page never fetches it), and the
// credentials that time_and_key asks for. The opening code, with noble and
// age-encryption, is imported on demand, so the page's first load does not
// carry it.
//
// The plaintext of a fixture is opened in memory. The plaintext of the
// person's own file goes to a private temporary file of the browser (OPFS,
// tempfile.ts), committed only after step 18 (spec §56), offered for
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
// download and deleted on request, when another capsule is opened or
// loaded, and when the page is left; in memory, up to MEMORY_LIMIT, when
// the browser has no such file or refuses it. Once the capsule opened, the
// start of the plaintext is shown when it is text (plaintextPreview). An
// opening in progress stops when the panel is destroyed, and its file is
// removed.
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
import { onDestroy , tick } from 'svelte';
import { toHex } from '$lib/dkc/index.ts';
import type { Fixture } from '$lib/inspector/fixtures.ts';
import { errorGloss , escapeInvisible , formatByteCount , formatInteger } from '$lib/inspector/format.ts';
Put the content first on opening, and name its download by its content
When a capsule opens, /inspect now shows its content right under the
verdict, before steps 9 to 18. A capsule does not keep the name of the
file it seals (spec §6, §55.2), and the capsula-<date>.dkc of /create
has no extension of its own, so the download was nameless for the
system: contentExtension now gives it .txt for a text, or the
extension of a common type of file by its first bytes (.pdf, .png,
.jpg, .zip…).
vite preview served the pages without Cache-Control, and a tab
reloaded after a build could keep the old page, whose chunks are gone;
a small plugin, before SvelteKit's, has the pages revalidated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
import { buildOpenReport , contentExtension , type OpenReport , plaintextFileName , plaintextPreview , SHOWN_TEXT } from '$lib/inspector/opening.ts';
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
import { drandReleaseURL , parseReleaseText , releaseText } from '$lib/inspector/release-input.ts';
import type { Report } from '$lib/inspector/report.ts';
import { browserPlatform , cancellable , createTempFile , freeSpace , type TempFile } from '$lib/inspector/tempfile.ts';
import ExtensionList from './ExtensionList.svelte';
import StepList from './StepList.svelte';
let {
report,
capsule,
fixture,
nowMs,
}: {
/** The inspection of the capsule: valid, with its round and profile. */
report: Report;
/** What was inspected: the bytes of a fixture or the person's file. */
capsule: Uint8Array | Blob;
/** The official fixture, when it is one. */
fixture: Fixture | undefined;
/** When the report was made. */
nowMs: number;
} = $props();
/** The largest plaintext opened in memory when the browser has no OPFS. */
const MEMORY_LIMIT = 64 < < 20 ;
interface Result {
readonly report: OpenReport;
readonly ms: number;
/** The plaintext offered for download: a temporary file, or memory. */
readonly download?: { readonly url : string ; readonly name : string ; readonly size : number ; readonly temporary : boolean } ;
}
// Read once: the panel is made again for each inspected capsule.
const initial = (): { round : number ; release : string } => ({
round: report.capsule!.round,
release: fixture?.release === undefined ? '' : releaseText(fixture.release.round, toHex(fixture.release.signature)),
});
const { round , release : initialRelease } = initial();
/** The input a problem is about. */
type ProblemField = 'release' | 'identities' | 'accessKey';
const FIELD_IDS: Readonly< Record < ProblemField , string > > = {
release: 'release-input',
identities: 'ids-input',
accessKey: 'dkk-input',
};
let releaseInput = $state(initialRelease);
let identities = $state('');
let accessKey: File | undefined = $state();
let problem: string | undefined = $state();
let problemField: ProblemField | undefined = $state();
let busy = $state(false);
let result: Result | undefined = $state();
let deleted = $state(false);
let announcement = $state('');
const timeAndKey = $derived(report.capsule?.accessPolicy === 'time_and_key');
const due = $derived(report.unlock?.epochMs !== undefined & & report.unlock.epochMs < = nowMs);
const drandURL = $derived(drandReleaseURL(report.profile!.chainHash, round));
const payloadLength = $derived(report.prelude?.payloadLength ?? 0);
// The temporary file and the object URL of the last opening.
let temp: TempFile | undefined;
let objectURL: string | undefined;
// The temporary file of the opening in progress, until it becomes `temp`
// or is removed: discard() removes it too, so a panel destroyed or a page
// left in the middle of an opening leaves nothing behind.
let pending: TempFile | undefined;
// Only the latest opening may show its result; destroying the panel
// invalidates the one in progress, which then stops writing.
let openId = 0;
async function discard(): Promise< void > {
if (objectURL !== undefined) URL.revokeObjectURL(objectURL);
objectURL = undefined;
const files = [temp, pending];
temp = undefined;
pending = undefined;
await Promise.all(files.map((f) => f?.remove()));
}
onDestroy(() => {
openId++;
void discard();
});
// pagehide also fires when the page goes into the back/forward cache: if
// it comes back, it must not offer what was deleted here.
function leave(): void {
if (result?.download !== undefined) deleted = true;
void discard();
}
async function deleteNow(): Promise< void > {
await discard();
deleted = true;
announcement = 'Fichero temporal borrado.';
}
function onAccessKey(event: Event & { currentTarget : HTMLInputElement } ): void {
accessKey = event.currentTarget.files?.[0];
}
async function submit(event: SubmitEvent): Promise< void > {
event.preventDefault();
if (busy) return;
problem = undefined;
problemField = undefined;
const parsed = parseReleaseText(releaseInput, round);
if (!parsed.ok) {
await fail(parsed.problem, 'release');
return;
}
const id = ++openId;
const stale = (): boolean => id !== openId;
busy = true;
result = undefined;
deleted = false;
announcement = 'Abriendo la cápsula.';
await discard();
// This opening's temporary file, until it becomes `temp`: the finally
// block removes it on every other path.
let t: TempFile | undefined;
try {
const opener = await import('$lib/inspector/opener.ts');
if (stale()) return;
if (fixture === undefined) {
const platform = browserPlatform();
let free: number | undefined;
if (platform !== undefined) {
try {
free = await freeSpace(platform);
if (free === undefined || free >= payloadLength) t = pending = await createTempFile(platform);
} catch {
// The OPFS is there but the browser refuses it (a private window,
// site data blocked): the capsule opens in memory instead.
}
if (stale()) return;
}
if (t === undefined && payloadLength > MEMORY_LIMIT) {
await fail(
free !== undefined & & free < payloadLength
? `El navegador deja ${ formatByteCount ( free )} libres para esta página y el contenido cifrado ocupa ${ formatByteCount ( payloadLength )} , más de los ${ formatByteCount ( MEMORY_LIMIT )} que la página abre en memoria. Libera espacio o usa la CLI (datekeys decrypt).`
: `Este navegador no deja a la página un fichero temporal privado (OPFS) y el contenido cifrado ocupa ${ formatByteCount ( payloadLength )} , más de los ${ formatByteCount ( MEMORY_LIMIT )} que la página abre en memoria. Usa otro navegador o la CLI (datekeys decrypt).`,
);
return;
}
}
const out = t;
const attempt = await opener.openCapsule({
capsule,
release: parsed.release,
...(timeAndKey ? { identities } : {} ),
...(timeAndKey && accessKey !== undefined ? { accessKey } : {} ),
// A stale opening stops writing, so open aborts the file and stops.
...(out === undefined ? {} : { output : { writable : cancellable ( out . writable , stale ), file : () => out . file (), remove : () => out . remove () } } ),
});
if (stale()) {
if (attempt.ok) attempt.plaintext?.fill(0);
return;
}
if (!attempt.ok) {
await fail(attempt.problem, attempt.field);
return;
}
const opened = attempt.opened.error === undefined;
let shown: { text : string ; cut : boolean } | undefined;
Put the content first on opening, and name its download by its content
When a capsule opens, /inspect now shows its content right under the
verdict, before steps 9 to 18. A capsule does not keep the name of the
file it seals (spec §6, §55.2), and the capsula-<date>.dkc of /create
has no extension of its own, so the download was nameless for the
system: contentExtension now gives it .txt for a text, or the
extension of a common type of file by its first bytes (.pdf, .png,
.jpg, .zip…).
vite preview served the pages without Cache-Control, and a tab
reloaded after a build could keep the old page, whose chunks are gone;
a small plugin, before SvelteKit's, has the pages revalidated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// The extension of the download, from the content: the capsule does
// not keep the name of the file it seals.
let ext = '';
if (opened && attempt.preview !== undefined) {
shown = plaintextPreview(attempt.preview.bytes, attempt.preview.whole);
Put the content first on opening, and name its download by its content
When a capsule opens, /inspect now shows its content right under the
verdict, before steps 9 to 18. A capsule does not keep the name of the
file it seals (spec §6, §55.2), and the capsula-<date>.dkc of /create
has no extension of its own, so the download was nameless for the
system: contentExtension now gives it .txt for a text, or the
extension of a common type of file by its first bytes (.pdf, .png,
.jpg, .zip…).
vite preview served the pages without Cache-Control, and a tab
reloaded after a build could keep the old page, whose chunks are gone;
a small plugin, before SvelteKit's, has the pages revalidated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
ext = contentExtension(attempt.preview.bytes, shown !== undefined);
attempt.preview.bytes.fill(0);
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 r = buildOpenReport(
attempt.opened,
attempt.digest === undefined
? undefined
: {
...attempt.digest,
...(fixture?.plaintextSHA256 === undefined ? {} : { expectedSHA256 : fixture.plaintextSHA256 } ),
...(shown === undefined ? {} : { text : shown.text , textCut : shown.cut } ),
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
},
);
let download: Result['download'];
if (opened && fixture === undefined) {
Put the content first on opening, and name its download by its content
When a capsule opens, /inspect now shows its content right under the
verdict, before steps 9 to 18. A capsule does not keep the name of the
file it seals (spec §6, §55.2), and the capsula-<date>.dkc of /create
has no extension of its own, so the download was nameless for the
system: contentExtension now gives it .txt for a text, or the
extension of a common type of file by its first bytes (.pdf, .png,
.jpg, .zip…).
vite preview served the pages without Cache-Control, and a tab
reloaded after a build could keep the old page, whose chunks are gone;
a small plugin, before SvelteKit's, has the pages revalidated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
const name = plaintextFileName(report.fileName, ext);
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
if (t !== undefined) {
const file = await t.file();
if (stale()) return;
objectURL = URL.createObjectURL(file);
temp = t;
pending = undefined;
t = undefined;
download = { url : objectURL , name , size : file.size , temporary : true } ;
} else {
const blob = new Blob([attempt.plaintext! as Uint8Array< ArrayBuffer > ]);
objectURL = URL.createObjectURL(blob);
download = { url : objectURL , name , size : blob.size , temporary : false } ;
}
}
attempt.plaintext?.fill(0);
result = download === undefined ? { report : r , ms : attempt.ms } : { report : r , ms : attempt.ms , download } ;
announcement = r.opened
? 'Cápsula abierta: pasos 9 a 18 superados.'
: `Apertura rechazada en el paso ${ r . failure ! . step } , ${ r . failure ! . code } .`;
await tick();
document.getElementById('open-result-title')?.focus();
} catch (err) {
if (!stale()) await fail(`No se pudo abrir: ${ escapeInvisible ( err instanceof Error ? err.message : String ( err ))} `);
} finally {
// A file that did not become `temp`: a failure, or a stale opening.
if (t !== undefined) {
if (pending === t) pending = undefined;
await t.remove();
}
if (!stale()) busy = false;
}
}
// Shows why the opening could not run, announces it and moves the focus to
// the field at fault, or to the message: the submit button that had it is
// disabled while the opening runs.
async function fail(message: string, field?: ProblemField): Promise< void > {
problem = message;
problemField = field;
announcement = '';
await tick();
announcement = message;
document.getElementById(field === undefined ? 'open-problem' : FIELD_IDS[field])?.focus();
}
function seconds(ms: number): string {
return new Intl.NumberFormat('es-ES', { maximumFractionDigits : 2 } ).format(ms / 1000);
}
< / script >
< svelte:window onpagehide = { leave } / >
< section class = "open" aria-labelledby = "open-title" >
< h3 id = "open-title" > Abrir la cápsula< / h3 >
{ #if ! due }
< p class = "prose" >
La fecha de apertura todavía no ha llegado según el reloj de este dispositivo. Hasta entonces drand no publica la
firma de la ronda { round } y nadie puede abrir la cápsula, tampoco esta página (paso 9, ERR_RELEASE_UNAVAILABLE).
< / p >
Format 3, step 3: read capsule format 3 of spec v0.10
Syncs testdata with datekeys-go at the tag spec-v0.10 (cc35d2c) and
moves the reader to the DateKeys Protocol Specification v0.10. The
three capsule formats are read.
- framing: FORMAT_3, and isPadded for formats 2 and 3. control: schema
version 3, with the keys 6 and 7 of version 2. SPEC_VERSION is 0.10.
- open: OpenOptions.sink receives the files of a format 3 capsule
(sink.ts: Sink with begin, create, commit and abort, as capsule.Sink,
and MemorySink). Without one, open rejects with a TypeError right
after step 2, before any request, as ErrSinkRequired. Opened gains
head, verdicts, areaLen and unusableHeadExtensions.
- open3.ts: step 17 of format 3 in its substeps 17.2 to 17.8, as
openBody of the reference: a failure of age or a plaintext whose
length is not P prevails, the first failing substep decides, and the
codes other than ERR_INTEGRITY are reported only after reading
PAYLOAD_AGE to its end. Reads grow with the bytes received, never
with the lengths BODY declares. A failure of the sink is ERR_INTEGRITY
with its text, and the sink is aborted once after begin.
- The page: opener.ts opens the fixtures of format 3 into a
MemorySink; the open panel says that it does not deliver their files
yet, and the glosses of the steps name format 3. check-build.mjs
refuses to ship the heads, salts, comments and paths of the format 3
fixtures.
Tests: the 21 fixtures, format 3 laid out byte by byte from its record
and opened into a sink with its files and verdicts; the 209 cases of
the corpus from memory and from a Blob, with the code, the step and,
new, the exact text of capsule.Open, frozen by
scripts/mutation-go-texts.go in testing/mutation-texts.json, which
replays the corpus as internal/testkit does (its extension validator
texts included); the 5110 differential cases over 14 bases; paths,
path_fold, head_schema and security vectors; the control of schema
version 3 in cbor.json; and step 17 on crafted plaintexts sealed again
to I_PAYLOAD, whose texts capsule.Open gives on the same plaintexts.
ibe-vectors.json gains the nine format 3 fixtures from
scripts/ibe-go-vectors.go; the twelve before are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
{ :else if report . prelude ? . format === 3 }
< p class = "prose" >
Esta cápsula es de formato 3: guarda varios ficheros, con sus rutas. Esta página todavía no los entrega; lo hará en
su próxima versión.
< / p >
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
{ : else }
< form class = "form" onsubmit = { submit } novalidate >
{ #if timeAndKey }
< fieldset >
< legend > Credencial de acceso< / legend >
< p class = "hint" >
La política time_and_key pide, además de la firma de la ronda, una clave .dkk de esta cápsula o la identidad
X25519 de uno de sus destinatarios. No salen de este navegador.
< / p >
< div class = "field" >
< label for = "dkk-input" > Clave .dkk< / label >
< input
id="dkk-input"
type="file"
accept=".dkk"
onchange={ onAccessKey }
aria-invalid={ problemField === 'accessKey' ? 'true' : undefined }
aria-describedby={ problemField === 'accessKey' ? 'open-problem' : undefined }
/>
< / div >
< div class = "field" >
< label for = "ids-input" > Identidades X25519 de age< / label >
< textarea
id="ids-input"
rows="2"
bind:value={ identities }
autocomplete="off"
spellcheck="false"
placeholder="AGE-SECRET-KEY-1…"
aria-invalid={ problemField === 'identities' ? 'true' : undefined }
aria-describedby={ problemField === 'identities' ? 'open-problem ids-hint' : 'ids-hint' }
>< / textarea >
< p id = "ids-hint" class = "hint" >
Si te enviaron la cápsula como destinatario de age, pega aquí tu identidad secreta: la línea
< code > AGE-SECRET-KEY-1…< / code > del fichero que creaste con < code > age-keygen< / code > . Sirve pegar el fichero entero,
porque las líneas que empiezan por < code > #< / code > se ignoran. Una por línea. Solo se usa en este navegador, para
probar cuál de los stanzas de la cápsula abre, y se borra después.
< / p >
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
< / div >
< / fieldset >
{ /if }
< div class = "field" >
< label for = "release-input" > Firma de la ronda { round } , el release que publica drand</ label >
< textarea
id="release-input"
rows="3"
bind:value={ releaseInput }
autocomplete="off"
spellcheck="false"
aria-invalid={ problemField === 'release' ? 'true' : undefined }
aria-describedby={ problemField === 'release' ? 'open-problem release-hint' : 'release-hint' }
>< / textarea >
< p id = "release-hint" class = "hint" >
{ #if fixture ? . release !== undefined }
Viene del registro del fixture: es la que publicó drand para la ronda { round } , como muestra
< a href = { drandURL } target="_blank" rel = "noopener noreferrer" > su página en drand</ a > . Cámbiala para ver cómo la
rechaza el paso 10.
{ : else }
Abre < a href = { drandURL } target="_blank" rel = "noopener noreferrer" > la firma de la ronda { round } en drand</ a > en
otra pestaña, copia todo lo que muestra y pégalo aquí; vale también la firma sola, en hexadecimal. La página no
se conecta a drand: la abres tú.
{ /if }
Solo se leen la ronda y la firma, que se verifican aquí con la clave pública del perfil fijado (§51).
< / p >
< / div >
{ #if problem }
< p id = "open-problem" class = "problem" tabindex = "-1" > { problem } </ p >
{ /if }
< div class = "actions" >
< button class = "button" type = "submit" disabled = { busy } > { busy ? 'Abriendo…' : 'Abrir la cápsula' } </button >
{ #if fixture === undefined }
< p class = "hint" >
El texto descifrado se escribe en un fichero temporal privado de este navegador y solo se muestra y se ofrece
si age lo autentica entero (§56); si es texto, la página muestra el principio. Se borra cuando lo pides, al abrir o cargar otra cápsula y al salir de la página; sin
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
ese fichero, se abre en la memoria de la página hasta 64 MiB.
< / p >
{ /if }
< / div >
< / form >
{ /if }
< p class = "visually-hidden" role = "status" > { announcement } </ p >
{ #if result }
{ @const r = result . report }
< div class = {[ 'verdict' , r . opened ? 'valid' : 'invalid' ]} >
< h4 id = "open-result-title" class = "state-word" tabindex = "-1" >
{ r . opened ? 'Abierta' : r . failure ? `Rechazada en el paso $ { r . failure . step } ` : 'Rechazada' }
< / h4 >
{ #if r . opened }
< p >
Supera los pasos 9 a 18 en { seconds ( result . ms )} s. age ha autenticado el texto entero con la clave de
PAYLOAD_AGE, pero eso no prueba quién lo escribió, ni que sea el original si otros abrieron la cápsula antes
(§55.1).
< / p >
{ :else if r . failure }
< p class = "code" > { r . failure . code } </ p >
< p > { errorGloss ( r . failure . code )} </ p >
{ #if r . steps . length === 0 }
< p class = "detail" > { escapeInvisible ( r . failure . message )} </ p >
{ /if }
{ /if }
< / div >
{ #if r . plaintext }
{ @const p = r . plaintext }
< section class = "block" aria-labelledby = "plaintext-title" >
Put the content first on opening, and name its download by its content
When a capsule opens, /inspect now shows its content right under the
verdict, before steps 9 to 18. A capsule does not keep the name of the
file it seals (spec §6, §55.2), and the capsula-<date>.dkc of /create
has no extension of its own, so the download was nameless for the
system: contentExtension now gives it .txt for a text, or the
extension of a common type of file by its first bytes (.pdf, .png,
.jpg, .zip…).
vite preview served the pages without Cache-Control, and a tab
reloaded after a build could keep the old page, whose chunks are gone;
a small plugin, before SvelteKit's, has the pages revalidated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
< h3 id = "plaintext-title" > Contenido< / h3 >
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
{ #if p . text !== undefined }
{ #if p . text === '' }
< p class = "muted" > El texto en claro está vacío.< / p >
{ : else }
< pre class = "plaintext" > { p . textCut ? `$ { p . text } \ n … ` : p.text } </ pre >
{ #if p . textCut }
< p class = "muted" >
Se muestra el principio, como mucho { formatInteger ( SHOWN_TEXT )} caracteres.{ fixture === undefined
? ' Descárgalo para verlo entero.'
: ''}
< / p >
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
{ /if }
{ /if }
{ : else }
< p class = "muted" >
No es texto UTF-8 imprimible, así que la página no lo muestra{ fixture === undefined
? ': descárgalo y ábrelo con el programa que corresponda.'
: '.'}
< / p >
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
{ /if }
{ #if result . download }
{ @const d = result . download }
{ #if deleted }
< p class = "muted" >
{ d . temporary
? 'Fichero temporal borrado. Para descargarlo otra vez, abre de nuevo la cápsula.'
: 'El texto descifrado ya no está en la página. Para descargarlo otra vez, abre de nuevo la cápsula.'}
< / p >
{ : else }
< div class = "actions" >
< a class = "button" href = { d . url } download= { d . name } > Descargar { d . name } </ a >
{ #if d . temporary }
< button class = "button quiet" type = "button" onclick = { deleteNow } > Borrar el fichero temporal </ button >
{ /if }
< / div >
< p class = "hint" >
{ #if d . temporary }
Está en un fichero temporal privado de este navegador ({ formatByteCount ( d . size )} ). Se borra cuando lo pides, al
abrir o cargar otra cápsula y al salir de la página.
{ : else }
Está en la memoria de esta página ({ formatByteCount ( d . size )} ), porque el navegador no le deja un fichero
temporal privado. Se libera al abrir o cargar otra cápsula y al salir de la página.
{ /if }
< / p >
{ /if }
{ /if }
Put the content first on opening, and name its download by its content
When a capsule opens, /inspect now shows its content right under the
verdict, before steps 9 to 18. A capsule does not keep the name of the
file it seals (spec §6, §55.2), and the capsula-<date>.dkc of /create
has no extension of its own, so the download was nameless for the
system: contentExtension now gives it .txt for a text, or the
extension of a common type of file by its first bytes (.pdf, .png,
.jpg, .zip…).
vite preview served the pages without Cache-Control, and a tab
reloaded after a build could keep the old page, whose chunks are gone;
a small plugin, before SvelteKit's, has the pages revalidated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
< dl >
< div >
< dt > Tamaño< / dt >
< dd > { formatByteCount ( p . length )} </ dd >
< / div >
{ #if r . padding }
< div >
< dt > Relleno< / dt >
< dd >
regla { r . padding . rule } : la cápsula sellaba { formatByteCount ( r . padding . paddedLength )} , el contenido seguido de ceros,
que se comprobaron y no se entregan
< / dd >
< / div >
{ /if }
< div >
< dt > SHA-256< / dt >
< dd class = "mono" > { p . sha256 } </ dd >
< / div >
{ #if p . expectedSHA256 !== undefined }
< div >
< dt > Registro del fixture< / dt >
< dd class = { p . expectedSHA256 === p . sha256 ? 'match' : 'mismatch' } >
{ p . expectedSHA256 === p . sha256 ? 'coincide' : 'no coincide' } : < span class = "mono" > { p . expectedSHA256 } </ span >
< / dd >
< / div >
{ /if }
< / dl >
< / section >
{ /if }
{ #if r . steps . length > 0 }
< section class = "block" aria-labelledby = "open-steps-title" >
< h3 id = "open-steps-title" > Pasos 9 a 18< / h3 >
< StepList steps = { r . steps } / >
< / section >
{ /if }
{ #if r . release }
< section class = "block" aria-labelledby = "release-title" >
< h3 id = "release-title" > Release verificado< / h3 >
< dl >
< div >
< dt > Ronda< / dt >
< dd class = "mono" > { r . release . round } </ dd >
< / div >
< div >
< dt > Firma< / dt >
< dd class = "mono" > { r . release . signature } </ dd >
< / div >
< / dl >
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
< / section >
{ /if }
{ #if r . controlExtensions }
< section class = "block" aria-labelledby = "control-ext-title" >
< h3 id = "control-ext-title" > Extensiones de CONTROL_CBOR< / h3 >
< ExtensionList extensions = { r . controlExtensions } object="CONTROL_CBOR" />
< / section >
{ /if }
{ /if }
< / section >
< style >
.open {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1.5rem;
align-content: start;
}
/* minmax(0, 1fr) and min-width 0: a fieldset is as wide as its content
by default, which widens the page on a phone. */
.form {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1.1rem;
max-width: var(--measure);
padding: 1.1rem 1.25rem 1.25rem;
border: 1px solid var(--rule);
border-radius: var(--radius);
background: var(--paper-2);
}
fieldset {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-width: 0;
gap: 0.9rem;
margin: 0;
padding: 0.75rem 1rem 1rem;
border: 1px solid var(--rule);
border-radius: var(--radius);
}
legend {
padding-inline: 0.35rem;
font-weight: 650;
}
.field {
display: grid;
gap: 0.35rem;
}
label {
font-weight: 600;
}
textarea {
width: 100%;
font-family: var(--mono);
font-size: 0.82rem;
line-height: 1.45;
padding: 0.5rem 0.6rem;
border: 1px solid var(--rule-strong);
border-radius: var(--radius);
background: var(--paper);
color: var(--ink);
resize: vertical;
overflow-wrap: anywhere;
}
input[type='file'] {
max-width: 100%;
font: inherit;
font-size: var(--t-small);
}
.hint {
font-size: var(--t-small);
color: var(--ink-muted);
max-width: var(--measure);
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1rem;
}
/* The download link quotes the file name, which may have no break points;
break-word would not let the flex item shrink below it on a phone. */
.actions a.button {
overflow-wrap: anywhere;
}
.button[disabled] {
opacity: 0.6;
cursor: progress;
}
.problem {
border-left: 4px solid var(--fail);
background: var(--fail-bg);
padding: 0.65rem 0.9rem;
border-radius: 0 var(--radius) var(--radius) 0;
overflow-wrap: anywhere;
}
.verdict {
display: grid;
gap: 0.5rem;
padding: 1.1rem 1.25rem 1.25rem;
border: 1px solid var(--rule);
border-top: 6px solid var(--state);
border-radius: var(--radius);
background: var(--paper-2);
max-width: var(--measure);
}
.verdict.valid {
--state: var(--pass);
}
.verdict.invalid {
--state: var(--fail);
}
.state-word {
font-size: var(--t-h2);
font-weight: 700;
letter-spacing: -0.015em;
line-height: 1.2;
color: var(--state);
}
.code,
.detail {
font-family: var(--mono);
font-weight: 600;
overflow-wrap: anywhere;
}
.detail {
font-size: 0.82rem;
font-weight: 500;
}
.block {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1rem;
align-content: start;
}
dl {
display: grid;
border-top: 1px solid var(--rule);
}
dl > div {
display: grid;
gap: 0.1rem 1rem;
padding-block: 0.55rem;
border-bottom: 1px solid var(--rule);
}
@media (min-width: 560px) {
dl > div {
grid-template-columns: 11rem minmax(0, 1fr);
}
}
dt {
font-size: var(--t-small);
font-weight: 600;
color: var(--ink-muted);
}
dd {
margin: 0;
overflow-wrap: anywhere;
}
dd.mono,
dd .mono {
font-size: 0.85rem;
}
dd.match {
color: var(--pass);
}
dd.mismatch {
color: var(--fail);
}
.plaintext {
max-height: 24rem;
overflow: auto;
padding: 0.85rem 1rem;
background: var(--paper-2);
border: 1px solid var(--rule);
border-radius: var(--radius);
font-size: 0.82rem;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
< / style >