@ -1,8 +1,9 @@
< 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
// the person has in hand, pasted from drand, chosen as a file (a release
// object, drand's JSON or a release archive, spec v0.15) or taken from the
// record of an official fixture, 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.
@ -30,7 +31,7 @@
import type { OpenedFiles } from '$lib/inspector/opener.ts';
import { buildOpenReport , contentExtension , type OpenReport , plaintextFileName , plaintextPreview , SHOWN_TEXT } from '$lib/inspector/opening.ts';
import { fetchRelease } from '$lib/inspector/drand.ts';
import { drandReleaseURL , parseReleaseText , releaseText } from '$lib/inspector/release-input.ts';
import { drandReleaseURL , parseReleaseText , releaseText , type SuppliedRelease } 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';
@ -96,6 +97,8 @@
};
let releaseInput = $state(initialRelease);
// A file with the release, which takes the place of the text.
let releaseFile: File | undefined = $state();
let identities = $state('');
let accessKey: File | undefined = $state();
let problem: string | undefined = $state();
@ -186,6 +189,10 @@
announcement = 'Fichero temporal borrado.';
}
function onReleaseFile(event: Event & { currentTarget : HTMLInputElement } ): void {
releaseFile = event.currentTarget.files?.[0];
}
function onAccessKey(event: Event & { currentTarget : HTMLInputElement } ): void {
accessKey = event.currentTarget.files?.[0];
}
@ -195,10 +202,18 @@
if (busy) return;
problem = undefined;
problemField = undefined;
const parsed = parseReleaseText(releaseInput, round);
if (!parsed.ok) {
await fail(parsed.problem, 'release');
return;
// A file with the release goes as it is to step 10; the text is read
// first, for a plain explanation of what cannot be a release.
let release: SuppliedRelease | Blob;
if (releaseFile !== undefined) {
release = releaseFile;
} else {
const parsed = parseReleaseText(releaseInput, round);
if (!parsed.ok) {
await fail(parsed.problem, 'release');
return;
}
release = parsed.release;
}
const id = ++openId;
const stale = (): boolean => id !== openId;
@ -238,7 +253,7 @@
const out = t;
const attempt = await opener.openCapsule({
capsule,
release: parsed.release ,
release,
...(timeAndKey ? { identities } : {} ),
...(timeAndKey && accessKey !== undefined ? { accessKey } : {} ),
...(timeAndKey && words.trim() !== '' ? { words : { text : words , chainHash : report.profile ! . chainHash , round , capsuleId : report.capsule ! . capsuleId } } : {} ),
@ -352,7 +367,8 @@
announcement = '';
await tick();
announcement = message;
document.getElementById(field === undefined ? 'open-problem' : FIELD_IDS[field])?.focus();
const id = field === 'release' & & releaseFile !== undefined ? 'release-file' : field === undefined ? 'open-problem' : FIELD_IDS[field];
document.getElementById(id)?.focus();
}
function seconds(ms: number): string {
@ -368,107 +384,126 @@
{ #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).
firma de la ronda { round } y nadie puede abrir la cápsula, tampoco esta página. Si ya tienes esa firma porque el reloj
va atrasado, puedes darla abajo: una firma válida prueba que la ronda se publicó, así que no se compara con el reloj.
< / p >
{ : else }
< form class = "form" onsubmit = { submit } novalidate >
{ #if timeAndKey }
< fieldset >
< legend > La llave< / legend >
< p class = "hint" >
Esta cápsula se creó «solo con una llave»: además de la firma de la ronda, hace falta su fichero < code > .dkk< / code > ,
las palabras con las que se creó o la clave secreta de < code > age< / code > de una de las personas a las que se dio. No
salen de este navegador.
{ /if }
< form class = "form" onsubmit = { submit } novalidate >
{ #if timeAndKey }
< fieldset >
< legend > La llave< / legend >
< p class = "hint" >
Esta cápsula se creó «solo con una llave»: además de la firma de la ronda, hace falta su fichero < code > .dkk< / code > ,
las palabras con las que se creó o la clave secreta de < code > age< / code > de una de las personas a las que se dio. No
salen de este navegador.
< / p >
< div class = "field" >
< label for = "dkk-input" > Fichero de la llave (.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 = "words-input" > O tus palabras< / label >
< input id = "words-input" type = "text" bind:value = { words } autocomplete="off" spellcheck = "false" aria-describedby = "words-hint" />
< p id = "words-hint" class = "hint" > Las que se escribieron al crear la cápsula. Dan igual mayúsculas, acentos y espacios.< / p >
< / div >
< div class = "field" >
< label for = "ids-input" > O tu clave secreta 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" >
Pega el fichero de tu clave entero, el que creó < code > age-keygen< / code > : vale la línea
< code > AGE-SECRET-KEY-1…< / code > , y las que empiezan por < code > #< / code > se ignoran. Solo se usa en este navegador y se
borra después.
< / p >
< div class = "field" >
< label for = "dkk-input" > Fichero de la llave (.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 = "words-input" > O tus palabras< / label >
< input id = "words-input" type = "text" bind:value = { words } autocomplete="off" spellcheck = "false" aria-describedby = "words-hint" />
< p id = "words-hint" class = "hint" > Las que se escribieron al crear la cápsula. Dan igual mayúsculas, acentos y espacios.< / p >
< / div >
< div class = "field" >
< label for = "ids-input" > O tu clave secreta 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" >
Pega el fichero de tu clave entero, el que creó < code > age-keygen< / code > : vale la línea
< code > AGE-SECRET-KEY-1…< / code > , y las que empiezan por < code > #< / code > se ignoran. Solo se usa en este navegador y se
borra después.
< / p >
< / div >
< / fieldset >
{ /if }
< / 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 >
{ #if fixture ? . release === undefined }
< div class = "actions" >
< button class = "button quiet" type = "button" onclick = { askDrand } disabled= { asking || busy } >
{ asking ? 'Pidiendo la firma…' : 'Pedir la firma a drand' }
< / button >
< / div >
{ #if askNote !== '' }
< p class = "hint" role = "status" > { askNote } </ p >
{ /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 >
{ #if fixture ? . release === undefined && due }
< div class = "actions" >
< button class = "button quiet" type = "button" onclick = { askDrand } disabled= { asking || busy } >
{ asking ? 'Pidiendo la firma…' : 'Pedir la firma a drand' }
< / button >
< / div >
{ #if askNote !== '' }
< p class = "hint" role = "status" > { askNote } </ p >
{ /if }
< 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 }
«Pedir la firma a drand» la pide a sus relays públicos, que ven tu dirección IP y qué ronda pides. También puedes
abrir < a href = { drandURL } target="_blank" rel = "noopener noreferrer" > la firma de la ronda { round } en drand</ a > en otra
pestaña, copiar lo que muestra y pegarlo aquí; vale también la firma sola, en hexadecimal.
{ /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. Si la cápsula guarda varios
ficheros, ese fichero temporal es un ZIP con todos ellos. Se borra cuando lo pides, al abrir o cargar otra
cápsula y al salir de la página; sin ese fichero, se abre en la memoria de la página hasta 64 MiB.
< / p >
< 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 if ! due }
Pega la respuesta de drand que guardaste, o la firma sola en hexadecimal. La página no la pide a drand mientras
este reloj diga que la fecha no ha llegado.
{ : else }
«Pedir la firma a drand» la pide a sus relays públicos, que ven tu dirección IP y qué ronda pides. También puedes
abrir < a href = { drandURL } target="_blank" rel = "noopener noreferrer" > la firma de la ronda { round } en drand</ a > en otra
pestaña, copiar lo que muestra y pegarlo aquí; vale también la firma sola, en hexadecimal.
{ /if }
< / div >
< / form >
{ /if }
Solo se leen la ronda y la firma, que se verifican aquí con la clave pública del perfil fijado (§51).
< / p >
< / div >
< div class = "field" >
< label for = "release-file" > O un fichero con el release< / label >
< input
id="release-file"
type="file"
onchange={ onReleaseFile }
aria-invalid={ problemField === 'release' && releaseFile !== undefined ? 'true' : undefined }
aria-describedby={ problemField === 'release' && releaseFile !== undefined ? 'open-problem release-file-hint' : 'release-file-hint' }
/>
< p id = "release-file-hint" class = "hint" >
Un release que guardaste: el objeto release de DateKeys, la respuesta de drand en JSON o un archivo de releases que
tenga la ronda { round } . Si eliges un fichero, se usa en lugar del texto de arriba. No sale de este navegador y se
comprueba igual (§47.1).
< / 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. Si la cápsula guarda varios
ficheros, ese fichero temporal es un ZIP con todos ellos. Se borra cuando lo pides, al abrir o cargar otra
cápsula y al salir de la página; sin ese fichero, se abre en la memoria de la página hasta 64 MiB.
< / p >
{ /if }
< / div >
< / form >
< p class = "visually-hidden" role = "status" > { announcement } </ p >
@ -484,6 +519,12 @@
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 >
{ #if r . clockBehind }
< p >
El reloj de este dispositivo dice que la fecha de apertura no ha llegado, pero la firma de la ronda { round } es
válida, así que drand ya la publicó: el reloj parece ir atrasado.
< / p >
{ /if }
{ :else if r . failure }
< p class = "code" > { r . failure . code } </ p >
< p > { errorGloss ( r . failure . code )} </ p >