datekeys-ts 0.5.0 implements spec v0.16 (tag spec-v0.16 of datekeys-go):
a seal without accuracy proves nothing before the opening date, and
drand's JSON is read strictly; it also draws the random words of a key of
words, from a computer or from dice, and says what the official SDK says
when it seals, besides everything 0.4.0 does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The author approved specification v0.16 on 7 October 2026, tagged
spec-v0.16 at b6ff17a. Only the annex changes, with the SHA-256 of the
approved text, and the README of testdata; the README and the CHANGELOG
name the approved version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3fd0e93 restores the escapes of release.json that 4f78854 had lost: the
cases "round escaped as round" and "round twice, once escaped as
round", and the surrogate pair of "a surrogate pair in a value". The
annex changes with the SHA-256 of the draft. The comment of the strict
reader had lost the same escape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The draft v0.16 of datekeys-go at 4f78854 (branch v0.16): SPEC_VERSION
0.16, and testdata, wordlists and annex synced from that commit. The
annex is §79 of the draft, with the CC BY-ND 4.0 license of the
specification in its title and the key of words in 79.7.
A seal without accuracy proves nothing before the opening date (§29.7,
§29.11, as 7e3b810): a valid seal is S4 only when its token carries
accuracy and t plus the accuracy is before round_time; otherwise S5,
with the first reason that holds: late, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy. cms.ts reads
hasAccuracy and the policy of the token (tokenIsBTSP); securitycms.ts
gives SealReason, Detail.sealReason and SignerLine.reason; S5 has no
fixed text any more, and verdictLines writes it and the line of a signer
of F6 with the reason, the texts of Go byte for byte (sealReasonText).
encryptFiles returns the verdicts of the area it wrote in
Encrypted.security, as Result.Security of Go, so that a writer warns of
a seal without accuracy (§62.1 rule 19).
drand's JSON is read strictly (§47.1, as b570338): parseDrandJSON, as
ParseDrandJSON of Go, reads RFC 8259 JSON in valid UTF-8 whose value is
an object, with no name repeated in any object, names compared exactly
once their escapes are decoded, a lone escaped surrogate malformed, the
round a number without sign, fraction or exponent from 1 to 2^53 - 1,
and signature and randomness strings, with the error texts of Go.
ParsedRelease is now a Release: no round above 2^53 - 1 is read. The
page reads the answers of the relays with it (drand.ts), as the client
of Go does, and the pasted release with strictJSON and jsonRound
(release-input.ts), so that it never reads another round than step 10.
Tests: security_cms.json with seal_reason (143 cases), the 38 JSON
inputs of release.json, the new cases of signature2_test.go and
drandjson_test.go (with the escapes written as escapes), and the new
fixtures: format3_time_and_key_words opens with the identity that the
words of its words_text give with normalizeWords and wordKey, in the
library and in the page, and format3_full_chunk, whose PAYLOAD_AGE ends
in a full STREAM chunk, opens. check-build.mjs counts words_text among
the secrets of the fixtures.
Reference files made again with Go at 4f78854: mutation-texts.json (its
spec field only), ibe-vectors.json (the two new fixtures, the rest
unchanged) and signing-vectors.json, in an export of 4f78854 with the
same frozen samples read again: the capsules are the same, and the
tokens of the sealer, without accuracy, now give S5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec §38.1: the hint of the person's own words says that they are not
enough for something valuable and that a password used elsewhere must not
be one, since once the date has come the capsule lets anyone test it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As aefc8f6 of datekeys-go. sync-testdata.mjs also vendors annex/ of Go,
the recovery annex (§79 under a title with the version and the SHA-256 of
the specification), and testdata, wordlists and annex are at aefc8f6.
/create recommends the key to a time_only capsule more than a year ahead
(§7.6), and once the capsule is made it says what opening it later will
take: the capsule, one of its keys if it has them, and the signature of
drand for its round, which an archive or a cache service must keep if
drand no longer serves it (§62.1, rule 26); and it offers the annex for
download as <capsule>.recuperacion.txt (rule 27, annex.ts). check-build.mjs
wants the annex shipped byte for byte.
profile.ts gains ProfileStatus, PROFILE_STATUS and profileStatusOf, as
StatusOf of Go: Quicknet is active. planCapsule writes no capsule with a
profile that is not, and /inspect warns when the profile of a capsule is
compromised (buildReport takes profileStatus).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For whoever does not trust the random numbers of a computer, as dice.go of
datekeys-go at 92e7154, with its texts: five dice for each word give a
number from 11111 to 66666, the position of the word in a list of 7776.
wordlist.ts gains DICE_LIST_SIZE, diceNumber, diceWord, diceWords and
diceList, the list numbered as the EFF publishes its own, and wordkey.ts
goFields, which splits at white space as strings.Fields of Go.
/create offers "Con dados" between the random words and the person's own:
it turns the numbers into words as they are typed, tells a number that is
not five dice or that gives a word again, and offers the list numbered for
dice, to print it, with its SHA-256. planCapsule takes dice and diceList,
and readDice reads the numbers one by one. The list loads once for random
words and dice, and an effect that finds it loaded writes no state, so it
does not run again without end.
testdata and wordlists at 92e7154, whose README of the lists records the
SHA-256 of each list numbered for dice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testdata and wordlists at datekeys-go e671032, with the same testdata:
wordlists/en.txt is the large wordlist of the EFF, 7776 words, CC BY 4.0,
in its order and without the dice numbers. WORD_LIST_SHA256 pins it, and
the alphabet of en is a to z and the hyphen of its four compound words.
/create still offers the Spanish list, and check-build.mjs wants the site
to ship that one alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The effect that checks the paths wrote pathCheck and read it back, so
Svelte ran it again without end once the rules had loaded, with the first
file, comment or author: effect_update_depth_exceeded in the console, and
checkPaths over the whole list each time. It reads the check from a local
now. Since 7dc88ef (1 October), and in 0.4.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the policy with a key, a check box opens the capsule with words too,
random by default: the page fetches the Spanish list of datekeys-go from
its own site (create-words.ts, the hashed file that Vite ships), takes it
only with its pinned SHA-256, and draws 7 words that never leave the
browser. It shows them numbered, since they are typed in that order, with
their strength computed from the list loaded and a button to draw others.
"Las elijo yo" lets the person type their own, with the warning that they
are weaker. Either way they are written again; case and accents do not
matter. planCapsule takes wordsKind (none, random or own) and asks for the
words that are missing.
licenses.txt carries the README of wordlists/, with the source, the method
and the license of the list (CC BY-SA 4.0), and check-build.mjs wants it,
and the list shipped byte for byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/sync-testdata.mjs copies wordkey/lists of the same commit of
datekeys-go into wordlists/, with its own SOURCE.json, and check verifies
both copies; the testdata test wants both from one commit. .gitattributes
keeps the bytes of wordlists/ as they are, like those of testdata/.
testdata and wordlists at datekeys-go 27a75ee, branch v0.15 after the tag
spec-v0.15: the files of testdata are those of the tag; wordlists brings the
Spanish list, 7776 words, CC BY-SA 4.0, with its README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cambios notables de la librería TypeScript y de la página. El proyecto usa versionado semántico; mientras sea 0.x, no hay promesa de estabilidad. La sección «Versiones» del [README](README.md) explica qué cubre cada número.
## 0.5.0 — 7 de octubre de 2026
La especificación 0.16, el tag `spec-v0.16` de `datekeys-go`: la revisión de Astra de la v0.15, con el sello sin `accuracy` y el JSON de drand estricto; y las palabras al azar de la llave de palabras, de una lista inglesa y una española, con el ordenador o con dados, y lo que dice el SDK oficial al sellar. El autor la cerró el 7 de octubre de 2026, con el tag `v0.5.0`.
### La especificación 0.16 (07-10-2026)
La v0.16 de `datekeys-go`, aprobada el 7 de octubre de 2026 con el tag `spec-v0.16` (`b6ff17a`): lo que encontró la revisión de Astra de la v0.15. No cambia ningún formato; cambian el veredicto de un sello sin `accuracy` y la lectura del JSON de drand (§76, «Cambios normativos de la v0.16»).
- `SPEC_VERSION` pasa a `0.16`. `testdata`, `wordlists` y `annex` se sincronizan con `b6ff17a`, el commit del tag (`node scripts/sync-testdata.mjs sync --commit b6ff17a`), cuyo anexo lleva el SHA-256 de la versión aprobada; antes, `3fd0e93` devolvió a `release.json` los escapes de sus casos `\u0072ound` y del par de sustitutos, que `4f78854` había perdido: el campo `spec` de cada fichero dice `0.16`; `vectors/security_cms.json` se hizo de nuevo, con 143 casos y `seal_reason`; `vectors/release.json` gana 26 casos del JSON de drand; `vectors/wordkey.json`, el texto del vector del anexo, también con sus marcas sueltas, y su llave; y llegan dos fixtures, `format3_time_and_key_words` y `format3_full_chunk`. El anexo de recuperación es el §79 del borrador v0.16, con la licencia de la especificación, CC BY-ND 4.0, en su título (`70d907b`) y la llave de palabras en 79.7.
- **El sello sin `accuracy`** (§29.7, §29.11, como `7e3b810` de Go). Un sello válido es S4 solo si el token lleva `accuracy` y t más la precisión es anterior a `round_time`; si no, S5, con el primer motivo que se cumple: `late`, sellado después o demasiado cerca; `no accuracy, BTSP`, sin `accuracy` en un token de la política BTSP de ETSI EN 319 421 (0.4.0.2023.1.1), que la exige; o `no accuracy`. Una `accuracy` de 0 segundos, o vacía, es una precisión de 0 y vale. `cms.ts`: el token gana `hasAccuracy` y `policy`, y `tokenIsBTSP`. `securitycms.ts`: `SealReason`, `Detail.sealReason` y `SignerLine.reason`. `security.ts`: S5 ya no tiene texto fijo; `verdictLines` escribe «No acredita que se sellara antes de la fecha de apertura: ‹motivo›.», y la línea de un firmante de F6 cuyo sello no lo acredita, «sin acreditar que fuera antes de la fecha de apertura: ‹motivo›», con los textos de `sealReasonText`, los de Go byte a byte.
- **El escritor avisa** (§62.1, regla 19). `encryptFiles` devuelve en `Encrypted.security` los veredictos del área que escribió, como `Result.Security` de Go: un sello sin `accuracy` se escribe, y quien escribe ve su S5, o la línea de su firmante, con el motivo. `/create` no sella, así que no cambia.
- **El JSON de drand, estricto** (§47.1, como `b570338` de Go). `releaseobject.ts` cambia el lector que imitaba `encoding/json` por `parseDrandJSON`, como `ParseDrandJSON` de Go: JSON de RFC 8259 en UTF-8 válido cuyo valor es un objeto, ningún nombre repetido en ningún objeto, nombres comparados exactos tras decodificar sus escapes (`"round"` es `round`, y `ROUND` otro nombre), un sustituto escapado sin su pareja es JSON mal formado, `round` es un número sin signo, fracción ni exponente de 1 a 2⁵³ − 1, `signature` y `randomness` son cadenas, y `randomness`, si viene, aunque sea vacía, es el SHA-256 de la firma. Los textos de error no cambian. Como la ronda ya no pasa de 2⁵³ − 1, `ParsedRelease` es un `Release`, sin `bigint`. `drand.ts` lee las respuestas de los relays con `parseDrandJSON`, como el cliente de Go, y `release-input.ts`, lo pegado, con `strictJSON` y `jsonRound`, para que la página no lea otra ronda que el paso 10.
- **Las pruebas.**`vectors.test.ts` compara `seal_reason` en cada caso de `security_cms.json` y en cada firmante, y corre los 38 JSON de `release.json`. `securitycms.test.ts` lleva los casos nuevos de `signature2_test.go` (sin `accuracy` años antes y después, BTSP con ella y sin ella, una `accuracy` de 0 segundos y vacía, y un firmante cuyo sello no la lleva), y `releaseobject.test.ts`, los de `drandjson_test.go`, con los escapes escritos como escapes, que el generador de `release.json` escribió sin escapar. `format3_time_and_key_words` abre con la identidad que dan las palabras de su `words_text` con `normalizeWords` y `wordKey`, en la librería y en la página, y `format3_full_chunk`, cuyo `PAYLOAD_AGE` acaba en un trozo completo, abre a su fichero. `check-build.mjs` cuenta `words_text` entre los secretos de los fixtures.
- **Los ficheros de `testing/` generados con Go**, de nuevo con `4f78854`: `mutation-texts.json` (solo cambia su campo `spec`); `ibe-vectors.json`, con los dos fixtures nuevos y el resto idéntico; y `signing-vectors.json`, en una exportación de `4f78854`, con las mismas cinco muestras, cuyo `ts` se volvió a leer: las cápsulas salen iguales, y como los tokens del sellador no llevan `accuracy`, sus sellos dan ahora S5.
### Las palabras al azar de la llave de palabras (07-10-2026)
El SHOULD de §38.1, ofrecer palabras al azar de una lista pública, como hace `datekeys encrypt -new-words` desde `c49c67c` de `datekeys-go`. No cambia ningún formato ni la derivación.
- **Las listas de `datekeys-go`.**`scripts/sync-testdata.mjs` copia del mismo commit `wordkey/lists` en `wordlists/`, con su `SOURCE.json`, y `check` comprueba las dos copias. `testdata` y `wordlists` están en `27a75ee` de `datekeys-go`, de la rama `v0.15` después del tag `spec-v0.15`: los ficheros de `testdata` son los del tag, y `wordlists` trae la lista española, de 7 776 palabras, un borrador aún sin revisar, con licencia CC BY-SA 4.0.
- **`wordlist.ts`**, como `wordkey.Generate`, `CheckList` y `Bits` de Go en `27a75ee`, con sus textos: `generateWords` sortea palabras distintas, 7 por defecto, con `crypto.getRandomValues`; `wordBits` da su fuerza, 90 bits para 7 de 7 776; `checkWordList` rechaza una lista de menos de 2 048 palabras, con dos que son una al normalizarlas o con un carácter que no es una letra del alfabeto de su idioma, que da el código y no la lista (para `es`, de la `a` a la `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` y `ñ`); y `readWordList` solo acepta una lista con el SHA-256 fijado para su idioma, que sea UTF-8 y que `checkWordList` acepte. `wordkey.ts` gana `wordRules`, que carga una vez las tablas de Unicode para quien lee muchas palabras.
- **La página `/create`** ofrece palabras al azar por defecto. Con la política «con llave», una casilla abre la cápsula también con unas palabras: al marcarla, la página descarga del propio sitio la lista española (`create-words.ts`, el fichero que Vite publica con un nombre con hash), la acepta solo con su SHA-256 fijado, y sortea 7 palabras que no salen del navegador. Las muestra numeradas, con su fuerza calculada con la lista cargada y un botón para sacar otras. «Las elijo yo» deja escribir las propias, con el aviso de que son más débiles. En los dos casos hay que escribirlas otra vez para comprobar que se tienen; dan igual mayúsculas y tildes. `planCapsule` recibe `wordsKind` (`none`, `random` u `own`) y pide las palabras que falten.
- **Corregido en `/create`:** el efecto que comprueba las rutas escribía `pathCheck` y lo volvía a leer, así que Svelte lo relanzaba sin fin en cuanto se cargaban las reglas, con el primer fichero, comentario o autor: `effect_update_depth_exceeded` en la consola y `checkPaths` repetido sobre toda la lista. Lo lee ahora de una variable local. Venía de `7dc88ef` (1 de octubre) y está en la `0.4.0`.
- **La lista inglesa.**`testdata` y `wordlists` pasan a `e671032` de `datekeys-go`, con el mismo `testdata`: `wordlists/en.txt` es la lista grande de la EFF, de 7 776 palabras, CC BY 4.0, en su orden y sin los números de los dados. `WORD_LIST_SHA256` fija su SHA-256, y el alfabeto de `en` es de la `a` a la `z` y el guion de sus cuatro palabras compuestas, como `t-shirt`. `/create` sigue ofreciendo la española, y `check-build.mjs` exige que la página publique solo esa.
- **Los dados**, para quien no se fía del azar del ordenador, como `dice.go` de `datekeys-go` en `92e7154`, con sus textos: cinco dados por palabra dan un número del 11111 al 66666, la posición de la palabra en una lista de 7 776. `wordlist.ts` gana `DICE_LIST_SIZE`, `diceNumber`, `diceWord`, `diceWords` y `diceList`, la lista numerada como la publica la EFF (la de `en` es su fichero, byte a byte), y `wordkey.ts`, `goFields`, que separa por espacios como `strings.Fields` de Go. En `/create`, «Con dados» convierte los números en palabras a medida que se escriben, avisa de un número que no vale o de una palabra repetida, y ofrece la lista numerada para imprimirla, con su SHA-256; `planCapsule` recibe `dice` y `diceList`, y `readDice` lee los números uno a uno. `testdata` y `wordlists` pasan a `92e7154`, cuyo `README.md` de las listas recoge el SHA-256 de cada lista numerada.
- **Lo que dice el SDK oficial al sellar**, como `aefc8f6` de `datekeys-go` (§7.6, §62.1 reglas 26 y 27, §71). `sync-testdata.mjs` copia también `annex/` de Go, el anexo de recuperación (§79 bajo un título con la versión y el SHA-256 de la especificación), y `testdata`, `wordlists` y `annex` pasan a `aefc8f6`. En `/create`: con una fecha a más de un año y «solo fecha», la recomendación de la llave; tras crear la cápsula, «Para abrirla más adelante», con lo que hará falta y la descarga de las instrucciones para abrirla sin DateKeys, `<cápsula>.recuperacion.txt` (`annex.ts`); y `check-build.mjs` exige el anexo byte a byte. `profile.ts` gana `ProfileStatus`, `PROFILE_STATUS` y `profileStatusOf`, como `StatusOf` de Go: Quicknet está activo; `planCapsule` no escribe con un perfil que no lo esté, y `/inspect` avisa si el de una cápsula está comprometido (`buildReport` recibe `profileStatus`).
- **`licenses.txt`** lleva el `README.md` de `wordlists/`, con el origen, el método y la licencia de la lista (CC BY-SA 4.0), y `check-build.mjs` exige que esté y que la lista se publique byte a byte.
## 0.4.0 — 7 de octubre de 2026
La especificación 0.15, el tag `spec-v0.15` de `datekeys-go`: la recuperación a largo plazo, con el objeto release, los archivos de releases y el release en la mano. El autor la cerró el 7 de octubre de 2026, con el tag `v0.4.0`.
@ -21,22 +21,24 @@ Hay tres números de versión, cada uno con su significado, como en la referenci
| Versión | Dónde | Cambia cuando |
|---|---|---|
| Formato | Dentro de los objetos: el formato de la cápsula, el `VERSION` del prelude de DKC1, 1, 2 o 3 al leer, que fija también la versión de schema de CONTROL_CBOR; y 1 en la trama DKK1 y en el schema de los demás objetos | Cambia el formato. Un lector rechaza una versión que no conoce (§22, §70) |
| Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.15`: la del tag `spec-v0.15` de `datekeys-go`, que aprobó el autor el 7 de octubre de 2026. `testdata` está en ese tag (`fe50885`), y todos sus ficheros dicen `0.15` | Cambia el texto normativo |
| Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.16`: la v0.16 de `datekeys-go`, aprobada el 7 de octubre de 2026 con el tag `spec-v0.16`, en `b6ff17a`. `testdata` está en ese commit, y todos sus ficheros dicen `0.16` | Cambia el texto normativo |
| Librería | `VERSION` de `src/lib/dkc/version.ts`, igual al campo `version` de `package.json` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de 1.0.0 |
`version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos.
La versión actual es la `0.4.0`, del 7 de octubre de 2026, con el tag `v0.4.0`. Cubre:
- la especificación 0.15 (el tag `spec-v0.15` de `datekeys-go`): lee los formatos de cápsula 1 a 3 y escribe el 3, y el 2 solo como generador de vectores (§62.1, regla 1);
- el objeto release y el release en la mano (§47.1, §49, §50, §63 pasos 9.c y 10): lee el objeto release y el JSON de drand con los textos de Go, busca la ronda en un archivo de releases local y abre con un release en la mano aunque el reloj vaya atrasado; `/inspect` acepta el release pegado o en un fichero;
La versión actual es la `0.5.0`, del 7 de octubre de 2026, con el tag `v0.5.0`. Cubre:
- la especificación 0.16 (el tag `spec-v0.16` de `datekeys-go`): lee los formatos de cápsula 1 a 3 y escribe el 3, y el 2 solo como generador de vectores (§62.1, regla 1);
- un sello sin `accuracy` no prueba nada antes de la fecha de apertura: los veredictos dicen por qué, y el escritor devuelve los del área que escribe (§29.7, §29.11, §62.1 regla 19);
- el objeto release y el release en la mano (§47.1, §49, §50, §63 pasos 9.c y 10): lee el objeto release, y el JSON de drand de forma estricta y con los textos de Go, busca la ronda en un archivo de releases local y abre con un release en la mano aunque el reloj vaya atrasado;
- la firma de autor de `alg` 1, con las claves `dkauthor1…`, y la de `alg` 2 con certificados, y el sello RFC 3161: las evalúa al abrir con los veredictos de Go y las escribe con los enganches del escritor;
- la llave de palabras, la nota pública y el localizador de `datekeys.capsule`, con su sellado, su sobre y la comprobación de la IP a la que resuelve una dirección, NAT64 incluido;
- la llave de palabras, con palabras al azar de la lista inglesa de la EFF o de la española, sorteadas en el dispositivo o con dados; la nota pública y el localizador de `datekeys.capsule`, con su sellado, su sobre y la comprobación de la IP a la que resuelve una dirección, NAT64 incluido;
- lo que dice el SDK oficial al sellar: el anexo de recuperación junto a la cápsula, lo que hará falta para abrirla y el estado del perfil (§7.6, §62.1 reglas 26 y 27, §71);
- solo el scheme de Quicknet (`bls-unchained-g1-rfc9380`), el único que admite §12.1: un perfil de otro scheme da `ERR_UNKNOWN_PROFILE`;
- la inspección de los pasos 1 a 8 y la apertura de los pasos 9 a 18, desde un `Uint8Array` o un `Blob` y hacia memoria o hacia un stream de salida, y las páginas `/inspect` y `/create`;
- todos los vectores y fixtures compartidos de `datekeys-go` en `fe50885` (`spec-v0.15`);
- todos los vectores y fixtures compartidos de `datekeys-go` en `b6ff17a` (`spec-v0.16`);
- navegadores con Web Crypto y Node 20 o posterior.
La `0.3.0`, del 6 de octubre de 2026, con el tag `v0.3.0`, cubría la especificación 0.14 sin el objeto release. La `0.2.0`, del mismo día, con el tag `v0.2.0`, cubría lo mismo para la especificación 0.13. La anterior es `0.1.0`, del 29 de septiembre de 2026, con el tag `v0.1.0`: la especificación 0.9, leer los formatos 1 y 2, y la página `/inspect`.
La `0.4.0`, del 7 de octubre de 2026, con el tag `v0.4.0`, cubría la especificación 0.15, sin las palabras al azar. La `0.3.0`, del 6 de octubre de 2026, con el tag `v0.3.0`, cubría la especificación 0.14 sin el objeto release. La `0.2.0`, del mismo día, con el tag `v0.2.0`, cubría lo mismo para la especificación 0.13. La anterior es `0.1.0`, del 29 de septiembre de 2026, con el tag `v0.1.0`: la especificación 0.9, leer los formatos 1 y 2, y la página `/inspect`.
[CHANGELOG.md](CHANGELOG.md) recoge los cambios de cada versión.
@ -53,13 +55,13 @@ La inspección (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `cbor.ts` | El codec propio de la referencia, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error: `Encoder` con error persistente; `Decoder`, cursor estricto (`map`/`key`/`endMap`, `array`, `uint`/`uint64`, `bstr`, `text`, `done`); `unmarshal` (decodifica, reencodifica y compara; `onReject` para borrar secretos); `peek`/`checkSchema` (capa 2 de §69.1: tipo y versión antes que nada) y `walk` (lector genérico acotado en profundidad y longitud) | `codec` |
| `schema.ts` | Lo que comparten los decodificadores de PUBLIC_HEADER, CONTROL_CBOR y el cuerpo de la `.dkk`: `key N: ` en los errores, claves obligatorias y arrays de extensiones | `capsule/framing.go`, `accesskey` |
| `extension.ts` | Arrays de extensiones, leídos con su objeto (capa 3 de §69.1). Registros con ubicación opcional (`registeredIn`): una extensión conocida fuera de los objetos y arrays de su registro cuenta allí como desconocida (§54, §72), como `extension.Placement` en Go. Reglas del array: de 1 a 64, en orden estrictamente ascendente de los bytes UTF-8 de `extension_id` (nunca por unidades UTF-16), `extension_version` hasta 2³² − 1, `data` ausente o `bstr` no vacío, ningún id en los dos arrays; registros, críticas y no críticas (capa 4). `checkWrite` es la regla de los codificadores del §72 para las extensiones de la especificación (`NOTE_ID`, `CAPSULE_ID`): `datekeys.note` solo en el array no crítico de la cabecera y `datekeys.capsule` solo en el de una `.dkk`, con datos válidos; la aplican el escritor de cápsulas y el de `.dkk` | `extension` (`CheckWrite` y el registro `Standard`) |
| `profile.ts` | Provider Profile: CBOR exacto, `profile_hash`, reglas 1 a 4 de §12.1 en su orden (el límite de `period` de §74 en la capa del esquema; alfabetos, el único scheme que admite la v0.14, `bls-unchained-g1-rfc9380`, con los textos de `validateDrand` de Go, clave pública de G2 y fórmula de `chain_hash`), registro pinneado; Quicknet fijado por su CBOR y su hash | `profile` |
| `profile.ts` | Provider Profile: CBOR exacto, `profile_hash`, reglas 1 a 4 de §12.1 en su orden (el límite de `period` de §74 en la capa del esquema; alfabetos, el único scheme que admite la v0.14, `bls-unchained-g1-rfc9380`, con los textos de `validateDrand` de Go, clave pública de G2 y fórmula de `chain_hash`), registro pinneado; Quicknet fijado por su CBOR y su hash; `profileStatusOf`, el estado de un perfil en el registro de §71 tal como lo conoce esta versión (`PROFILE_STATUS`: Quicknet, activo), como `StatusOf` de Go | `profile` |
| `bls12381.ts` | Pertenencia de claves públicas BLS12-381 comprimidas (G1 y G2) al subgrupo, como `FromCompressed` de kilic | `kyber-bls12381` |
| `ibe.ts` | IBE-CCA de tlock sobre G2 para Quicknet (§63 paso 11): `decryptOnG2` y `encryptOnG2RFC9380` (Qid = H(id) en G1 con el DST de RFC 9380, sigma aleatorio, U = r·G2), con la puerta de codificación canónica de `bls12381.ts` sobre la firma y U; H2 sobre GT serializado en el orden de kilic (nunca `Fp12.toBytes` de noble), H3 (`h3Base` y `h3Try`, que desplaza el primer byte de cada intento un bit a la derecha, como kyber) y H4; `roundIdentity` y `hashToG1`, el hash a G1 de RFC 9380 que usan también `release.ts` y el cifrado; el cuerpo `U ‖ V ‖ W` de 128 bytes del stanza. Errores `IbeError` con motivo (`length`, `encoding`, `identity`, `proof`) y texto fijos, sin ningún valor del cálculo; borra sigma y los hashes derivados. Sobre `@noble/curves` 2.4.0; lleva el aviso MIT de `tlock-js`, cuya estructura sigue. Lo usa la apertura (`open.ts`) | `encrypt/ibe` de drand/kyber (`DecryptCCAonG2`), `tlock.BytesToCiphertext` y `TimeUnlock` |
| `release.ts` | Verificación local del release (§17, §51, §63 paso 10), en el orden y con los textos de `provider.Verify`:<br>1. el rango de la ronda (`ERR_DATEKEY_INVALID`);<br>2. la ronda del release antes que la firma (`ERR_ROUND_MISMATCH`);<br>3. la longitud de la firma;<br>4. la clave pinneada (`ERR_UNKNOWN_PROFILE`);<br>5. la firma: codificación canónica de un punto de G1 que no sea el infinito, y firma BLS válida de la ronda sobre `@noble/curves` 2.4.0, con el DST de RFC 9380 para G1 (`ERR_RELEASE_INVALID`).<br>Nada de noble se copia a los errores. Solo verifica el scheme de Quicknet, el único que admite un perfil desde la v0.14: un `Profile` de otro scheme, que `validateProfile` rechaza, falla aquí con `ERR_UNKNOWN_PROFILE` tras las comprobaciones de ronda (decisión 3 del plan de la fase 2). También define `ReleaseSource`, con su contrato de fuentes de red y de la corrección 6, y `suppliedRelease`, el release que entrega quien llama. Desde la v0.15, antes de la ronda compara la cadena que nombra el release, si nombra una, con la del perfil fijado (`ERR_PROFILE_MISMATCH`), y reexporta `releaseobject.ts` | `provider` (`Verify`, `ReleaseSource`) |
| `releaseobject.ts` | El objeto release de la v0.15 (§47.1), sin noble, como `provider/release.go` y `provider/archive.go` de Go, con sus textos byte a byte:<br>- `encodeRelease`, `newReleaseObject` y `decodeRelease`, con las capas del paso 10: el tamaño, de 1 a 1024 bytes, antes de decodificar; el tipo y la versión; el schema (`ERR_NON_CANONICAL_CBOR` o `ERR_UNSUPPORTED_VERSION`);<br>- `parseRelease`, que lee además el JSON de drand cuando su primer byte que no es un espacio es `{`, como lo lee `encoding/json` de Go en su struct: claves sin distinguir mayúsculas, la última gana, `null` deja el campo sin poner, una ronda que no es un entero de 0 a 2⁶⁴ − 1 o un campo de otro tipo hacen fallar la entrada, y una ronda por encima de 2⁵³ − 1 se guarda como `bigint` para el texto de `ERR_ROUND_MISMATCH`; todo fallo es `ERR_RELEASE_INVALID`;<br>- `ReleaseSupplier`, el release en la mano (`provider.Supplier`), y `encodedRelease`;<br>- `ReleaseArchive`, el archivo de releases local, formato informativo de §50: de un `Blob` lee solo la cabecera y una firma, y cada fallo, una ronda a ceros incluida, es `ERR_RELEASE_UNAVAILABLE` | `provider` (`EncodeRelease`, `DecodeRelease`, `ParseRelease`, `Supplier`, `Encoded`, `Archive`) |
| `releaseobject.ts` | El objeto release de la v0.15 (§47.1), sin noble, como `provider/release.go`, `provider/drandjson.go` y `provider/archive.go` de Go, con sus textos byte a byte:<br>- `encodeRelease`, `newReleaseObject` y `decodeRelease`, con las capas del paso 10: el tamaño, de 1 a 1024 bytes, antes de decodificar; el tipo y la versión; el schema (`ERR_NON_CANONICAL_CBOR` o `ERR_UNSUPPORTED_VERSION`);<br>- `parseRelease`, que lee además el JSON de drand cuando su primer byte que no es un espacio es `{`, con `parseDrandJSON`, el lector estricto de la v0.16, como `ParseDrandJSON` de Go: como mucho 8 KiB de JSON de RFC 8259 en UTF-8 válido cuyo valor es un objeto; ningún objeto repite un nombre, y los nombres se comparan exactos, punto de código a punto de código, tras decodificar sus escapes, así que `"round"` es `round` y `ROUND` otro nombre, que se ignora; el escape de un sustituto sin su pareja es JSON mal formado; `round` es un número sin signo, fracción ni exponente, de 1 a 2⁵³ − 1; `signature` y `randomness` son cadenas, y `randomness`, si viene, el SHA-256 de la firma en hexadecimal, en cualquier caja; todo fallo es `ERR_RELEASE_INVALID`. `strictJSON` y `jsonRound` son el lector y la ronda, que usan también `drand.ts` y `release-input.ts` de la página;<br>- `ReleaseSupplier`, el release en la mano (`provider.Supplier`), y `encodedRelease`;<br>- `ReleaseArchive`, el archivo de releases local, formato informativo de §50: de un `Blob` lee solo la cabecera y una firma, y cada fallo, una ronda a ceros incluida, es `ERR_RELEASE_UNAVAILABLE` | `provider` (`EncodeRelease`, `DecodeRelease`, `ParseRelease`, `ParseDrandJSON`, `Supplier`, `Encoded`, `Archive`) |
| `open.ts` | Los pasos 9 a 18 de §63 sobre los pasos 1 a 8 de `inspectWith`, con los checks, códigos y textos de `capsule.Open`:<br>- las credenciales y el release (paso 9), que cualquier fallo de la fuente convierte en `ERR_RELEASE_UNAVAILABLE` (corrección 6). El release llega de `source`, una fuente de red, a la que no se pide nada antes de `round_time`, o de `release`, un release en la mano (v0.15), que no se compara con el reloj: `Opened.clockBehind` dice si el reloj iba por detrás, y el paso 10 empieza por decodificarlo;<br>- la verificación del release (10);<br>- `OUTER_TIME_AGE` (11), la estructura frente a `access_policy` (12) e `INNER_ACCESS_AGE` (13);<br>- `CONTROL_CBOR` (14), `header_binding` (15), `I_PAYLOAD` (16), `PAYLOAD_AGE` (17) y el commit (18).<br>Lee los dos formatos (§22, §70). En el formato 2, `INNER_ACCESS_AGE` tiene exactamente 16 stanzas (paso 12); `CONTROL_CBOR` es de la versión de schema 2, con L y la regla de relleno (14); el paso 16 calcula P, y el 17 exige un texto en claro de exactamente P bytes con ceros tras el contenido, `ERR_INTEGRITY` en otro caso. Solo se entregan los L primeros bytes, nunca el relleno (§29.1, §56). `Opened` da el formato y, en los formatos 2 y 3, L, la regla y P.<br>En el formato 3, el paso 17 lo hace `open3.ts`, y los ficheros van a `sink`; sin él, `open` rechaza con un `TypeError` justo tras el paso 2, antes de pedir nada, como `ErrSinkRequired`. `Opened` da entonces el head, los veredictos del área de seguridad y el tamaño del área.<br>Abre los tres ficheros `age` con el `Decrypter` de `age-encryption` y con identidades propias que aplican las reglas de `agewrap`: la de tiempo, sobre `ibe.ts`; las de acceso y payload, sobre `x25519.ts`, stanza a stanza. Los fallos de `age` que no informa una identidad son `ERR_INTEGRITY` con el motivo fijo de su fase, cabecera o STREAM, sin copiar el texto de `age-encryption`.<br>La entrada puede ser un `Uint8Array` o un `Blob`, como un `File`. De un `Blob` solo se lee el prefijo de los pasos 1 a 8 (`prefix.ts`), el `capsule_digest` de la `.dkk` se calcula sobre su stream (`digest.ts`) y `PAYLOAD_AGE` se descifra en streaming.<br>El texto en claro va a memoria o a `output`, un `WritableStream`. Se escribe a medida que `age` autentica cada chunk, se cierra solo tras el paso 18 y se aborta ante cualquier fallo, en cualquier paso (§56). Un fallo del stream de salida es `ERR_INTEGRITY` con su texto, como en Go. El `WritableStream` de un fichero OPFS guarda lo escrito en un fichero de intercambio hasta el cierre: comprobado en el navegador, un fallo de STREAM deja intacto el contenido anterior | `capsule.Open`, `agewrap` (`TimeIdentity`, `AccessIdentity`, `PayloadIdentity`) |
| `encrypt.ts`, `writer.ts` | Los writers. `encryptFiles(files, opts)` escribe un `.dkc` de formato 3, como `capsule.EncryptFiles`: comprueba las rutas y los textos con las reglas del lector y con los textos de Go, pone los ficheros en el orden de los bytes de sus rutas, mide L con un head de sal y hashes a cero, lee cada fichero dos veces y falla si cambió entre las dos lecturas; el head, el control y el área de seguridad se decodifican antes de escribir. El área de seguridad mide 32 KiB sea lo que sea lo que guarde la cápsula (§62.1, regla 13), y va vacía o con la firma y el sello de los enganches de Go: `authorKey` firma con `alg` 1 (un `AuthorKey` de `authorkey.ts` o cualquier `AuthorSigner`), `cmsSigner` con `alg` 2, la firma con certificados, y `sealer` pide el sello de `seal_type` 2; `largeArea` deja ensanchar el área a 64 KiB solo si lo firmado no cabe en 32 KiB. Los enganches pueden ser asíncronos. Se comprueban como en `newSealer` de Go, en su orden y con sus textos: una sola firma, y con `cmsSigner` los sellos van dentro de cada firma. Se llaman cuando el control y el head ya son los finales y antes de escribir nada: la firma se compromete con ellos y el sello con la firma (§29.8, §29.11). El área se evalúa con el lector de la librería en el contexto de la cápsula antes de escribirla, como `security` de Go, y una firma que no daría F4 o F6, o un sello que no daría S4 o S5, la hace fallar con el texto de Go (reglas 17, 19 y 21). Lo que lanza `cmsSigner` o `sealer` llega con `capsule: signing: ` o `capsule: sealing: ` y su mensaje, y el error como `cause`. `publicNote` es la nota pública de la cabecera (§24.1), que se rechaza con los textos de `extension.CheckNote` tras `capsule: `, y nunca se corrige; las opciones se comprueban en el orden de `newSealer` de Go. Con un área de 512 bytes, que solo puede pedir un generador de vectores, reproduce byte a byte `PRELUDE`, PUBLIC_HEADER, CONTROL_CBOR y `BODY` de los cinco fixtures que escribió `EncryptFiles` en la v0.10. `fileSource` hace la fuente de un `File`.<br>El formato 2 solo lo escribe un generador de vectores (§62.1, regla 1). `encrypt(src, opts)` tiene la forma de `capsule.Encrypt`, pero sus opciones no pueden pedirlo, así que falla con el texto de Go. Lo que solo pide un generador, el formato 2 y otra área (`TestVectors`, como `EncryptOptions.TestVectors` de Go), solo lo pasan al núcleo los ayudantes de `testing/encrypt.ts` (`encryptVectors`, `encryptWith` y `encryptFilesWith`), que ninguna página puede cargar. Con ellos, las pruebas y los scripts escriben un `.dkc` de formato 2, sin head, área ni nota, y, si se pide, una `.dkk` portable (§61, §62, §62.1), en el orden y con los textos y códigos de `capsule.Encrypt`:<br>- el formato 2 siempre; L conocida de antemano (el tamaño de un `Uint8Array` o un `Blob`, o `length` con un `ReadableStream`), y una fuente que da más o menos bytes falla con los textos de Go;<br>- el relleno `reforzado` por defecto, o `bloque256`;<br>- de 1 a 16 credenciales, canónicas y no de orden bajo, un señuelo en cada hueco libre, cuyo escalar se borra al derivar su clave pública, y un orden uniforme de los 16 (`random.ts`);<br>- `SEALED_CONTROL_LEN` con la fórmula del §62.1, comprobada con el sellado real;<br>- las autocomprobaciones de la regla 11 y dos más: `OUTER_TIME_AGE` con las reglas del lector, y la cabecera de `PAYLOAD_AGE`, que `I_PAYLOAD` abre antes de escribir nada.<br>Nada se escribe hasta que todo lo anterior al contenido está comprobado. El contenido va en trozos de 64 KiB, seguido de los ceros del relleno, con presión inversa, hacia memoria (hasta `MAX_MEMORY_DKC`, 1 GiB) o hacia `output`, que se cierra solo con la cápsula completa y comprobada y se aborta ante cualquier fallo. Los errores de la fuente y de la salida se relanzan tal cual.<br>El núcleo, `writer.ts`, recibe la aleatoriedad de quien lo llama: `encrypt.ts` le da la de `crypto.getRandomValues`, y solo `testing/encrypt.ts` la fija, para reproducir los fixtures de Go | `capsule.Encrypt`, `accesskey.Encode` |
| `encrypt.ts`, `writer.ts` | Los writers. `encryptFiles(files, opts)` escribe un `.dkc` de formato 3, como `capsule.EncryptFiles`: comprueba las rutas y los textos con las reglas del lector y con los textos de Go, pone los ficheros en el orden de los bytes de sus rutas, mide L con un head de sal y hashes a cero, lee cada fichero dos veces y falla si cambió entre las dos lecturas; el head, el control y el área de seguridad se decodifican antes de escribir. El área de seguridad mide 32 KiB sea lo que sea lo que guarde la cápsula (§62.1, regla 13), y va vacía o con la firma y el sello de los enganches de Go: `authorKey` firma con `alg` 1 (un `AuthorKey` de `authorkey.ts` o cualquier `AuthorSigner`), `cmsSigner` con `alg` 2, la firma con certificados, y `sealer` pide el sello de `seal_type` 2; `largeArea` deja ensanchar el área a 64 KiB solo si lo firmado no cabe en 32 KiB. Los enganches pueden ser asíncronos. Se comprueban como en `newSealer` de Go, en su orden y con sus textos: una sola firma, y con `cmsSigner` los sellos van dentro de cada firma. Se llaman cuando el control y el head ya son los finales y antes de escribir nada: la firma se compromete con ellos y el sello con la firma (§29.8, §29.11). El área se evalúa con el lector de la librería en el contexto de la cápsula antes de escribirla, como `security` de Go, y una firma que no daría F4 o F6, o un sello que no daría S4 o S5, la hace fallar con el texto de Go (reglas 17, 19 y 21). Desde la v0.16, `Encrypted.security` da los veredictos del área escrita, como `Result.Security` de Go: un sello sin `accuracy` se escribe, pero da S5, o la línea de su firmante en F6, con su motivo, para que quien escribe avise de él y ofrezca pedir otro (regla 19). Lo que lanza `cmsSigner` o `sealer` llega con `capsule: signing: ` o `capsule: sealing: ` y su mensaje, y el error como `cause`. `publicNote` es la nota pública de la cabecera (§24.1), que se rechaza con los textos de `extension.CheckNote` tras `capsule: `, y nunca se corrige; las opciones se comprueban en el orden de `newSealer` de Go. Con un área de 512 bytes, que solo puede pedir un generador de vectores, reproduce byte a byte `PRELUDE`, PUBLIC_HEADER, CONTROL_CBOR y `BODY` de los cinco fixtures que escribió `EncryptFiles` en la v0.10. `fileSource` hace la fuente de un `File`.<br>El formato 2 solo lo escribe un generador de vectores (§62.1, regla 1). `encrypt(src, opts)` tiene la forma de `capsule.Encrypt`, pero sus opciones no pueden pedirlo, así que falla con el texto de Go. Lo que solo pide un generador, el formato 2 y otra área (`TestVectors`, como `EncryptOptions.TestVectors` de Go), solo lo pasan al núcleo los ayudantes de `testing/encrypt.ts` (`encryptVectors`, `encryptWith` y `encryptFilesWith`), que ninguna página puede cargar. Con ellos, las pruebas y los scripts escriben un `.dkc` de formato 2, sin head, área ni nota, y, si se pide, una `.dkk` portable (§61, §62, §62.1), en el orden y con los textos y códigos de `capsule.Encrypt`:<br>- el formato 2 siempre; L conocida de antemano (el tamaño de un `Uint8Array` o un `Blob`, o `length` con un `ReadableStream`), y una fuente que da más o menos bytes falla con los textos de Go;<br>- el relleno `reforzado` por defecto, o `bloque256`;<br>- de 1 a 16 credenciales, canónicas y no de orden bajo, un señuelo en cada hueco libre, cuyo escalar se borra al derivar su clave pública, y un orden uniforme de los 16 (`random.ts`);<br>- `SEALED_CONTROL_LEN` con la fórmula del §62.1, comprobada con el sellado real;<br>- las autocomprobaciones de la regla 11 y dos más: `OUTER_TIME_AGE` con las reglas del lector, y la cabecera de `PAYLOAD_AGE`, que `I_PAYLOAD` abre antes de escribir nada.<br>Nada se escribe hasta que todo lo anterior al contenido está comprobado. El contenido va en trozos de 64 KiB, seguido de los ceros del relleno, con presión inversa, hacia memoria (hasta `MAX_MEMORY_DKC`, 1 GiB) o hacia `output`, que se cierra solo con la cápsula completa y comprobada y se aborta ante cualquier fallo. Los errores de la fuente y de la salida se relanzan tal cual.<br>El núcleo, `writer.ts`, recibe la aleatoriedad de quien lo llama: `encrypt.ts` le da la de `crypto.getRandomValues`, y solo `testing/encrypt.ts` la fija, para reproducir los fixtures de Go | `capsule.Encrypt`, `accesskey.Encode` |
| `tlock.ts` | `timeRecipient`, el `Recipient` de `age-encryption` para `OUTER_TIME_AGE` (§32, §35), como `agewrap.TimeRecipient`: cifra la file key con `ibe.ts` para una ronda de un perfil pinneado y escribe el stanza `tlock <ronda> <chain hash>` de tlock. Comprueba el perfil y luego el rango de la ronda, con los textos de `NewTimeRecipient`. `age-encryption` no tiene etiquetas, así que quien escriba `OUTER_TIME_AGE` (fase 3) lo añade como único recipient | `agewrap.TimeRecipient` |
| `lengths.ts` | El tamaño de un `.dkc` de formato 2 o 3 antes de escribirlo. Para el formato 3, `bodyLength` da L con `headLength`, que mide el head por los tamaños de sus elementos CBOR sin codificarlo ni cargar las tablas de Unicode, y `mtimeSeconds` y `headComment` dan la mtime y el comentario tal como el writer los guarda. Para los dos formatos: `sealedControlLength`, la fórmula de `SEALED_CONTROL_LEN` del §62.1 con la que el writer comprueba su sellado, y `capsuleLength`, el tamaño exacto que escriben los writers para una ronda, una política, L, el relleno, las extensiones y la nota pública, que la página muestra antes de cifrar porque cualquiera con el fichero lo ve (§55.2). Sin noble, `age-encryption` ni tablas de Unicode | `capsule.Encrypt`, que mide un borrador sellado |
| `padding.ts` | El relleno del formato 2 (§29.1): los códigos 1 (`bloque256`) y 2 (`reforzado`), `paddedLength`, exacta hasta L_MAX = 2⁵³ − 2⁴⁶ (`bitlen` con `BigInt` y los redondeos con `ceil`, exactos en doubles; nunca operaciones de 32 bits, `Math.clz32` ni `Math.log2`), y la longitud de `PAYLOAD_AGE` | `capsule/padding.go` |
@ -71,16 +73,18 @@ La inspección (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `bech32.ts` | Bech32 (BIP 173) tal como `internal/bech32` de `age`, que la referencia copia como `codec/bech32`; conserva su aviso MIT | `codec/bech32` |
| `datekey.ts` | `dk1_` canónico con las reglas de lectura de §19 (CR, LF y todo carácter fuera del alfabeto fallan el paso 1; números JSON por su valor decimal exacto), ronda desde una fecha con precisión de nanosegundos y cota de 9999-12-31T23:59:59Z (§15), parser RFC 3339 equivalente a `time.Parse(time.RFC3339Nano, …)`; `compareInstants` e `isInstant`; `LONG_HORIZON_SECONDS` e `isLongHorizon`, el umbral de 365 días de los avisos de §53 y §50, una política de producto | `datekey` |
| `header.ts`, `control.ts`, `accesskey.ts` | PUBLIC_HEADER, CONTROL_CBOR y `.dkk` (cuerpo y trama), decodificar y codificar, con las capas de §69.1. CONTROL_CBOR se lee y se escribe para un formato: versión de schema 1 sin las claves 6 y 7, o 2 y 3 con `payload_length` (8 bytes, hasta L_MAX) y `padding` (1 o 2); 103 bytes sin extensiones sea cual sea L | `capsule`, `accesskey` |
| `body.ts`, `security.ts`, `head.ts` | El formato 3 (§29.2 a §29.7): la trama de `BODY` (`AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`) y los ceros del área, `ERR_INTEGRITY`; el `SECURITY_CBOR` que escriben los writers, vacío o con la firma y el sello (`encodeSecurityWith`, `encodeAuthorSignatureItem` y `encodeSealItem`), y los veredictos de la v0.11 con sus textos y las líneas que los muestran, las de un certificado con los textos del borrador v0.12 (cada nombre entre « y », la autoridad del sello de cada firmante de F6 y el aviso de que DateKeys no comprueba quién emitió los sellos): X; F0 a F6, con la firma de `alg` 1 y la de `alg` 2 comprobadas en el contexto de la cápsula; y S0 a S5, con el sello de `seal_type` 2. `evaluateSecurity` nunca lanza: una excepción al evaluar la firma da F1, y una al evaluar el sello, S2, cada una sin tocar el otro veredicto. Y el head, con las capas de §69.1: R1 y R8 en el CDDL, R8 por los bytes UTF-8 y no por el orden UTF-16 de las cadenas de JavaScript, y luego el comentario, el autor declarado, las rutas, la maquetación, R7 y R9, todo `ERR_HEAD_INVALID`, y las extensiones críticas del objeto `head` | `capsule/format3.go`, `capsule/signature.go` |
| `body.ts`, `security.ts`, `head.ts` | El formato 3 (§29.2 a §29.7): la trama de `BODY` (`AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`) y los ceros del área, `ERR_INTEGRITY`; el `SECURITY_CBOR` que escriben los writers, vacío o con la firma y el sello (`encodeSecurityWith`, `encodeAuthorSignatureItem` y `encodeSealItem`), y los veredictos de la v0.11 con sus textos y las líneas que los muestran, las de un certificado con los textos del borrador v0.12 (cada nombre entre « y », la autoridad del sello de cada firmante de F6 y el aviso de que DateKeys no comprueba quién emitió los sellos): X; F0 a F6, con la firma de `alg` 1 y la de `alg` 2 comprobadas en el contexto de la cápsula; y S0 a S5, con el sello de `seal_type` 2. Desde la v0.16, S5 no tiene texto fijo: «No acredita que se sellara antes de la fecha de apertura: ‹motivo›.», con el motivo de `sealReasonText`, y la línea de un firmante de F6 cuyo sello no lo acredita dice «sin acreditar que fuera antes de la fecha de apertura: ‹motivo›». `evaluateSecurity` nunca lanza: una excepción al evaluar la firma da F1, y una al evaluar el sello, S2, cada una sin tocar el otro veredicto. Y el head, con las capas de §69.1: R1 y R8 en el CDDL, R8 por los bytes UTF-8 y no por el orden UTF-16 de las cadenas de JavaScript, y luego el comentario, el autor declarado, las rutas, la maquetación, R7 y R9, todo `ERR_HEAD_INVALID`, y las extensiones críticas del objeto `head` | `capsule/format3.go`, `capsule/signature.go` |
| `authorkey.ts` | Las claves de autor de `alg` 1 (§29.9, §29.12), como el paquete `authorkey` de Go en `spec-v0.12`, con sus comprobaciones en su orden y sus textos byte a byte: `AuthorKey` (`generate` con una fuente de azar inyectable, `fromSeed`, `publicKey`, `sign`, `clear`, `secret`, y `toString`, `toJSON` y `util.inspect` que ocultan el secreto), `authorPublicString`, `parseAuthorPublic` (canónica, en la curva y no de orden pequeño), `parseAuthorSecret` y `marshalAuthorKey`. Las cadenas se leen como bytes de Go: la mayúscula y la minúscula son las de `strings.ToLower` y `strings.ToUpper` de Go, y los espacios de una línea los de `strings.TrimSpace`, con las tablas de `gounicode.ts`; un `Uint8Array` es una cadena de Go que puede no ser UTF-8. El fichero de clave: `encryptAuthorKey` lo escribe con el `Encrypter` de `age-encryption` y una frase de paso, scrypt con logN 16; `readAuthorKey` lee uno cifrado o en claro de hasta 64 KiB, con las líneas de `bufio.Scanner`, y del cifrado lee la cabecera con `age.ts`, comprueba el stanza scrypt como `ScryptIdentity` de Go con un factor máximo de 16, deja a `age-encryption` el scrypt y el MAC, y descifra el STREAM, todo con los textos de `age` de Go. A diferencia de Go, una clave borrada lanza al usarla. JavaScript no promete tiempo constante ni borrar la memoria: se borran las copias propias, no las del motor, `age-encryption` o noble, ni las cadenas. En Node 24.9, firmar tarda unos 6 ms, y escribir o leer un fichero de clave cifrado, unos 0,3 s, los del scrypt de 64 MiB | `authorkey` |
| `ed25519sign.ts` | La firma Ed25519 (RFC 8032, 5.1.5 y 5.1.6) en código propio: `crypto_sign` de TweetNaCl, como el port de Dart, con el SHA-512 de `@noble/hashes`. Aritmética exacta en `Float64Array` (16 miembros de 16 bits en el cuerpo, 64 de 8 bits para los escalares módulo ℓ); los secretos nunca pasan por `BigInt`, una rama o un índice que dependa de ellos. Da la firma de Go byte a byte, también si la clave pública que se le da es otra | `crypto/ed25519` |
| `gounicode.ts` | Las tablas de `unicode.ToLower`, `unicode.ToUpper` y `unicode.IsSpace` de Go 1.26 (Unicode 15.0.0) que usan `strings.ToLower`, `strings.ToUpper` y `strings.TrimSpace`, en tramos. Las genera `scripts/go-unicode-tables.go` con el SHA-256 de cada conjunto, que una prueba recalcula | `unicode` |
| `author.ts` | Lo que se firma y se sella (§29.8, §29.11): `payload_commit`, `control_commit` sobre `CONTROL_SIG`, `head_digest`, `signers_digest`, `AUTHOR_MESSAGE` (99 bytes de ASCII) y su código, que se toma byte a byte como en Go, `SIG_PART` y `SEAL_SUBJECT`. Nada se guarda: todo se recalcula de la cápsula abierta | `capsule/signature.go` |
| `ed25519strict.ts` | La verificación estricta de la firma de `alg` 1 (§29.9): las cuatro condiciones del perfil, con la aritmética del grupo de `@noble/curves`, cuyo `verify` usa la ecuación con cofactor y acepta lo que el perfil rechaza. Da la respuesta de Go en los 18 vectores de `ed25519_strict.json` | `internal/ed25519strict` |
| `der.ts` | La comprobación estricta de DER que hace la firma CMS antes de mirar dentro (§29.10): longitudes definidas y mínimas, BOOLEAN, INTEGER, NULL, OID y BIT STRING canónicos, UTCTime y GeneralizedTime en sus formas de X.690 y con una fecha que existe (`parseTime` los lee), solo los tipos universales que usan los certificados, las firmas y los tokens, los tipos de cadena restringidos entre ellos, y una profundidad de 32; `setOfSorted` para los SET OF cuyo esquema conoce quien llama, que pueden repetir un elemento | `internal/der` |
| `cms.ts` | La firma CMS (RFC 5652) de `alg` 2 y el token RFC 3161 del sello (§29.10, §29.11), con el orden de comprobaciones y los resultados de Go y la tabla cerrada de algoritmos: RSA PKCS #1 v1.5 y PSS con `BigInt`, de 2048 a 4096 bits y con un módulo impar, y ECDSA sobre P-256, P-384 y P-521 con la aritmética de `@noble/curves`, solo con el punto sin comprimir. Lee el certificado campo a campo con el perfil del §29.10 del borrador v0.12, como Go: a quién nombra (su `givenName` y su `surname` antes que su `commonName`), el emisor que dice (su `commonName` o su `organizationName`), su validez y su clave; nunca comprueba quién lo emitió ni si se revocó. Compara los OID por los bytes de su DER, acepta un SET OF que repite un elemento y cuenta como uno un certificado repetido, y un nombre conserva un U+FEFF inicial, como en Go | `internal/cms` |
| `securitycms.ts` | Los veredictos de una firma de `alg` 2, F1, F2, F5 y F6, con cada firmante requerido y ajeno nombrado como el §29.7 del borrador v0.12 (su nombre si cumple las reglas del autor declarado, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no el SHA-256 del certificado; su emisor, o el SHA-256 de su `Name`; y la autoridad de su sello), y los de un sello, S1 a S5, con su autoridad y t. `encodeSigners` escribe `SIGNERS` para el escritor, ordenado y con los textos de Go | `capsule/signature2.go` |
| `cms.ts` | La firma CMS (RFC 5652) de `alg` 2 y el token RFC 3161 del sello (§29.10, §29.11), con el orden de comprobaciones y los resultados de Go y la tabla cerrada de algoritmos: RSA PKCS #1 v1.5 y PSS con `BigInt`, de 2048 a 4096 bits y con un módulo impar, y ECDSA sobre P-256, P-384 y P-521 con la aritmética de `@noble/curves`, solo con el punto sin comprimir. Lee el certificado campo a campo con el perfil del §29.10 del borrador v0.12, como Go: a quién nombra (su `givenName` y su `surname` antes que su `commonName`), el emisor que dice (su `commonName` o su `organizationName`), su validez y su clave; nunca comprueba quién lo emitió ni si se revocó. Compara los OID por los bytes de su DER, acepta un SET OF que repite un elemento y cuenta como uno un certificado repetido, y un nombre conserva un U+FEFF inicial, como en Go. Del token lee además, desde la v0.16, si lleva `accuracy` (`hasAccuracy`) y su política, y `tokenIsBTSP` dice si es la BTSP de ETSI EN 319 421 (0.4.0.2023.1.1) | `internal/cms` |
| `securitycms.ts` | Los veredictos de una firma de `alg` 2, F1, F2, F5 y F6, con cada firmante requerido y ajeno nombrado como el §29.7 del borrador v0.12 (su nombre si cumple las reglas del autor declarado, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no el SHA-256 del certificado; su emisor, o el SHA-256 de su `Name`; y la autoridad de su sello), y los de un sello, S1 a S5, con su autoridad y t. Desde la v0.16, un sello válido es S4, o la línea de un firmante dice «antes de la fecha de apertura», solo si el token lleva `accuracy` y t más la precisión es anterior a `round_time`; si no, S5, con el primer motivo que se cumple (`SealReason`, como en Go): `late`, sellado después o demasiado cerca; `no accuracy, BTSP`, sin `accuracy` en un token de la política BTSP, que la exige; o `no accuracy`. `encodeSigners` escribe `SIGNERS` para el escritor, ordenado y con los textos de Go | `capsule/signature2.go`, `capsule/format3.go` (`SealReason`) |
| `note.ts` | La nota pública (§24.1): la extensión `datekeys.note` de la cabecera, de 1 a 1024 bytes de UTF-8 que cumplen las reglas del autor declarado, con los textos y el orden de `extension.CheckNote`. Una cadena con un sustituto suelto se rechaza: nunca se escribe con U+FFFD. `checkNoteData` comprueba los bytes de una nota en el orden de Go: la longitud, el UTF-8 y las reglas. `publicNote` la lee como Go, con un U+FEFF inicial que la deja inservible, y `unusableNote` distingue una nota inservible de ninguna | `extension` (`CheckNote`, `NewNote`, `Note`), `capsule` (`Header.UnusableNote`) |
| `wordkey.ts` | La llave de palabras (§38.1): `normalizeWords` (NFD con las tablas de Unicode 18.0.0, sin las marcas U+0300 a U+036F, la minúscula simple de cada punto de código, partido por los espacios de la lista), `checkWords` (al menos 6 palabras distintas de 3 letras o más, sin controles, invisibles ni puntos sin asignar), `wordRules` (las dos, con las tablas cargadas una vez) y `wordKey`, PBKDF2-SHA256 de 600 000 rondas de Web Crypto con la sal de la cadena, la ronda y `capsule_id`. `quickWords`, `hiddenCodePoint` y `countedWords` leen con las tablas de la plataforma, para un formulario que no puede esperar a las de Unicode 18.0.0 | `wordkey.Normalize`, `Check`, `Key` |
| `wordlist.ts` | Las palabras al azar de la llave de palabras (el SHOULD de §38.1), con los textos de Go: `generateWords` sortea `DEFAULT_WORD_COUNT` palabras distintas, 7, con `randomIndex` y `crypto.getRandomValues`; `wordBits` da su fuerza, 90 bits para 7 de 7 776; `checkWordList` rechaza una lista de menos de 2 048 palabras, con dos que son una al normalizarlas o con un carácter que no es una letra del alfabeto de su idioma, que da el código y no la lista (para `es`, de la `a` a la `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` y `ñ`; para `en`, de la `a` a la `z` y el guion de las cuatro palabras compuestas de la lista de la EFF): una letra cirílica que parece latina se volvería a escribir con la latina, y la cápsula no se abriría; y `readWordList` solo acepta una lista con el SHA-256 fijado para su idioma en `WORD_LIST_SHA256`, que sea UTF-8 y que `checkWordList` acepte. Ninguna lista se da por buena, tampoco las de DateKeys. Los dados, para quien no se fía del azar del ordenador: `diceNumber` numera las palabras de una lista de 7 776 del 11111 al 66666, `diceWord` da la palabra de un número de cinco dados, `diceWords` las de varios (al menos 6 y ninguna repetida) y `diceList` es la lista numerada para imprimirla, como la publica la EFF: la de `en` es su fichero, byte a byte | `wordkey.Generate`, `CheckList`, `Bits`, `List`, `DiceNumber`, `DiceWord`, `DiceWords`, `DiceList` |
| `pathrule.ts`, `pathrule-tables.ts` | Las reglas de las rutas y de los textos del head (§29.5, §29.6), de R1 a R10 con R4b, R6b, R6c y R9, NFD, el pliegue y la clave de R7, con los textos de error de Go. Nunca usa `normalize`, `toLowerCase`, `localeCompare`, `Intl` ni las clases `\p{…}`, cuya versión de Unicode cambia con el motor: las tablas de Unicode 18.0.0 y WindowsBestFit las genera `datekeys-go` (`pathrule/gen -ts`), y una prueba recalcula su digest | `internal/pathrule` |
| `open3.ts`, `sink.ts` | El paso 17 del formato 3 en sus subpasos 17.2 a 17.8, con la precedencia de Go: un fallo de `age` o un texto en claro que no mide P prevalecen, manda el primer subpaso que falla, y los códigos distintos de `ERR_INTEGRITY` solo se dan tras leer `PAYLOAD_AGE` hasta el final. `Sink` (`begin`, `create`, `commit`, `abort`) recibe los ficheros y solo los publica en el paso 18; `MemorySink` los guarda en memoria, que crece con los bytes recibidos y nunca con los tamaños que declara el head | `capsule/open3.go` |
| `framing.ts` | Prelude DKC1 (16 bytes), con el formato de la cápsula, de 1 a 3, y DKK1 (12 bytes) en el orden de §23 y §40, longitudes de 1 byte hasta los límites de §57, y troceo de secciones | `capsule/framing.go` |
@ -155,7 +159,7 @@ Todo el texto leído de la cápsula pasa por interpolación de texto de Svelte (
Tras los pasos 1 a 8, si la cápsula es válida, la página ofrece abrirla: pasos 9 a 18 de §63 con `open` (fase 2, paso 8). Si según el reloj del dispositivo la fecha no ha llegado, lo dice, no ofrece pedir la firma a drand y deja dar un release que la persona ya tenga, porque el reloj puede ir atrasado (v0.15, paso 9.c); si ese release abre la cápsula, el resultado dice que el reloj parece ir atrasado.
- **El release lo da quien abre** (decisión 4 del plan de la fase 2, confirmada el 28-09-2026): la página nunca lo pide a la red. Se pega la respuesta JSON de drand o la firma sola en hexadecimal (`release-input.ts`), y solo se leen `round` y `signature`: cualquier otro campo, como una clave pública, se ignora, porque la raíz de confianza es el perfil fijado (§11, §13). La página enlaza la URL de drand de esa ronda (`https://api.drand.sh/<chainhash>/public/<ronda>`, con `rel="noopener noreferrer"`), que abre la persona en otra pestaña. Es un release que suministra quien llama: el paso 10 lo verifica y da sus códigos (`ERR_ROUND_MISMATCH`, `ERR_RELEASE_INVALID`). En los fixtures oficiales el campo viene relleno con el release de su registro, que es el que publicó drand. Desde la v0.15 también se puede elegir un fichero con el release: el objeto release, la respuesta de drand en JSON o un archivo de releases local. Va tal cual a `open` como release en la mano (`opener.ts`), y el paso 10 lo lee con los códigos de Go; un fichero de más de 8 KiB que no es un archivo de releases no se lee, y la página lo dice. Lo pegado va como el JSON de drand, que no nombra cadena.
- **El release lo da quien abre** (decisión 4 del plan de la fase 2, confirmada el 28-09-2026): la página nunca lo pide a la red. Se pega la respuesta JSON de drand o la firma sola en hexadecimal (`release-input.ts`), y solo se leen `round` y `signature`, con el lector estricto de la librería (v0.16, §47.1), para que la página nunca lea otra ronda que la del paso 10: un nombre repetido es JSON no válido, `ROUND` es otro campo y la ronda va de 1 a 2⁵³ − 1. Cualquier otro campo, como una clave pública, se ignora, porque la raíz de confianza es el perfil fijado (§11, §13). La página enlaza la URL de drand de esa ronda (`https://api.drand.sh/<chainhash>/public/<ronda>`, con `rel="noopener noreferrer"`), que abre la persona en otra pestaña. Es un release que suministra quien llama: el paso 10 lo verifica y da sus códigos (`ERR_ROUND_MISMATCH`, `ERR_RELEASE_INVALID`). En los fixtures oficiales el campo viene relleno con el release de su registro, que es el que publicó drand. Desde la v0.15 también se puede elegir un fichero con el release: el objeto release, la respuesta de drand en JSON o un archivo de releases local. Va tal cual a `open` como release en la mano (`opener.ts`), y el paso 10 lo lee con los códigos de Go; un fichero de más de 8 KiB que no es un archivo de releases no se lee, y la página lo dice. Lo pegado va como el JSON de drand, que no nombra cadena.
- **Credenciales**, solo en `time_and_key`: una `.dkk` (se leen como mucho 16 MiB + 13 bytes, `readAccessKey`) o identidades `AGE-SECRET-KEY-1…`, una por línea, como un fichero de identidades de `age`. La página explica de dónde sale la identidad: del fichero que se creó con `age-keygen`, que se puede pegar entero. Un error de una línea se da por su número, nunca por su contenido, y el foco va al campo; las identidades se borran tras usarlas. En `time_only` la página no las pide y `open` no las usa.
- **El código de la apertura se carga bajo demanda**: la página importa `opener.ts` con `import()` al pulsar "Abrir", y con él `open.ts`, noble y `age-encryption`. `opening.ts`, que construye lo que se muestra, solo importa tipos de `open.ts`. `check-build.mjs` comprueba que ninguna página carga noble, `@scure/base` ni `age-encryption` en la primera carga.
- **El texto en claro** se muestra, cuando la cápsula se abre y es texto (`plaintextPreview`): UTF-8 imprimible, hasta 100 000 caracteres de sus primeros 128 KiB. Se muestra el CR LF de Windows como salto de línea y se omite el BOM, como hace un editor; la descarga conserva los bytes exactos. Lo que no es texto solo se ofrece para descargar. El de un fixture se abre en memoria, con su SHA-256 comparado con el del registro. El contenido va justo debajo del veredicto, antes de los pasos 9 a 18.
@ -181,6 +185,7 @@ Medido en Chromium (el navegador de la app de escritorio) sobre la compilación
- **Las rutas se pueden editar**, y se comprueban a medida que se escriben con las reglas del lector (`create-check.ts`, bajo demanda con las tablas de Unicode): cada problema en su fila, en español y con su regla, y un choque de R7 en las dos rutas. Una prueba de propiedad exige que la página no vea ningún problema exactamente cuando `checkPath` y `checkTree` aceptan las rutas. Una lista de más de 2000 ficheros se vuelve a comprobar tras una pausa al escribir, con el problema de cada ruta y la clave de cada segmento en memoria; se muestran los 500 primeros y los que han tenido un problema. Cada fila da el tamaño, la fecha y la ruta original si cambió.
- **El comentario y el autor declarado** son opcionales, se comprueban igual (§29.6) y van cifrados en el head, con el comentario en LF. La casilla de la fecha de modificación, marcada por defecto, guarda la de cada fichero, que se omite si cae fuera de 1970 a 9999 (§62.1, regla 16). Una cápsula puede guardar solo un comentario (decisión 9).
- **El formulario** (`create-input.ts`) mide los ficheros una vez por cambio de la lista (`measureFiles`), y con ellos da el tamaño exacto del `.dkc` a cada cambio del comentario, del autor o de la fecha. Después, el día y la hora, en la zona del dispositivo, en otra de las que conoce el navegador o en UTC. Y la política: «solo con la fecha» (`time_only`, la de por defecto) o «con la fecha y una clave» (`time_and_key`). Esta lleva destinatarios `age1…`, uno por línea y comprobados mientras se escriben (`recipient.ts`, sin noble), y la casilla de la clave portable, marcada por defecto; como mucho 16 credenciales. Una ayuda plegable explica qué es un destinatario de `age` y cómo se consigue: quien abrirá la cápsula crea su par con `age-keygen` y envía solo su `age1…`. Los campos se comprueban en su orden, y el foco va al campo del primer problema: una ruta que no vale lleva el foco a su fila.
- **La llave de palabras**, con la política «con llave»: una casilla la añade a la `.dkk` y a las personas con `age`, con palabras al azar por defecto (el SHOULD de §38.1). La primera vez, la página descarga del propio sitio la lista española de `datekeys-go` (`create-words.ts`), la acepta solo con su SHA-256 fijado y si `checkWordList` la da por buena, y sortea 7 palabras con `crypto.getRandomValues`, que no salen del navegador. Las muestra numeradas, porque se escriben en ese orden, con su fuerza calculada con la lista cargada (unos 90 bits) y un botón para sacar otras. «Con dados», para quien no se fía del azar del ordenador, convierte los números de cinco dados que escribe la persona en las palabras de la lista, a medida que los escribe, con su problema si un número no vale o repite una palabra, y ofrece la lista numerada para imprimirla, con su SHA-256. «Las elijo yo» deja escribir las propias, con el aviso de que son más débiles. En todos los casos hay que escribirlas otra vez, y dan igual mayúsculas y tildes.
- **La hora local** (`localtime.ts`) se convierte al instante UTC con `Intl` y las reglas de zona que conoce hoy el navegador. Una hora que la zona se salta al adelantar los relojes se rechaza. De una que repite al atrasarlos se toma la más tardía, para no abrir nunca antes de lo querido. La cápsula no guarda la zona.
- **Antes de cifrar**, la página muestra el instante efectivo, que es el de la ronda, en la zona elegida y en UTC, y cuánto cae después del pedido. También la ronda, la `dk1_`, la hora del dispositivo junto a la UTC, el contenido, el tamaño exacto del `.dkc` (`capsuleLength` con `bodyLengthOf`) y lo que la cápsula deja ver hasta la fecha (§55.2). Y los avisos: el de protocolo preliminar (§74), siempre; los de §53 y §50, con sus textos, si el instante efectivo está a más de 365 días (`LONG_HORIZON_SECONDS`); y uno informativo si está a menos de una hora. La hora del dispositivo se lee cada segundo mientras la pestaña se ve y al crear la cápsula; la página no la pregunta a ningún servidor. Si el navegador no conoce la zona del dispositivo (V8 da entonces `Etc/Unknown`), la página empieza en UTC.
- **El writer se carga bajo demanda**: la página importa `creator.ts` con `import()` al pulsar «Crear la cápsula», y con él `encrypt.ts`, noble, `age-encryption` y las tablas de Unicode de las rutas. El relleno es siempre `reforzado`, y no hay extensiones. `encryptFiles` lee cada fichero dos veces, y la página muestra el progreso de las dos pasadas: la primera, en bytes de los ficheros, contada por `creator.ts` con un stream propio que lee a demanda; la segunda, en bytes del `.dkc`. «Cancelar» detiene cualquiera de las dos, y un fichero que cambia entre ellas hace fallar la escritura. Lo escrito no se ofrece si no mide lo que se mostró o si los pasos 1 a 8 lo rechazan, y ante cualquier fallo se borra la `.dkk`. Si la fecha llega mientras la página se prepara para escribir, el writer la rechaza con su propio reloj (§62.1, regla 2), y la página lo dice en el campo de la fecha.
@ -188,6 +193,7 @@ Medido en Chromium (el navegador de la app de escritorio) sobre la compilación
- **La `.dkk`** (152 bytes sin extensiones) vive solo en la memoria de la página: nunca en OPFS, y nunca se muestra como `AGE-SECRET-KEY-1…`. Sus bytes se borran al pulsar «Olvidar la clave», al crear otra cápsula y al salir, y con ellos las URL de sus descargas. Junto a ella va el aviso de §7.4. Si la cápsula solo se abre con su `.dkk` y no se ha descargado, la página pide confirmación antes de borrarla, al olvidarla o al crear otra, y avisa antes de salir: el navegador pregunta al cerrar o recargar, y la página, al seguir un enlace del sitio. Salir de la página también cancela una escritura en curso.
- **Las descargas** van por separado, con nombres que se pueden editar. Por defecto son `capsula-<apertura en UTC>.dkc` y `.dkk`, que no dicen cuándo se creó la cápsula. La URL `blob:` de cada descarga se revoca un minuto después del clic.
- **El resultado** muestra el `capsule_id`, la política, el contenido, el tamaño y el relleno, y el informe de los pasos 1 a 8 del `.dkc` escrito, con el componente del inspector.
- **Lo que pide el SDK oficial al sellar** (§7.6, §62.1 reglas 26 y 27, §71). Con una fecha a más de un año y la política «solo fecha», la página recomienda la llave, y la opción «solo fecha» lo recuerda para algo valioso. Tras crear la cápsula, «Para abrirla más adelante» dice qué hará falta: la cápsula, una de sus llaves si la lleva, y la firma de drand de su ronda, que tendrá que guardar un archivo de firmas o un servicio de caché si drand ya no la sirve; y ofrece las instrucciones para abrirla sin DateKeys (`annex.ts`), el anexo del §79 que `datekeys-go` copia en `annex/`, con el nombre de la cápsula y `.recuperacion.txt`. `planCapsule` no escribe con un perfil que no esté activo en el registro de §71, y `/inspect` avisa si el de una cápsula está comprometido.
Comprobado el 30-09-2026 en Chromium, con el formato 3:
- una lista de dos ficheros y una carpeta con `.DS_Store`, un fichero bajo `__MACOSX`, una ruta con «:», dos nombres que solo cambian en mayúsculas y una fecha de 1969: los dos del sistema, tachados; el problema de R4 y el choque de R7, en sus filas; la fecha de 1969, omitida;
@ -215,7 +221,7 @@ Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una
| `src/lib/inspector/diagnostic.ts` | Notación de diagnóstico CBOR (RFC 8949 §8) de `walk`, acotada a 16 384 caracteres |
| `src/lib/dkc/prefix.ts` | Lectura por prefijo, que también usa la apertura: de un `.dkc` grande solo se leen 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN + 2 MiB + 1 bytes, y solo 16 si los pasos 1 y 2 rechazan el prelude (otro tipo de fichero, un `.dkk`, longitudes fuera de §57), siempre con el mismo resultado que el fichero entero (lo comprueba `prefix.test.ts`) |
| `src/lib/inspector/fixtures.ts` | Los fixtures oficiales, empaquetados desde `testdata/fixtures` |
| `src/lib/inspector/release-input.ts` | Lee el release pegado (respuesta de drand o firma sola) y construye la URL de drand de la ronda; un fichero con el release lo lee `opener.ts` |
| `src/lib/inspector/release-input.ts` | Lee el release pegado (respuesta de drand, con `strictJSON` y `jsonRound` de la librería, o firma sola) y construye la URL de drand de la ronda; un fichero con el release lo lee `opener.ts` |
| `src/lib/inspector/opener.ts` | La apertura, cargada bajo demanda: identidades, `.dkk`, `open` con el release suministrado, SHA-256 del texto en claro |
| `src/lib/inspector/opening.ts` | `buildOpenReport`: el modelo de la apertura (pasos 9 a 18, release, extensiones de CONTROL_CBOR, texto en claro), sin DOM ni reloj; nombre del fichero descifrado |
| `src/lib/inspector/tempfile.ts` | El fichero temporal de OPFS, con un directorio y un Web Lock por pestaña y por zona (la apertura y crear), la cuota libre y la limpieza de lo que quedó. Su `writable` acepta además trozos con posición (`TempChunk`), como `FileSystemWritableFileStream`, para parchear el CRC-32 del ZIP |
@ -226,13 +232,15 @@ Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una
| `src/lib/inspector/create-files.ts` | La lista de ficheros de `/create`, sin tablas: lo elegido y lo soltado, carpetas recorridas incluidas, los ficheros de un sistema fuera por defecto como en `collect.go`, las rutas editadas y lo que la cápsula guarda |
| `src/lib/inspector/create-check.ts` | Las reglas de las rutas y de los textos (§29.5, §29.6) como las explica `/create`: todos los problemas de todas las rutas, en español, con los de `pathrule.ts`. Se carga bajo demanda, con las tablas |
| `src/lib/inspector/create-input.ts` | El formulario de `/create`, sin DOM, reloj ni writer: `planCapsule` comprueba los campos en su orden y da lo que se muestra antes de cifrar, con los ficheros medidos una vez (`chooseFiles`); los destinatarios y los nombres de los ficheros |
| `src/lib/inspector/annex.ts` | El anexo de recuperación que `/create` ofrece junto a la cápsula: `annex/recovery.md`, que Vite publica con un nombre con hash, y el nombre de su fichero, el de la cápsula con `.recuperacion.txt`, como `RecoveryAnnexSuffix` de Go |
| `src/lib/inspector/create-words.ts` | La lista de las palabras al azar de `/create`: `wordListLoader` descarga una vez `wordlists/es.txt`, que Vite publica con un nombre con hash, y la lee con `readWordList`, con su SHA-256 fijado; un fallo no se guarda y la siguiente llamada lo vuelve a intentar |
| `src/lib/inspector/creator.ts` | La escritura, cargada bajo demanda: `encryptFiles` con los ficheros, el comentario y el autor hacia el fichero temporal o la memoria, con el progreso de las dos lecturas, la cancelación y la cuota, y los pasos 1 a 8 de lo escrito |
| `src/routes/` | Layout, portada, inspector y crear |
### Fixtures
`fixtures.ts` importa con `import.meta.glob` los `.dkc` de `testdata/fixtures` como URL (`?url`) y, de cada registro JSON, solo tres campos públicos: `description`, `release` (la ronda y la firma que publicó drand, con las que la página abre el fixture) y `plaintext_sha256` (para comparar con lo descifrado). `testdata/` sigue siendo la única fuente: Vite copia cada `.dkc` como fichero con hash en `_app/immutable/assets/` y nunca lo incrusta como `data:` (`assetsInlineLimit: 0`), y no se copia nada más. Los `.dkk`, los textos en claro y los demás campos de los registros (`payload_identity`, `control_cbor`…) no llegan al sitio; `check-build.mjs` lo comprueba. En desarrollo, `server.fs.allow` deja que Vite sirva `testdata/fixtures`.
`fixtures.ts` importa con `import.meta.glob` los `.dkc` de `testdata/fixtures` como URL (`?url`) y, de cada registro JSON, solo tres campos públicos: `description`, `release` (la ronda y la firma que publicó drand, con las que la página abre el fixture) y `plaintext_sha256` (para comparar con lo descifrado). `testdata/` sigue siendo la única fuente: Vite copia cada `.dkc` como fichero con hash en `_app/immutable/assets/` y nunca lo incrusta como `data:` (`assetsInlineLimit: 0`), y no se copia nada más. Los `.dkk`, los textos en claro y los demás campos de los registros (`payload_identity`, `control_cbor`, `words_text`…) no llegan al sitio; `check-build.mjs` lo comprueba. La descripción de `format3_time_and_key_words` (v0.16) nombra sus palabras, las del vector público del anexo, 79.7, así que en la página se abre escribiéndolas. En desarrollo, `server.fs.allow` deja que Vite sirva `testdata/fixtures`.
- `connect-src`: `fetch` llega al propio origen, para los fixtures, y a los tres relays públicos de drand que usa la CLI de la referencia, solo cuando la persona pulsa «Pedir la firma a drand» en `/inspect` (`drand.ts`: en carrera, 6 s, como mucho 8 KiB, sin redirecciones, y la aleatoriedad comprobada contra la firma, que el paso 10 verifica con la clave fijada). El relay ve la IP y la ronda pedida. Tampoco llega a URL `blob:`: la descarga del texto en claro es una navegación.
- `connect-src`: `fetch` llega al propio origen, para los fixtures, y a los tres relays públicos de drand que usa la CLI de la referencia, solo cuando la persona pulsa «Pedir la firma a drand» en `/inspect` (`drand.ts`: en carrera, 6 s, como mucho 8 KiB, sin redirecciones, y la respuesta leída con `parseDrandJSON`, el lector estricto que usa desde la v0.16 el cliente de Go, con la aleatoriedad comprobada contra la firma, que el paso 10 verifica con la clave fijada). El relay ve la IP y la ronda pedida. Tampoco llega a URL `blob:`: la descarga del texto en claro es una navegación.
- `script-src`: los módulos del sitio y el hash SHA-256 del único script en línea, el arranque de SvelteKit (los nonces no sirven en HTML prerenderizado).
- `style-src 'self'`: solo hojas de estilo del sitio; sin fuentes web ni CDN, con las fuentes del sistema.
- `style-src-attr`: solo el atributo `style` del anunciador de rutas de SvelteKit, por su hash (`ANNOUNCER_STYLE_HASH`, válido para `@sveltejs/kit` 2.70.3; `app.css` lo oculta también si el navegador bloquea el atributo).
`npm run build` ejecuta después `scripts/check-build.mjs` (`postbuild`; también `npm run build:check`), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en `script-src` o sobra un hash; si `style-src-attr` no coincide con los atributos `style` del bundle; si hay estilos en línea, manejadores de eventos en atributos, `@import` o URL a otro origen; si algún `.dkc` oficial no está byte a byte; si aparece en el sitio algún secreto de los fixtures (`.dkk`, textos en claro, identidades, `payload_identity`, `access_material`, `control_cbor`); si el bundle del cliente contiene `tlock-js`, `drand-client` o helpers de Babel, o una copia anidada de un paquete que no sea la de noble bajo `@noble/post-quantum`; si contiene un test o un módulo de `src/lib/dkc/testing/`, cuyos ayudantes escriben lo que solo puede escribir un generador de vectores; si una página carga noble, `@scure/base` o `age-encryption` en su primera carga, o las claves de autor y su firma (`authorkey.ts`, `ed25519sign.ts` y `gounicode.ts`), que `/inspect` no carga ni bajo demanda, o si `/inspect` y `/create` no pueden cargar bajo demanda `age-encryption`, `@noble/curves` y `@noble/ciphers`, y `/create` también `@noble/hashes`; o si a `licenses.txt` le falta el aviso de un paquete del bundle, las líneas de copyright de un módulo de `src/` derivado de otro proyecto o la licencia del sitio. `vite.config.ts` registra los módulos de cada chunk en `.svelte-kit/output/client-modules.json`, fuera del sitio. Al terminar informa del JavaScript que carga cada página, en bytes y con gzip, en la primera carga y bajo demanda, y de los paquetes npm que lleva el bundle.
`npm run build` ejecuta después `scripts/check-build.mjs` (`postbuild`; también `npm run build:check`), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en `script-src` o sobra un hash; si `style-src-attr` no coincide con los atributos `style` del bundle; si hay estilos en línea, manejadores de eventos en atributos, `@import` o URL a otro origen; si algún `.dkc` oficial no está byte a byte; si aparece en el sitio algún secreto de los fixtures (`.dkk`, textos en claro, identidades, el texto de una llave de palabras, `payload_identity`, `access_material`, `control_cbor`); si el bundle del cliente contiene `tlock-js`, `drand-client` o helpers de Babel, o una copia anidada de un paquete que no sea la de noble bajo `@noble/post-quantum`; si contiene un test o un módulo de `src/lib/dkc/testing/`, cuyos ayudantes escriben lo que solo puede escribir un generador de vectores; si una página carga noble, `@scure/base` o `age-encryption` en su primera carga, o las claves de autor y su firma (`authorkey.ts`, `ed25519sign.ts` y `gounicode.ts`), que `/inspect` no carga ni bajo demanda, o si `/inspect` y `/create` no pueden cargar bajo demanda `age-encryption`, `@noble/curves` y `@noble/ciphers`, y `/create` también `@noble/hashes`; o si a `licenses.txt` le falta el aviso de un paquete del bundle, las líneas de copyright de un módulo de `src/` derivado de otro proyecto o la licencia del sitio. `vite.config.ts` registra los módulos de cada chunk en `.svelte-kit/output/client-modules.json`, fuera del sitio. Al terminar informa del JavaScript que carga cada página, en bytes y con gzip, en la primera carga y bajo demanda, y de los paquetes npm que lleva el bundle.
### Avisos de licencia: `licenses.txt`
El JavaScript minimizado no conserva comentarios, así que el sitio publica `licenses.txt`, enlazado desde el pie de cada página. Lo escribe un plugin de `vite.config.ts` al compilar el cliente, con tres partes:
El JavaScript minimizado no conserva comentarios, así que el sitio publica `licenses.txt`, enlazado desde el pie de cada página. Lo escribe un plugin de `vite.config.ts` al compilar el cliente, con cuatro partes:
- los módulos de `src/` derivados de otros proyectos, con el aviso de su cabecera: `ibe.ts` (de `tlock-js`, MIT) y `bech32.ts` (de `age`, MIT);
- el fichero de licencia de cada paquete npm con algún módulo en el bundle;
- el `README.md` de `wordlists/`, con el origen, el método y la licencia de las listas de palabras que publica el sitio (CC BY-SA 4.0 la española);
- la licencia Apache-2.0 del sitio.
En un hosting estático basta con servir `build/`. Las directivas que solo funcionan como cabecera HTTP (`frame-ancestors`, `sandbox`, `report-to`) quedan para el servidor que la aloje. `crypto.subtle` exige contexto seguro: `https`, o `http` en `localhost`.
@ -295,12 +304,13 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
- `testdata/vectors/padding.json`: P con las dos reglas y la longitud de `PAYLOAD_AGE` de cada L, incluidas las fronteras en que fallan las operaciones de 32 bits y un logaritmo en coma flotante, y las longitudes por encima de L_MAX, que se rechazan. `padding.test.ts` contrasta además `paddedLength` con el §29.1 escrito en `BigInt` sobre 20 000 longitudes de todo el rango.
- `testdata/vectors/tlock_ibe.json`: el vector de H2 del IBE de tlock (§63 paso 11). Hasta que llegue `ibe.ts` (fase 2), el test lo recalcula con `@noble/curves` 2.4.0: los puntos son canónicos para `bls12381.ts`, el pairing serializado en el orden de kilic es el GT del vector y su H2 coincide; el orden propio de noble (`Fp12.toBytes`) da otro hash.
- `testdata/vectors/tlock_steps.json`: los pasos 10 y 11 de §63 para Quicknet, valor a valor (v0.14), con el código de la librería: M con `roundIdentity`, H(M) con `hashToG1`, el release con `verifyRelease` y la ecuación de pairing; las tres partes del stanza con `ciphertextFromBody`, e(firma, U) con `gtBytes`, H2, sigma, H4 y la file key; la base de H3 con `h3Base`, cada intento con `h3Try` y su digest, el desplazamiento del primer byte y su aceptación, r con `h3`, y r·G2 = U con `proofHolds`; y `decryptOnG2` sobre el cuerpo entero. También las lecturas erróneas que descarta el generador: el DST de G2 o la ronda sin SHA-256 no verifican, y poner a cero el bit más alto en vez de desplazar el byte da otra r, que U no prueba; una firma de otra ronda y un V o un W editados no descifran.
- `testdata/vectors/release.json` y `releases/` (v0.15): los 36 objetos y los 12 JSON de drand pasan por `parseRelease` y `verifyRelease` frente a Quicknet y la ronda de cada caso, con el resultado, el texto de Go y lo que decodifican; un objeto se reescribe a sus bytes. Cada fichero de `releases/` es el objeto de su ronda, que verifica, y las 7 consultas del archivo dan lo que dice el fichero, en memoria y desde un `Blob`. La guarda de `testdata` exige cada fichero de `releases/`.
- `testdata/vectors/release.json` y `releases/` (v0.15): los 36 objetos y los 38 JSON de drand, 26 de ellos de la lectura estricta de la v0.16, pasan por `parseRelease` y `verifyRelease` frente a Quicknet y la ronda de cada caso, con el resultado, el texto de Go y lo que decodifican; un objeto se reescribe a sus bytes. Cada fichero de `releases/` es el objeto de su ronda, que verifica, y las 7 consultas del archivo dan lo que dice el fichero, en memoria y desde un `Blob`. La guarda de `testdata` exige cada fichero de `releases/`.
- `testdata/vectors/mutations.json`: se leen los 222 casos enteros (ediciones sobre un fixture o hex congelado, release, su fuente, reloj, registro, extensiones, `.dkk`, identidades y, en los once del formato 3 que abren, sus veredictos). Cada caso usa la fuente que dice su campo `source`, como `singleSource` de Go: `supplied`, un release en la mano que se da a `open` como objeto release, con la cadena de Quicknet salvo que el caso nombre otra, o `network`, una fuente que verifica el release y lo descarta. Los 222 pasan por `open` con su release, su reloj, su registro, sus extensiones, su `.dkk`, sus identidades y un `MemorySink`, y dan el mismo código en el mismo paso que Go y el mismo texto que `capsule.Open` (`testing/mutation-texts.json`); los cuatro de seguridad del formato 3 abren con los veredictos X, F1, F2 y S1 del registro, y siete de la lista de la v0.11 con los suyos. «round not reached yet», del formato 1, abre con el release en la mano y el reloj atrasado. También pasan como `Blob` con un stream de salida, que termina abortado en los 210 que fallan, y un sumidero que nunca se publica. Son los 178 del §64: las 33 mutaciones de las dos primeras listas en cada formato, las 23 de la lista del formato 2, las 48 de la del formato 3 y las 8 de la lista de la v0.11; y 44 más. Ninguno de los que fallan sin red pide un release. Los 57 de los pasos 1 a 8 pasan además por `inspect`, y un test fija los recuentos y el orden. Las `.dkk` ofrecidas se decodifican.
- `testdata/vectors/inspect_differential.json`: las 5 110 mutaciones de los catorce fixtures de base dan el mismo veredicto, código y paso que Go; los `bases` se comprueban por su SHA-256.
- `testdata/vectors/paths.json` y `path_fold.json`, con las tablas cuyo digest nombran: cada ruta pasa por las reglas de una entrada con el texto exacto, cada árbol por la decodificación de un head de ficheros de 0 bytes, y cada segmento da su NFD y su clave de R7.
- `testdata/vectors/head_schema.json` y `security.json`: cada head pasa por `decodeHead` con el código de la primera capa que falla y el texto exacto de `ERR_HEAD_INVALID`, y un head válido se reescribe a sus bytes; cada área de seguridad da, en el contexto del fichero, sus veredictos y sus líneas.
- `testdata/vectors/security_cms.json`: los 135 casos de `alg` 2 y de `seal_type` 2 dan, en su contexto, los veredictos, el resultado de cada firmante exigido y de cada ajeno, la autoridad y la hora del sello y las líneas de Go, byte a byte. `ed25519_strict.json` lo corre `ed25519strict.test.ts`.
- `testdata/vectors/security_cms.json`: los 143 casos de `alg` 2 y de `seal_type` 2, hechos de nuevo para la v0.16, dan, en su contexto, los veredictos, el resultado de cada firmante exigido y de cada ajeno, la autoridad y la hora del sello, el motivo (`seal_reason`) de un S5 o de un firmante cuyo sello no acredita la fecha, y las líneas de Go, byte a byte.
- `format3_time_and_key_words` y `format3_full_chunk` (v0.16): el primero abre con la identidad que dan las palabras de su `words_text`, normalizadas con `normalizeWords` y derivadas con `wordKey` con la cadena, la ronda y su `capsule_id`, que es la de `identities`, y también en la página con el texto tal cual y con sus marcas sueltas; el segundo, cuyo `PAYLOAD_AGE` acaba en un trozo STREAM completo, abre a su fichero. `ed25519_strict.json` lo corre `ed25519strict.test.ts`.
- `testdata/vectors/note.json`: cada nota pasa por `checkNoteData`, con su resultado y el texto exacto de la regla que incumple, y por `publicNote`, `unusableNote` y `newNote`.
- `testdata/vectors/locator.json`: se corre entero, como `TestLocatorVectors` de Go, con los textos de Go de `testing/locator-vectors.json` y `testing/locator-uris.json`: la extensión se lee, el localizador se abre con el release de su ronda y da su texto en claro de 4096 bytes y sus campos, el resto se encuentra en el host en su desplazamiento y abre el sobre; las 36 bases de relleno; las 247 direcciones, con el veredicto del fichero y el texto de Go; el localizador mixto, del que se usa solo la dirección que se acepta y que un escritor no escribe; los 5 restos, los 8 datos de la extensión, con `ERR_EXTENSION_DATA_INVALID` como único código, y los 16 textos en claro.
- `src/lib/dkc/testing/locator-uris.json` y `locator-vectors.json`: el localizador sin su escritura contra Go en `spec-v0.12`, que comprueban `locator.test.ts` y `envelope.test.ts`, todo con el resultado y el texto de Go. Los escribe `scripts/locator-go-vectors.go`, el generador de la etapa 7a de `datekeys-dart` con las semillas de este repositorio:
@ -312,7 +322,7 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
- `src/lib/dkc/testing/locator-seal.json`: lo que escriben `Seal` y `NewEnvelope` de Go mientras `crypto/rand` lee el keystream de una semilla (ChaCha20 bajo el SHA-256 de la semilla, nonce a cero), con cada valor que saca. Lo escribe `scripts/locator-seal-go-vectors.go`, y `envelope.test.ts` lo comprueba con `seededFill` de `testing/seeded.ts`, la misma fuente: `seal` y `newEnvelope` sacan los mismos valores en el mismo orden y escriben los mismos bytes en los 10 sellados, de uno a tres bloques y de la ronda 1 a la última de Quicknet, en los 8 sobres, de 0 bytes a 1 MiB, y en el camino entero; y rechazan las 12 entradas que rechaza Go, con su texto y antes de sacar nada. Se regenera como el anterior, con `-source spec-v0.12`.
- `src/lib/dkc/testing/locator-interop.json`: Go abre lo que escribe esta librería. `scripts/locator-ts-samples.mjs` escribe los ficheros de las recetas de `testing/locator-interop.ts`, siete localizadores sellados con sus sobres, de un `.dkc` de 0 bytes a uno de 16 MiB y un byte, en el que el contador del nonce de STREAM pasa de un byte; `scripts/locator-go-verdicts.go` los abre con `locator.Open`, comprueba que `Marshal` da el texto sellado y que se usan todas sus direcciones, abre el sobre con `OpenEnvelope` y busca el resto escondido con `Hide` y `RestIn`. `locator.interop.test.ts` vuelve a escribir cada fichero de su receta, exige el SHA-256 que leyó Go y lo abre también. Para regenerarlo: `node scripts/locator-ts-samples.mjs DIR`, y desde la exportación de `datekeys-go`, `go run .../scripts/locator-go-verdicts.go -source spec-v0.12 -testdata .../testdata -samples DIR > locator-interop.json`.
- `src/lib/dkc/testing/zip-vectors.json`: cómo lee `archive/zip` de Go los ZIP de la página. `scripts/zip-ts-samples.mjs` escribe con `ZipSink` las muestras de `testing/zip.ts` en un directorio: ficheros en carpetas con nombres fuera de ASCII y todas las clases de fecha (1970, 2³¹ − 1, 2³¹, 9999 y ninguna, que toma la hora de la ronda), un fichero dentro de una carpeta, y 65 535 ficheros, que hacen el ZIP64 por número de entradas. `scripts/zip-go-read.go`, solo con la biblioteca estándar, registra el SHA-256 de cada ZIP y una línea por entrada: nombre, bit 11, método, CRC-32, tamaños, fecha leída de los campos extra y SHA-256 del contenido, leído con el CRC comprobado. `zipsink.test.ts` vuelve a escribir cada muestra, exige su SHA-256 y calcula las líneas que Go tiene que dar. Para regenerarlo: `node scripts/zip-ts-samples.mjs DIR > zip-samples.json` y `go run scripts/zip-go-read.go DIR zip-samples.json > zip-vectors.json`.
- `src/lib/dkc/testing/mutation-texts.json`: el texto del error de `capsule.Open` para cada caso de `mutations.json`, u `ok`. Lo escribe `scripts/mutation-go-texts.go`, que reproduce los casos como `internal/testkit` de la referencia, que un módulo de fuera no puede importar: el perfil de Quicknet o ninguno, una fuente con el release del caso, como `OpenOptions.Release` o como `OpenOptions.Source` según su campo `source` (v0.15), la `.dkk` ya decodificada, las identidades, las extensiones del caso con los textos de `testkit.KnownExtensions` y un sumidero que descarta. Comprueba cada código contra el corpus. Para regenerarlo, desde un módulo Go temporal como el de abajo: `go run mutation-go-texts.go ../datekeys-ts/testdata > mutation-texts.json`.
- `src/lib/dkc/testing/mutation-texts.json`: el texto del error de `capsule.Open` para cada caso de `mutations.json`, u `ok`. Lo escribe `scripts/mutation-go-texts.go`, que reproduce los casos como `internal/testkit` de la referencia, que un módulo de fuera no puede importar: el perfil de Quicknet o ninguno, una fuente con el release del caso, como `OpenOptions.Release` o como `OpenOptions.Source` según su campo `source` (v0.15), la `.dkk` ya decodificada, las identidades, las extensiones del caso con los textos de `testkit.KnownExtensions` y un sumidero que descarta. Comprueba cada código contra el corpus. Para regenerarlo, desde un módulo Go temporal como el de abajo: `go run mutation-go-texts.go ../datekeys-ts/testdata > mutation-texts.json`. Se regeneró con `4f78854` (v0.16): solo cambió su campo `spec`.
- `src/lib/dkc/testing/ibe-vectors.json`: los valores de referencia de `ibe.ts`. Los escribe `scripts/ibe-go-vectors.go` con kyber, tlock y `age`, las librerías de la referencia Go, y `ibe.test.ts` los comprueba todos:
- el GT de e(G1, G2) y de su cuadrado, con H2 de 16 y 32 bytes;
- H3 y H4 sobre entradas fijas, entre ellas una H3 aceptada en la segunda iteración y otra en la tercera;
@ -327,7 +337,7 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
Para regenerarlo: `node scripts/tlock-ts-samples.mjs > ts-samples.json`, y desde el mismo módulo Go temporal, `go run tlock-go-vectors.go ts-samples.json > tlock-vectors.json`.
En `ibe-vectors.json`, H2, H3 y H4 no son públicas en kyber: el script las reescribe con sus etiquetas y las comprueba en cada fixture contra la file key de tlock y contra U = r·G2. Los cifrados de kyber usan un sigma aleatorio, así que el fichero se genera una vez y se congela. Para regenerarlo, desde un módulo Go temporal que requiera la referencia (`replace g.activething.com/go/DateKeys => ../datekeys-go`, `GOFLAGS=-mod=mod`, y la directiva `go` de la referencia, para que se use su toolchain): `go run ibe-go-vectors.go ../datekeys-ts/testdata/fixtures > ibe-vectors.json`. Los siete fixtures del formato 2 se añadieron el 29-09-2026 así, sobre `spec-v0.9`, tomando solo el bloque `fixtures`: los valores de los cinco anteriores salieron idénticos, y el resto del fichero no cambió. Los nueve del formato 3 se añadieron igual el 30-09-2026, sobre `spec-v0.10`, con los doce anteriores idénticos. El 01-10-2026, sobre `spec-v0.11`, se añadieron `format3_signed`, `format3_signed_cms` y `format3_sealed`, y se rehicieron los dos de `format3_signature_unsupported` y `format3_seal_unsupported`, que Go regeneró con `alg` 4294967295; los demás salieron idénticos.
En `ibe-vectors.json`, H2, H3 y H4 no son públicas en kyber: el script las reescribe con sus etiquetas y las comprueba en cada fixture contra la file key de tlock y contra U = r·G2. Los cifrados de kyber usan un sigma aleatorio, así que el fichero se genera una vez y se congela. Para regenerarlo, desde un módulo Go temporal que requiera la referencia (`replace g.activething.com/go/DateKeys => ../datekeys-go`, `GOFLAGS=-mod=mod`, y la directiva `go` de la referencia, para que se use su toolchain): `go run ibe-go-vectors.go ../datekeys-ts/testdata/fixtures > ibe-vectors.json`. Los siete fixtures del formato 2 se añadieron el 29-09-2026 así, sobre `spec-v0.9`, tomando solo el bloque `fixtures`: los valores de los cinco anteriores salieron idénticos, y el resto del fichero no cambió. Los nueve del formato 3 se añadieron igual el 30-09-2026, sobre `spec-v0.10`, con los doce anteriores idénticos. El 01-10-2026, sobre `spec-v0.11`, se añadieron `format3_signed`, `format3_signed_cms` y `format3_sealed`, y se rehicieron los dos de `format3_signature_unsupported` y `format3_seal_unsupported`, que Go regeneró con `alg` 4294967295; los demás salieron idénticos. El 07-10-2026, sobre `4f78854` (v0.16), se añadieron `format3_full_chunk` y `format3_time_and_key_words`, con los 26 anteriores y el resto del fichero idénticos.
- El writer (`encrypt.test.ts`, `encrypt.stream.test.ts`, `encrypt.internal.test.ts`, `encrypt.property.test.ts`):
- con los valores de su registro, reproduce byte a byte el PRELUDE, PUBLIC_HEADER, `header_binding` y CONTROL_CBOR de los siete fixtures de formato 2 de Go, con las mismas longitudes; cada credencial cae en el hueco del registro y la `.dkk` sale igual, salvo su `capsule_digest`;
- lo que escribe se abre con `open`: las dos políticas, de 1 a 16 credenciales, cada una sola y todas juntas; contenidos en todos los bordes de trozo y de relleno, hasta 5 000 000 de bytes, con las dos reglas; rondas 1000, 1001 y 2000;
@ -348,13 +358,15 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
Para regenerarlo, en una exportación `git archive` de `datekeys-go` en el tag `spec-v0.12`, sin tocar el repositorio: las órdenes están en la cabecera del script. Todos los valores salen de Go, y cada ejecución escribe los mismos bytes.
- `src/lib/dkc/gounicode.ts` lo escribe `scripts/go-unicode-tables.go` con el toolchain de Go 1.26 (`go run scripts/go-unicode-tables.go -out src/lib/dkc/gounicode.ts`), y comprueba sus tramos contra las funciones de Go en cada punto de código.
- `src/lib/dkc/testing/signing-vectors.json`: los enganches del escritor contra `capsule.EncryptFiles` de Go, que comprueba `encrypt.signing.test.ts`. Lo escribe `scripts/signing-go-vectors_test.go`, que importa paquetes internos y corre como prueba en una exportación `git archive` de `spec-v0.12`:
- `src/lib/dkc/testing/signing-vectors.json`: los enganches del escritor contra `capsule.EncryptFiles` de Go, que comprueba `encrypt.signing.test.ts`. Lo escribe `scripts/signing-go-vectors_test.go`, que importa paquetes internos y corre como prueba en una exportación `git archive` de `datekeys-go`, hoy de `4f78854`:
- 23 recetas de formato 3 en `time_only`, para la ronda 1000. Go escribe cada cápsula con `crypto/rand` leyendo un flujo ChaCha20 fijo, y guarda cada valor en su orden, y lo que recibió y devolvió cada enganche: una clave de autor de una semilla, y las firmas CMS y los tokens RFC 3161 de `internal/cms/cmstest`, cuyos ECDSA y RSA fija `cryptotest.SetGlobalRandom`;
- con esos valores y esas firmas, `encryptFiles` escribe las ocho cápsulas de Go byte a byte: `alg` 1, `alg` 1 y un sello, un sello solo, uno posterior a la fecha (S5), `alg` 2 con dos firmantes sellados, `alg` 2 en un área de 64 KiB, `largeArea` sin ensanchar y un área de 512 bytes de generador de vectores. Pide la firma y el sello sobre los mismos mensajes, y lee en lo que escribe los veredictos y las líneas de `capsule.Open`, también con la clave guardada (F3). La prueba entrega a `age-encryption` y a `ibe.ts` los valores de Go por `crypto.getRandomValues`, sin el sellado de medida de Go, que esta librería calcula con una fórmula, ni las etiquetas al azar de sus recipients, que `age-encryption` no saca, y deja pasar el cegado de las multiplicaciones de noble, que no cambia ningún resultado;
- con esos valores y esas firmas, `encryptFiles` escribe las ocho cápsulas de Go byte a byte: `alg` 1, `alg` 1 y un sello, un sello solo, uno posterior a la fecha (S5, `late`), `alg` 2 con dos firmantes sellados, `alg` 2 en un área de 64 KiB, `largeArea` sin ensanchar y un área de 512 bytes de generador de vectores. Pide la firma y el sello sobre los mismos mensajes, y lee en lo que escribe los veredictos y las líneas de `capsule.Open`, también con la clave guardada (F3). La prueba entrega a `age-encryption` y a `ibe.ts` los valores de Go por `crypto.getRandomValues`, sin el sellado de medida de Go, que esta librería calcula con una fórmula, ni las etiquetas al azar de sus recipients, que `age-encryption` no saca, y deja pasar el cegado de las multiplicaciones de noble, que no cambia ningún resultado;
- las otras 15 fallan con el texto de Go: las exclusiones, el área de prueba con `largeArea`, una clave de 31 bytes, una firma de ceros (F2), una firma CMS sin un firmante exigido (F5, con el nombre), sin sellos o que no es una firma (F1), un área que no cabe en 32 KiB o en 64 KiB, `SIGNERS` vacío o repetido, y la aplicación de firma o la autoridad que fallan;
- `samples`: cinco cápsulas que `scripts/signing-ts-samples.mjs` escribe con `encryptFiles`, sus propios valores al azar, un `AuthorKey` nuevo y los certificados, firmas y tokens de `testing/cmsbuild.ts`. `capsule.Open` de Go las abre a sus ficheros y les da los mismos veredictos y las mismas líneas que esta librería.
Para regenerarlo: `node scripts/signing-ts-samples.mjs > ts-signing.json`, y la prueba de Go con `-samples ts-signing.json`; las órdenes están en la cabecera del script. Las cápsulas de Go salen igual en cada ejecución; las muestras son aleatorias y se congelan.
Se regeneró el 07-10-2026 en una exportación de `4f78854` (v0.16), con las mismas cinco muestras, cuyo `ts` se volvió a leer con esta librería. Las cápsulas y los errores salen byte a byte iguales; cambian lo que lee `capsule.Open` y un texto de error: los tokens del sellador de Go y los de las muestras no llevan `accuracy`, así que los cinco sellos válidos dan ahora S5, «el sello no dice su precisión», el posterior a la fecha da su motivo, `late`, y la firma de ceros con sello falla con «F2 and S5».
- `src/lib/dkc/testing/capsule-vectors.json`: la interoperabilidad de los writers con Go a nivel de cápsula (plan de la fase 3, sección 8, punto 9, y paso 4 del plan del formato 3), que comprueba `interop.test.ts`. Se regeneró el 02-10-2026 con el escritor de la v0.11, contra `spec-v0.11`. `scripts/capsule-ts-samples.mjs` escribe con `encryptVectors` de `testing/encrypt.ts`, como generador de vectores, trece cápsulas de formato 2 para las rondas 1000, 1001 y 2000:
- `time_only` de 0, 46, 65 536 y 78 000 bytes, con las dos reglas de relleno;
- `time_and_key` con una clave portable, con tres recipients y una clave portable, y con dieciséis recipients;
@ -455,7 +467,7 @@ Todas las versiones se fijan exactas y `package-lock.json` se versiona. `.npmrc`
npm run testdata:sync
```
Copia `testdata/` de `../datekeys-go` en `HEAD`, leyendo los blobs con git para no arrastrar cambios sin commit, y escribe `testdata/SOURCE.json` con el commit completo y el SHA-256 de cada fichero. Para fijar otro commit: `node scripts/sync-testdata.mjs sync --commit <rev>`.
Copia `testdata/` de `../datekeys-go` en `HEAD`, leyendo los blobs con git para no arrastrar cambios sin commit, y escribe `testdata/SOURCE.json` con el commit completo y el SHA-256 de cada fichero. Del mismo commit copia `wordkey/lists` en `wordlists/`, con su `wordlists/SOURCE.json`: las listas de las palabras al azar, que publica la página; y `annex/`, el anexo de recuperación que la página ofrece junto a cada cápsula, con su `annex/SOURCE.json`. Para fijar otro commit: `node scripts/sync-testdata.mjs sync --commit <rev>`.
```bash
npm run testdata:check
@ -463,10 +475,12 @@ npm run testdata:check
Comprueba que los ficheros coinciden con `SOURCE.json`, sin faltantes ni sobrantes, y vuelve a leerlos del repositorio Go en el commit registrado. Sin la opción `--against`, `node scripts/sync-testdata.mjs check` verifica solo la copia local. Ambos comandos usan solo Node y git.
`.gitattributes` marca `testdata/**` como binario para que git no altere ningún byte.
`.gitattributes` marca `testdata/**`y `wordlists/**`como binarios para que git no altere ningún byte.
Copia actual: la de `testdata/SOURCE.json` (el tag `spec-v0.15` de `datekeys-go`, `fe50885`), que se sincroniza con `node scripts/sync-testdata.mjs sync --commit spec-v0.15`. Añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`.
Copia actual: la de `testdata/SOURCE.json`, `datekeys-go` en `b6ff17a`, el commit del tag `spec-v0.16`, que se sincroniza con `node scripts/sync-testdata.mjs sync --commit b6ff17a`. Del mismo commit vienen `wordlists/` y `annex/`, el anexo de recuperación, que es ya el §79 de la v0.16, con la licencia CC BY-ND 4.0 de la especificación en su título y la llave de palabras en 79.7. Sobre el `testdata` de `spec-v0.15` (`fe50885`), el de la v0.16 cambia el campo `spec` de cada fichero a `0.16`, hace de nuevo `vectors/security_cms.json`, con 143 casos y `seal_reason`, añade a `vectors/release.json` los 26 casos de la lectura estricta del JSON de drand y a `vectors/wordkey.json` el vector del anexo, y trae dos fixtures, `format3_time_and_key_words` y `format3_full_chunk`. `wordkey/lists` trae la lista inglesa de la EFF y la española; la página solo publica la española.
## Licencia
Apache-2.0 ([LICENSE](LICENSE)), como la librería Go de referencia. El código que se derive de terceros conserva su aviso de copyright y licencia en el propio fichero: el núcleo IBE (`ibe.ts`), derivado de `tlock-js` (Apache-2.0 OR MIT, usado bajo MIT), y `bech32.ts`, portado de `age` (MIT). El sitio publica esos avisos y los de sus paquetes npm en `licenses.txt`.
Las listas de palabras de `wordlists/` no son código de este proyecto ni tienen su licencia: la inglesa es la de la EFF, CC BY 4.0, y la española es CC BY-SA 4.0, una adaptación de FrequencyWords de Hermit Dave. Su `README.md`, copiado de `datekeys-go`, dice de dónde sale, cómo se hizo y su licencia.
# Cómo abrir una cápsula DateKeys sin software de DateKeys
Este texto acompaña a una cápsula del tiempo de DateKeys, un fichero `.dkc`: dice cómo abrirla, llegada su fecha, sin ningún software de DateKeys, por si ya no existe. Es el anexo informativo §79 de la especificación del protocolo DateKeys v0.16, cuyo texto tiene el SHA-256 807d4fe85ac09ad6f97abc75ab3e2156bb2f3fb0dc589777f4420627fad545e1. Es el mismo para toda cápsula: no lleva ningún dato de esta. Su licencia es CC BY-ND 4.0, Atribución-SinDerivadas 4.0 Internacional (https://creativecommons.org/licenses/by-nd/4.0/deed.es): se puede copiar y compartir sin cambios, citando su origen.
## 79. Anexo informativo: recuperación sin software DateKeys
Este anexo no es normativo. Dice cómo abrir una cápsula de Quicknet sin ningún software de DateKeys, por si dentro de décadas no existe. Repite lo que fijan las secciones que cita, que deciden en caso de duda.
La regla 27 de §62.1 recomienda al SDK oficial guardar este anexo junto al `.dkc`. No contiene ningún dato de una cápsula.
Hace falta:
- el `.dkc`;
- el release de su ronda, de cualquier fuente: un relay de drand, un archivo de releases, un servicio de caché (§50) o cualquier copia. No hace falta confiar en quien lo da: se verifica con la clave pública de 79.1 (79.3);
- en `time_and_key`, una credencial: la `.dkk`, la identity `age` de un recipient o las palabras de una llave de palabras (§38.1);
- una librería de BLS12-381 con pairing y con el hash a G1 de RFC 9380, SHA-256, HMAC-SHA256, HKDF-SHA256 (RFC 5869), ChaCha20-Poly1305 (RFC 8439), un decodificador de CBOR y la herramienta `age` (§77) o una librería compatible;
- con una llave de palabras, PBKDF2-HMAC-SHA256 (RFC 8018) y, si las palabras llevan otras letras que las de 79.7, `UnicodeData.txt` de Unicode 18.0.0.
No sirven las herramientas de drand: `tle` pide el release a la red y no acepta uno dado, y `age` no acepta una file key, que es lo que da el stanza tlock (79.4). Por eso este anexo describe esos dos pasos enteros (79.4 y 79.5).
La implementación de referencia lo sigue en `scripts/recovery`, un programa que no importa ningún paquete de DateKeys, tlock ni drand: solo la librería estándar de Go, `golang.org/x/crypto`, `filippo.io/age` y la librería BLS12-381 `drand/kyber-bls12381`, con las interfaces de `drand/kyber`. `scripts/recovery_check.sh` abre con él una cápsula `time_only`, otra `time_and_key` con su `.dkk`, otra con una llave de palabras y otra cuyo `PAYLOAD_AGE` acaba en un bloque completo, de los fixtures oficiales. Las palabras se le dan en un fichero de texto, y `UnicodeData.txt`, si hace falta, en otro.
### 79.1 Parámetros de Quicknet
Son los de §12, y pueden no estar ya en ningún otro sitio:
Los puntos se codifican comprimidos, en el formato de ZCash (§12.2): 48 bytes en G1, la firma, y 96 en G2, la clave pública y U, con la coordenada c1 antes que c0.
### 79.2 La cápsula
El `.dkc` empieza por 16 bytes (§22):
```text
0 4 "DKC1"
4 1 VERSION: el formato, 1, 2 o 3
5 1 0
6 2 0
8 4 PUBLIC_HEADER_LEN, entero big-endian
12 4 SEALED_CONTROL_LEN, entero big-endian
```
Le siguen `PUBLIC_HEADER`, de `PUBLIC_HEADER_LEN` bytes; `SEALED_CONTROL`, de `SEALED_CONTROL_LEN` bytes; y `PAYLOAD_AGE`, desde el byte 16 + `PUBLIC_HEADER_LEN` + `SEALED_CONTROL_LEN` hasta el final del fichero.
`PUBLIC_HEADER` es un mapa CBOR (§24). Su clave 2 es `capsule_id`, 16 bytes; su clave 4, la política, 0 para `time_only` y 1 para `time_and_key`; y su clave 3, la DateKey, un texto `dk1_` seguido del Base64URL sin relleno de un JSON (§18):
Un objeto release es un mapa CBOR (§47.1): la clave 0 es `"datekeys-release"`; la 2, el `chain_hash`, que ha de ser el de 79.1; la 3, la ronda, que ha de ser la de la DateKey; y la 4, la firma, de 48 bytes. Así lo sirven la Release API y un servicio de caché. En un archivo de releases (§50), la firma de la ronda r son los 48 bytes que empiezan en |cabecera| + (r − primera ronda)·48, y 48 ceros si el archivo no la tiene. Un relay de drand la entrega como JSON, `{"round": …, "signature": "…"}`, con la firma en hexadecimal. Hoy se pide así, aunque las direcciones pueden cambiar:
```text
GET https://api.drand.sh/v2/chains/<chain_hash>/rounds/<ronda>
```
La firma no necesita confianza: se verifica (§63, paso 10). Con M el SHA-256 de la ronda en 8 bytes big-endian, y H el hash a G1 de RFC 9380 con la suite `BLS12381G1_XMD:SHA-256_SSWU_RO_` y el DST de 79.1:
```text
e(H(M), clave_pública) == e(firma, G2)
```
con e el pairing de G1 × G2, el primer argumento en G1 y el segundo en G2, y G2 el generador de G2. La firma y la clave pública se decodifican como puntos comprimidos válidos del subgrupo, distintos del punto en el infinito. Para una ronda solo hay una firma válida: cualquier copia que verifique es el release.
### 79.4 El stanza tlock y FK_TIME
`SEALED_CONTROL` es un fichero `age` (79.5) cuya cabecera tiene un solo stanza:
El cuerpo mide 128 bytes, U ‖ V ‖ W: U, de 96 bytes, un punto de G2, y V y W, de 16 bytes. Con la firma del release (§63, paso 11):
```text
sigma = V XOR H2(e(firma, U))
FK_TIME = W XOR H4(sigma)
r = H3(sigma, FK_TIME)
comprobar r·G2 == U
```
- H2(x) son los 16 primeros bytes de SHA-256(`IBE-H2` ‖ x), con x los 576 bytes del elemento de GT en el orden de §63, «Serialización de GT en H2»: c1 antes que c0 en cada nivel de la torre, y c2, c1, c0 en Fp6, cada elemento de Fp en 48 bytes big-endian. Una librería que serializa con c0 primero da otro H2. El vector de `testdata/vectors/tlock_ibe.json` lo comprueba: H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`, con G1 y G2 los generadores.
- H4(sigma) son los 16 primeros bytes de SHA-256(`IBE-H4` ‖ sigma).
- H3(sigma, FK_TIME): base = SHA-256(`IBE-H3` ‖ sigma ‖ FK_TIME); para i = 1, 2, … hasta 65534, d = SHA-256(uint16_le(i) ‖ base), con el contador en 2 bytes little-endian delante de base; se desplaza un bit a la derecha el primer byte de d, solo ese byte; y si d, como entero big-endian de 32 bytes, es menor que q, r = d.
Las etiquetas son los bytes ASCII, sin longitud ni terminador. FK_TIME, de 16 bytes, es la file key del fichero `age` de `SEALED_CONTROL`. `testdata/vectors/tlock_steps.json` da cada valor intermedio de cinco stanzas.
### 79.5 Abrir un fichero `age` con su file key
Es la especificación `age` v1 de C2SP (§77), resumida. Un fichero `age` es una cabecera de texto y un payload binario:
1. La cabecera: clave_mac = HKDF-SHA256(ikm = FK, salt = vacío, info = `header`), 32 bytes. El MAC es HMAC-SHA256(clave_mac, la cabecera desde `age-encryption.org/v1` hasta `---` inclusive, sin el espacio que lo sigue). Si no coincide con el de la línea `---`, la file key o la cabecera son otras.
2. El payload empieza tras el salto de línea del MAC por un nonce de 16 bytes. clave = HKDF-SHA256(ikm = FK, salt = nonce, info = `payload`), 32 bytes.
3. Lo demás son bloques de ChaCha20-Poly1305 de 65 536 bytes de texto, 65 552 cifrados; el último puede ser más corto, o estar completo: un plaintext de 65 536 bytes es un solo bloque, completo y marcado como último. El nonce de 12 bytes del bloque n, desde 0, es n en 11 bytes big-endian seguido de 0x01 en el último bloque y de 0x00 en los demás. No hay datos asociados. El último bloque solo puede estar vacío si es el único, y nada sigue al último bloque.
### 79.6 Las capas siguientes
El plaintext de `SEALED_CONTROL` es:
- en `time_only`, `CONTROL_CBOR`;
- en `time_and_key`, otro fichero `age`, `INNER_ACCESS_AGE`, con stanzas X25519, uno por credencial y señuelos hasta 16 en los formatos 2 y 3. Se abre con `age -d -i clave.txt`, con la identity de la credencial en `clave.txt`. La de una `.dkk` es su `access_material`. La `.dkk` empieza por 12 bytes, `DKK1`, `01`, `00`, `00 00` y `BODY_LEN` en 4 bytes big-endian (§40), y le sigue un mapa CBOR cuya clave 5 es ese `access_material`, 32 bytes (§41), y cuya clave 3 es el `capsule_id` de su cápsula. Una llave de palabras da la identity con 79.7.
`CONTROL_CBOR` es un mapa CBOR (§31). Su clave 3 es `I_PAYLOAD`, otra identity X25519 de 32 bytes, y en los formatos 2 y 3 su clave 6 es L, una cadena de 8 bytes con un entero big-endian, no un entero CBOR. Con `I_PAYLOAD`, `age -d -i payload.txt` abre `PAYLOAD_AGE`.
Una identity X25519 de 32 bytes se escribe para `age` en Bech32 (BIP 173, §77), no Bech32m: el prefijo `age-secret-key-`, los 32 bytes reagrupados de 8 en 5 bits con ceros al final, que dan 52 caracteres, y la suma de comprobación de BIP 173, calculada con el prefijo en minúsculas. Después, todo en mayúsculas: `AGE-SECRET-KEY-1…`.
### 79.7 La llave de palabras
Unas palabras dan la identity X25519 de una credencial (§38.1):
```text
P = las palabras normalizadas, en UTF-8, separadas por un espacio (0x20)
S = "DateKeys llave de palabras v2|" || chain_hash || "|" || ronda || "|" || capsule_id
En S, `chain_hash` es el de 79.1 y `capsule_id` el de 79.2, en hexadecimal en minúsculas, y la ronda, la de la DateKey en decimal, sin ceros a la izquierda. `id` es la identity, que se escribe para `age` como dice 79.6.
**Normalización sin tablas.** Si el texto solo lleva caracteres ASCII imprimibles (U+0021 a U+007E), los espacios U+0009 a U+000D y U+0020, las letras á, é, í, ó, ú, ü y ñ y sus mayúsculas, y marcas de U+0300 a U+036F, que es lo que da cualquier palabra de las listas de DateKeys, las palabras normalizadas salen así:
1. se quitan las marcas de U+0300 a U+036F;
2. á y Á pasan a a; é y É, a e; í e Í, a i; ó y Ó, a o; ú, ü, Ú y Ü, a u; ñ y Ñ, a n;
3. A a Z pasan a a a z;
4. se parte el texto por los espacios, sin palabras vacías.
El resto se queda: la puntuación y las cifras cuentan, y «perro,» no es «perro».
**Normalización completa.** Para cualquier otro texto, en este orden: la NFD de UAX #15, con la descomposición canónica de cada punto de código (el campo 5 de `UnicodeData.txt`, sin las que llevan una etiqueta entre `<` y `>`, aplicada hasta el final), la de las sílabas Hangul, que se calcula, y la reordenación canónica por la clase de combinación (campo 3); se quitan los puntos de código de U+0300 a U+036F; cada punto de código pasa a su minúscula simple (campo 13), si la tiene; y se parte por los espacios U+0009 a U+000D, U+0020, U+0085, U+00A0, U+1680, U+2000 a U+200A, U+2028, U+2029, U+202F, U+205F y U+3000, sin palabras vacías. Sirve cualquier copia de `UnicodeData.txt` de Unicode 18.0.0 cuyo SHA-256 sea `0736451de439ae7baf1425136617da495e09ee5afbe6e394374db7009ea08950`; unicode.org la publica en `https://www.unicode.org/Public/18.0.0/ucd/UnicodeData.txt`. Otra versión de Unicode puede dar otras palabras: una que asigne un punto de código nuevo, o que cambie una descomposición o una minúscula.
Vectores, con el chain hash de Quicknet, la ronda 1000 y `capsule_id` = `000102030405060708090a0b0c0d0e0f`:
- «perro luna casa verde tren mar» da `id` = `fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41`;
- el texto «Ñandú», dos espacios, «PINGÜINO», un tabulador (U+0009) y «camión árbol Éter ola» da «nandu pinguino camion arbol eter ola» e `id` = `273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7`, lo mismo que ese texto con las tildes, la diéresis y la tilde de la eñe escritas como marcas sueltas detrás de su letra (U+0301, U+0308 y U+0303).
### 79.8 El contenido
El plaintext de `PAYLOAD_AGE` es:
- en formato 1, el contenido entero;
- en formato 2, el contenido en sus L primeros bytes, seguido de ceros;
- en formato 3, `BODY` en sus L primeros bytes, seguido de ceros (§29.2).
`BODY` empieza por tres enteros de 4 bytes big-endian: `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`. Siguen el área de `security`, de `AREA_LEN` bytes, que se puede saltar: solo da los veredictos de la firma y del sello (§29.7); el head, un mapa CBOR de `HEAD_LEN` bytes que empieza en 12 + `AREA_LEN`; y los ficheros, desde 12 + `AREA_LEN` + `HEAD_LEN`, el origen de sus desplazamientos.
La clave 5 del head es la lista de ficheros (§29.4). Cada uno es un mapa:
```text
0 → ruta, con "/" entre carpetas
1 → tamaño
2 → start
3 → end
4 → SHA-256 de sus bytes
5 → mtime, en segundos Unix, opcional
```
Sus bytes van de start a end, sin incluir end, contados desde el origen. Las claves 3 y 4 del head son el comentario y el autor declarado: textos del creador que no prueban nada (§29.7). Una ruta que saldría de la carpeta de destino no se escribe.
### 79.9 Lo que el anexo no comprueba
Este anexo comprueba lo que decide que el resultado es el correcto: la firma del release, r·G2 == U, los MAC de cada fichero `age` y el SHA-256 de cada fichero. No comprueba, entre otras cosas, la codificación canónica de cada objeto, `header_binding` (§26), los 16 stanzas de `INNER_ACCESS_AGE` (§39), los ceros del relleno (§29.1) ni las reglas de las rutas (§29.5). Una cápsula que el lector de §63 rechazaría puede abrirse siguiendo este anexo; su contenido es el que sellaron las MAC de `age`, pero no tiene la garantía de un lector conforme.
expect((awaitopenedOf(sealed.dkc!)).lines).toEqual(['Sin firma de autor.','No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión.']);
// F6 and S4 write the names that the reader found (spec v0.12 §29.7, §29.10), each between « and »: a signer sealed
// before the round time or not, with the authority of its seal, the warning that DateKeys does not check who issued
// the seals, and a signer who does not count, with its result in Spanish.
// F6 and S4 write the names that the reader found (spec v0.16 §29.7, §29.10), each between « and »: a signer whose seal
// proves that it came before the round time or not, and then why not, with the authority of its seal, the warning
// that DateKeys does not check who issued the seals, and a signer who does not count, with its result in Spanish.
it('writes the lines of F6 and S4 from the signers and the authorities that were found',()=>{
constt={seconds: 1_790_000_000,nanos: 0};
constline=(holder: string,before: boolean,result='valid')=>({holder,issuer:`emisor de ${holder}`,result,sealHolder:`TSA de ${holder}`,sealTime: t,before});
'Firmado con un certificado a nombre de «Ana», «Luis». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
'Firmado con un certificado a nombre de «Ana», «Luis», «Eva». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
' «Ana» (emisor según su certificado: «emisor de Ana»), sellado por «TSA de Ana» el 2026-09-21T14:13:20Z, antes de la fecha de apertura.',
' «Luis» (emisor según su certificado: «emisor de Luis»), sellado por «TSA de Luis» el 2026-09-21T14:13:20Z, no antes de la fecha de apertura.',
' «Luis» (emisor según su certificado: «emisor de Luis»), sellado por «TSA de Luis» el 2026-09-21T14:13:20Z, sin acreditar que fuera antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.',
' «Eva» (emisor según su certificado: «emisor de Eva»), sellado por «TSA de Eva» el 2026-09-21T14:13:20Z, sin acreditar que fuera antes de la fecha de apertura: el sello no dice la precisión que exige su política.',
' DateKeys no comprueba quién emitió los sellos.',
' Otro firmante, «Otro»: inválida. No cuenta.',
'Según un sello a nombre de «TSA», existía el 2026-09-21T14:13:20Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
'Firmado con un certificado a nombre de «Ana». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
' «Ana» (emisor según su certificado: «CA»), sellado por «TSA» el 2026-09-21T14:13:20.25Z, no antes de la fecha de apertura.',
' «Ana» (emisor según su certificado: «CA»), sellado por «TSA» el 2026-09-21T14:13:20.25Z, sin acreditar que fuera antes de la fecha de apertura: el sello no dice su precisión.',
'Según un sello a nombre de «TSA», existía el 2026-09-21T14:13:20.000000001Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
expect(verdictLines({signature:'F5',seal:'S0',detail:{signers:[],foreign}})).toEqual([verdictText('F5'),` Otro firmante, «Otro»: ${text}. No cuenta.`]);
});
// The verdicts of spec v0.11 (§29.7) whose text is fixed, and those whose text names a key, a holder or a time, F3, F4, F6
// and S4, which verdictLines writes from what the reader found and verdictText leaves empty.
// The verdicts of spec v0.11 (§29.7) whose text is fixed, and those whose text names a key, a holder, a time or a
// reason, F3, F4, F6, S4 and, since v0.16, S5, which verdictLines writes from what the reader found and verdictText
// leaves empty.
it('has the text of the verdicts of v0.11 that do not name anything, and none for those that do',()=>{
expect(verdictText('F2')).toBe('La firma no corresponde a este contenido.');
expect(verdictText('F5')).toBe('Faltan firmas o sellos que la propia cápsula exige: trátala como no firmada.');
expect(verdictText('S3')).toBe('El sello no corresponde a este contenido.');
expect(verdictText('S5')).toBe('Sellado después de la fecha de apertura: no prueba nada anterior.');
expect(s5('late')).toBe('No acredita que se sellara antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.');
expect(s5('no accuracy')).toBe('No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión.');
expect(s5('no accuracy, BTSP')).toBe('No acredita que se sellara antes de la fecha de apertura: el sello no dice la precisión que exige su política.');
expect(verdictLines({signature:'F0',seal:'S5'})).toEqual(['Sin firma de autor.']);
@ -144,7 +170,7 @@ export function verdictLines(v: Verdicts): string[] {
if(v.signature==='F6'&&d!==undefined){
lines[0]=`Firmado con un certificado a nombre de ${d.signers.map((s)=>quoted(s.holder)).join(', ')}. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.`;
for(constsofd.signers){
constwhen=s.before?'antes de la fecha de apertura':'no antes de la fecha de apertura';
constwhen=s.before?'antes de la fecha de apertura':`sin acreditar que fuera antes de la fecha de apertura: ${sealReasonText(s.reason)}`;
lines.push(`${quoted(s.holder)} (emisor según su certificado: ${quoted(s.issuer)}), sellado por ${quoted(s.sealHolder!)} el ${formatRFC3339Nano(s.sealTime!)}, ${when}.`);
}
// §29.7: whoever says that a capsule was signed before the date says that it does not check who issued the seal.
@ -153,6 +179,8 @@ export function verdictLines(v: Verdicts): string[] {
for(constsofd?.foreign??[])lines.push(` Otro firmante, ${quoted(s.holder)}: ${RESULT_TEXT[s.result]!}. No cuenta.`);
if(v.seal==='S4'&&d?.sealHolder!==undefined){
lines.push(`Según un sello a nombre de ${quoted(d.sealHolder)}, existía el ${formatRFC3339Nano(d.sealTime!)}, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.`);
}elseif(v.seal==='S5'&&d!==undefined){
lines.push(`No acredita que se sellara antes de la fecha de apertura: ${sealReasonText(d.sealReason)}.`);
}elseif(verdictText(v.seal)!==''){
lines.push(verdictText(v.seal));
}
@ -335,7 +363,7 @@ function decodeAlg(d: Decoder): number {
// What the evaluation of the signature gives, and that of the seal.
expect(verdictLines(late)[1]).toBe(' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2030-01-01T01:00:00Z, no antes de la fecha de apertura.');
' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2030-01-01T01:00:00Z, sin acreditar que fuera antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.',
);
// t plus the accuracy of a second equal to the round time is not before it.
` «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2026-09-30T12:00:00Z, sin acreditar que fuera antes de la fecha de apertura: ${sealReasonText(reason)}.`,
expect(verdictLines(fraction)[1]).toBe(' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2026-09-30T12:00:00.25Z, antes de la fecha de apertura.');
@ -311,14 +336,15 @@ describe('a seal of seal_type 2', () => {
expect([v.signature,v.seal,v.detail!.sealHolder,v.detail!.sealTime,v.detail!.sealReason]).toEqual(['F4','S4','Autoridad de Sellado',at(signedAt),undefined]);
expect(verdictLines(v)[1]).toBe('Según un sello a nombre de «Autoridad de Sellado», existía el 2026-09-30T12:00:00Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.');
// Sealed with its own signature part: without key 2 the subject differs.
expect(out).toMatch(/^testdata: \d+ files match g\.activething\.com\/go\/DateKeys at [0-9a-f]{40}/);
constmatch=
/^testdata: \d+ files match g\.activething\.com\/go\/DateKeys at ([0-9a-f]{40})\nwordlists: \d+ files match g\.activething\.com\/go\/DateKeys at ([0-9a-f]{40})\nannex: \d+ files match g\.activething\.com\/go\/DateKeys at ([0-9a-f]{40})\n$/.exec(
"description":"The text of the error of capsule.Open for every case of testdata/vectors/mutations.json, or ok for a capsule that opens; see the header of scripts/mutation-go-texts.go.",
"Firmado con la clave dkauthor1pdrcy0n3p9watxl83tp8r3tkauuflpakg4s6kp70nf8te5pdypqszvracp. No prueba quién la tiene.",
"Según un sello a nombre de «TSA de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result":"ok",
"verdicts":[
"F4",
"S4"
"S5"
]
},
"opened_saved":{
@ -254,12 +254,12 @@
"length":32989,
"lines":[
"Firmado con la clave que guardaste como mi clave de 2026.",
"Según un sello a nombre de «TSA de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result":"ok",
"verdicts":[
"F3",
"S4"
"S5"
]
},
"recipe":{
@ -363,12 +363,12 @@
"length":32893,
"lines":[
"Sin firma de autor.",
"Según un sello a nombre de «TSA de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result":"ok",
"verdicts":[
"F0",
"S4"
"S5"
]
},
"recipe":{
@ -941,7 +941,7 @@
"length":32893,
"lines":[
"Firmado con la clave dkauthor1pdrcy0n3p9watxl83tp8r3tkauuflpakg4s6kp70nf8te5pdypqszvracp. No prueba quién la tiene.",
"Sellado después de la fecha de apertura: no prueba nada anterior."
"No acredita que se sellara antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella."
],
"result":"ok",
"verdicts":[
@ -963,7 +963,7 @@
"length":32893,
"lines":[
"Firmado con la clave que guardaste como mi clave de 2026.",
"Sellado después de la fecha de apertura: no prueba nada anterior."
"No acredita que se sellara antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella."
],
"result":"ok",
"verdicts":[
@ -1208,7 +1208,7 @@
"n":16
}
],
"error":"capsule: self-check: the reader finds the verdicts F2 and S4 in this security area, not F4 and S4",
"error":"capsule: self-check: the reader finds the verdicts F2 and S5 in this security area, not F4 and S4",
"Firmado con la clave dkauthor1xes558a6zzqw4urrz8c80u33znlv2yzc9t9f5ywa8a0c8ysq9atqt2k706. No prueba quién la tiene.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result":"ok",
"verdicts":[
"F4",
"S4"
"S5"
]
},
"opened_saved":{
@ -1890,23 +1890,23 @@
"length":32990,
"lines":[
"Firmado con la clave que guardaste como la clave de prueba.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result":"ok",
"verdicts":[
"F3",
"S4"
"S5"
]
},
"ts":{
"area_len":32768,
"lines":[
"Firmado con la clave que guardaste como la clave de prueba.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"verdicts":[
"F3",
"S4"
"S5"
]
}
},
@ -1931,23 +1931,23 @@
"length":32990,
"lines":[
"Sin firma de autor.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result":"ok",
"verdicts":[
"F0",
"S4"
"S5"
]
},
"ts":{
"area_len":32768,
"lines":[
"Sin firma de autor.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
awaitexpect(readWordList('es',bom,awaitpin('es',bom))).rejects.toThrow(/^wordkey: the list "es": line 1: wordkey: the words hold the invisible character U\+FEFF$/);
constlatin1=fileOf(base);
latin1[3]=0xe1;
awaitexpect(readWordList('es',latin1,awaitpin('es',latin1))).rejects.toThrow(/^wordkey: the list "es" is not UTF-8$/);
awaitexpect(readWordList('xx',ok,awaitpin('xx',ok))).rejects.toThrow(/^wordkey: the list "xx": no alphabet for the language "xx"$/);
});
});
describe('checkWordList',()=>{
it('accepts a list of 2048 different words of the alphabet, and refuses the cases of Go wordkey.TestCheckList',async()=>{
if(!alphabet.has(ch))thrownewError(`line ${i+1}, ${goQuote(w)}, holds U+${hex4(ch.codePointAt(0)!)}, which is not in the alphabet of ${goQuote(lang)}`);
}
constprev=seen.get(n[0]!);
if(prev!==undefined)thrownewError(`line ${i+1}, ${goQuote(w)}, is the same word as ${goQuote(prev)} once normalized`);
thrownewError(`capsule: self-check: the reader finds the verdicts ${v.signature} and ${v.seal} in this security area, not ${wantSig} and ${wantSeal}${detailText(v.detail)}`);
}
returnsecurity;
return{security,verdicts: v};
}
// What a hook returned, which must be bytes: a copy, which the hook cannot
constmany=`Una cápsula admite como mucho 16 llaves, contando el fichero .dkk y las palabras.`;
expect(problem({policy: TIME_AND_KEY,recipients: lines(16),portable: true})).toEqual(['recipients',`${many} Hay 16 personas.`]);
expect(problem({policy: TIME_AND_KEY,recipients: lines(17),portable: false})).toEqual(['recipients',`${many} Hay 17 personas.`]);
expect(problem({policy: TIME_AND_KEY,recipients: lines(15),portable: true,words:'perro luna casa verde tren mar',wordsAgain:'perro luna casa verde tren mar'})).toEqual(['recipients',`${many} Hay 15 personas.`]);
expect(problem({policy: TIME_AND_KEY,recipients: lines(15),portable: true,wordsKind:'own',words:'perro luna casa verde tren mar',wordsAgain:'perro luna casa verde tren mar'})).toEqual(['recipients',`${many} Hay 15 personas.`]);
'Sin personas ni palabras, el fichero de la llave es la única forma de abrir la cápsula: déjalo marcado, escribe tus palabras o añade una persona.',
'Sin personas ni palabras, el fichero de la llave es la única forma de abrir la cápsula: déjalo marcado, ábrela también con unas palabras o añade una persona.',
]);
// Words are a key of their own, of at least six different words of three
// letters or more, written twice, and with nothing a person cannot see.
consttyped=' Perro LUNA casa verde trén mar ';
constworded=planCapsule(input({policy: TIME_AND_KEY,portable: false,words: typed,wordsAgain:'perro luna casa verde tren mar'}),GENESIS_MS);
constworded=planCapsule(input({policy: TIME_AND_KEY,portable: false,wordsKind:'own',words: typed,wordsAgain:'perro luna casa verde tren mar'}),GENESIS_MS);
'glaciar abad tocar abadía abandonar útil abdomen',
]);
expect(problem(keyed({diceList: undefined}))).toEqual(['words','La lista de palabras aún no está lista.']);
expect(problem(keyed({dice:'35214 11111 64253 11112 11113'}))).toEqual(['words','Escribe al menos 6 números de cinco dados, uno por palabra.']);
expect(problem(keyed({dice:''}))).toEqual(['words','Escribe al menos 6 números de cinco dados, uno por palabra.']);
expect(problem(keyed({dice:'35214 11111 64253 11112 11113 66667'}))).toEqual(['words','«66667» no vale: cada palabra son cinco dados, cinco cifras del 1 al 6.']);
expect(problem(keyed({dice:'35214 11111 64253 11112 11113 35214'}))).toEqual(['words','«35214» da otra vez «glaciar»: vuelve a tirar esos dados.']);
'DateKeys ya no escribe cápsulas con la red Quicknet de drand: su registro de perfiles la tiene en «solo lectura» (§71). Las cápsulas que ya existen se siguen abriendo.',
]);
expect(plan('compromised')).toEqual([
'date',
'DateKeys ya no escribe cápsulas con la red Quicknet de drand: su registro de perfiles la tiene por comprometida (§71). Las cápsulas que ya existen se siguen abriendo, pero su contenido pudo leerse antes de la fecha.',
]);
});
});
describe('readDice',()=>{
it('reads each number of five dice with its word, or why it gives none',()=>{
?'DateKeys ya no escribe cápsulas con la red Quicknet de drand: su registro de perfiles la tiene en «solo lectura» (§71). Las cápsulas que ya existen se siguen abriendo.'
:'DateKeys ya no escribe cápsulas con la red Quicknet de drand: su registro de perfiles la tiene por comprometida (§71). Las cápsulas que ya existen se siguen abriendo, pero su contenido pudo leerse antes de la fecha.',
);
}
if(input.date==='')returnfail('date','Elige el día de apertura.');
if(input.time==='')returnfail('time','Elige la hora de apertura.');
if(rolls.length<MIN_WORDS)returnfail('words',`Escribe al menos ${MIN_WORDS} números de cinco dados, uno por palabra.`);
text=rolls.map((r)=>r.word).join(' ');
}
words=kind==='none'?[]:quickWords(text);
if(kind!=='none'&&words.length===0){
returnfail('words',kind==='random'?'Las palabras al azar aún no están listas.':`Escribe tus palabras: al menos ${MIN_WORDS} distintas de ${MIN_LETTERS} letras o más.`);
'Sin personas ni palabras, el fichero de la llave es la única forma de abrir la cápsula: déjalo marcado, escribe tus palabras o añade una persona.',
'Sin personas ni palabras, el fichero de la llave es la única forma de abrir la cápsula: déjalo marcado, ábrela también con unas palabras o añade una persona.',
it('adds the key of the words, which opens the capsule without a .dkk',async()=>{
constp=plan({files: one(4),policy: TIME_AND_KEY,portable: false,words:'Perro luna casa verde trén mar',wordsAgain:'perro luna casa verde tren mar'});
constp=plan({files: one(4),policy: TIME_AND_KEY,portable: false,wordsKind:'own',words:'Perro luna casa verde trén mar',wordsAgain:'perro luna casa verde tren mar'});
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh dio la ronda undefined, no la 32668196; api3.drand.sh no se pudo conectar.',
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh respondió algo que no es una firma; api3.drand.sh no se pudo conectar.',
);
});
// The client of the reference reads the body with json.Unmarshal, which refuses a byte order mark before the JSON.
// Spec v0.16, §47.1: the client of the reference reads the answers of the relays with the strict reader of drand's
// JSON, as a release that the caller gives: a name twice is malformed, and ROUND is another name, ignored.
it('reads an answer with the strict reader of drand JSON',async()=>{
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh dio una firma que no es hexadecimal; api3.drand.sh respondió algo que no es una firma.',
"description":"format 3 time_only capsule with padding code 1 (bloque256) and one file, whose BODY and P are 65536 bytes: PAYLOAD_AGE ends in a full STREAM chunk, which the annex of spec v0.16 (79.5) allows",
"description":"format 3 time_only capsule with a single file, nota.txt, and the public note «Cartas del viaje a Lisboa» in the noncritical array of PUBLIC_HEADER (spec v0.11, §24.1)",
"description":"format 3 time_only capsule with an author-signature of alg 4294967295, as in format3_signature_unsupported, and a seal of seal_type 4294967295, reserved for tests, with a random token of 32 bytes: verdicts F1 and S1",
"description":"format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by the test key of format3_signed and sealed with seal_type 2 by a test time-stamping authority before the round time: verdicts F4 and S4, with SEAL_SUBJECT and the token in the record",
"description":"format 3 time_only capsule with an author-signature of alg 4294967295, a random key of 32 bytes and a random signature of 64: verdicts F1 and S0",
"description":"format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by a test key whose seed the record gives: verdict F4, and the commitments and the message of the signature",
"description":"format 3 time_only capsule with a single file, nota.txt, signed with alg 2 by two test certificates, an ECDSA P-256 one and an RSA 2048 one, each sealed by a test time-stamping authority before the round time: verdict F6, with the certificates, the commitments, SIGNERS and the result of each signer in the record",
"description":"format 3 time_and_key capsule with one credential, a key of words, and 15 dummies: the text of the vector of the annex of spec v0.16 (79.7), «Ñandú», two spaces, «PINGÜINO», a tab and «camión árbol Éter ola», whose words are «nandu pinguino camion arbol eter ola»",
"description":"format 3 time_only capsule with five files in three folders, one of them over two STREAM chunks and one without mtime, a comment of two lines and a declared author",
"description":"format 3 time_only capsule with a single file, nota.txt, as format3_signed, without a signature: the area of 32 KiB of spec v0.11 holds the empty security, and P is the one of format3_signed",
"description":"portable X25519 .dkk of time_and_key_portable.dkc with a noncritical extension: the credential of time_and_key_portable.dkk re-issued with org.example.delivery",
"description":"CBOR profile of spec §58 and the schemas of spec/datekeys.cddl, generated by the reference implementation. accept and reject are walked as one data item of the profile with the limits of walk; schemas are decoded with the decoder of their schema. See testdata/README.md.",
"description":"Ed25519 signatures and the result of the strict profile of the author signature (spec v0.11, §29.9), after the cases of «Taming the many EdDSAs»; stdlib is the result of crypto/ed25519 of Go, for the record. Generated by the reference implementation. See testdata/README.md.",
"description":"HEAD_CBOR of format 3 (spec §29.4 to §29.6) and the result of decoding it with no extension known, generated by the reference implementation: layer 2 (type tag and version), layer 3 (the CDDL with R1 and R8), then layer 4 in key order (spec §69.1). See testdata/README.md.",
"description":"Differential corpus of the pre-unlock checks (spec §63 steps 1 to 8): deterministic mutations of the official .dkc fixtures with the verdict of the reference implementation. See testdata/README.md.",
"format":"Each mutation is bases[base].file (in testdata/fixtures) with its edits applied. An edit is [at, delete, insert]: the delete bytes at offset at of the base are replaced by the bytes of the hex string insert. The edits of one mutation refer to offsets of the unmodified base, are sorted by offset and do not overlap. result is the verdict of steps 1 to 8 of spec §63 (capsule.Inspect, the Quicknet profile pinned, no extension known, no network, no secret): ok, or the normative error code, with step the step that failed. kind names the generator of the mutation and is informative.",
"description":"The extension datekeys.capsule of a .dkk and what it points to (spec v0.12, 44.1): an envelope of age with its header apart from its rest, the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. On the same envelope, what a reader rejects and what it uses (64): addresses, a locator with rejected and usable addresses, resources of the rest, data of the extension and plaintexts of the locator. Frozen. See testdata/README.md.",
"description":"Mutation corpus of spec §64 and further cases of capsule.TestMutationCorpus, generated by the reference implementation: each case is a .dkc and what the reader is given, with the normative error and the step of spec §63 at which capsule.Open fails. See testdata/README.md.",
"description":"The data of the public note, datekeys.note version 1 in the noncritical array of PUBLIC_HEADER (spec §24.1): the text in UTF-8, from 1 to 1024 bytes, that meets the rules of the declared author of §29.6. See testdata/README.md.",
"notes":[
{
Some files were not shown because too many files have changed in this diff
Show More