From e2296b02f00cf37e2d70f33e0fd27688c5a00ff4 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 7 Oct 2026 16:35:34 +0200 Subject: [PATCH] The recovery annex and the state of a profile, as annex.go and profile/status.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What the official SDK says when it seals, from datekeys-go aefc8f6, without the CLI. lib/src/annex.dart ports annex.go: recoveryAnnex, the text of datekeys.RecoveryAnnex that the official SDK saves next to each .dkc (spec §62.1, rule 27), §79 of the specification under a title with its version and SHA-256; and recoveryAnnexSuffix, ".recuperacion.txt". Dart has no go:embed, so the text is the constant of lib/src/recovery_annex.g.dart, which tool/recovery_annex_copy.dart writes from the vendored annex/recovery.md once it matches its SOURCE.json: a multi-line string that escapes the backslash, the dollar sign, the quote and, as Unicode escapes, every rune but the line feed that Go's strconv.IsPrint rejects, with the SHA-256 of its bytes, recoveryAnnexSha256, internal, for the tests compiled to JavaScript. lib/src/profile.dart ports status.go: ProfileStatus, active, readOnly and compromised, with the names of Go's Status.String, and profileStatusOf, the state of the profile of a profile_hash and whether this release knows it, from a table where Quicknet is active (spec §71). An unknown profile is active, Go's zero Status, and not known. lib/datekeys.dart exports the four. test/annex_test.dart, also on Node.js, checks what TestRecoveryAnnex checks of the text and the SHA-256 of its bytes; test/annex_vm_test.dart, that the constant is annex/recovery.md byte for byte and that the file is §79 of the specification at the commit of annex/SOURCE.json under the title and the paragraph of TestRecoveryAnnex; formats_objects_test.dart runs the cases of TestStatus. README and CHANGELOG for them. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 2 +- CHANGELOG.md | 12 +++ README.md | 38 +++++-- lib/datekeys.dart | 7 +- lib/src/annex.dart | 15 +++ lib/src/profile.dart | 45 ++++++++- lib/src/recovery_annex.g.dart | 180 +++++++++++++++++++++++++++++++++ test/annex_test.dart | 44 ++++++++ test/annex_vm_test.dart | 77 ++++++++++++++ test/formats_objects_test.dart | 29 +++++- tool/recovery_annex_copy.dart | 93 +++++++++++++++++ tool/sync_testdata.dart | 4 +- 12 files changed, 532 insertions(+), 14 deletions(-) create mode 100644 lib/src/annex.dart create mode 100644 lib/src/recovery_annex.g.dart create mode 100644 test/annex_test.dart create mode 100644 test/annex_vm_test.dart create mode 100644 tool/recovery_annex_copy.dart diff --git a/.gitattributes b/.gitattributes index 24ced97..74d50ba 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,5 +10,5 @@ testdata/** -text wordlists/** -text # So must the vendored recovery annex of datekeys.RecoveryAnnex: its SHA-256 is -# recorded in annex/SOURCE.json. +# recorded in annex/SOURCE.json and lib/src/recovery_annex.g.dart is its text. annex/** -text diff --git a/CHANGELOG.md b/CHANGELOG.md index b046c9c..8a46263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ Cambios notables de la librería Dart. El proyecto usa versionado semántico; mi ## Especificación 0.15, en la rama `v0.15` — sin versión +### Lo que dice el SDK oficial al sellar (07-10-2026) + +- `datekeys-go` añade en `aefc8f6`, en la rama `v0.15`, lo que el SDK oficial debe hacer al sellar y su CLI no hacía todavía (§7.6, §62.1 reglas 26 y 27, §71): `encrypt` escribe junto al `.dkc` el anexo de recuperación, `FICHERO.dkc.recuperacion.txt`, el §79 de la especificación bajo un título con su versión y su SHA-256, el mismo para toda cápsula; dice qué hará falta para abrir la cápsula años después; y no escribe ninguna cápsula con un perfil que no esté activo en el registro de perfiles del §71. No cambia ningún formato. +- **El anexo de recuperación.** `lib/src/annex.dart` porta `annex.go`: `recoveryAnnex`, el texto de `datekeys.RecoveryAnnex`, y `recoveryAnnexSuffix`, `.recuperacion.txt`, lo que se añade al nombre del `.dkc` para nombrar el fichero del anexo. Dart no incrusta ficheros como `go:embed`: el texto es la constante de `lib/src/recovery_annex.g.dart`, que escribe `dart run tool/recovery_annex_copy.dart` desde `annex/recovery.md` tras comprobarlo contra su `SOURCE.json`. Va en un string de varias líneas que escapa la barra invertida, el dólar, la comilla y, como escape Unicode, cada runa salvo el salto de línea que `strconv.IsPrint` de Go rechaza, para que ningún carácter sea invisible en el código y los finales de línea de una copia de trabajo no lleguen al texto; el anexo de hoy no tiene nada que escapar. El programa escribe también el SHA-256 de sus bytes, `recoveryAnnexSha256`, interno, para las pruebas compiladas a JavaScript. +- **El estado de un perfil (§71).** `lib/src/profile.dart` porta `status.go` de `profile`: `ProfileStatus`, `active`, `readOnly` o `compromised`, con el nombre de `Status.String` de Go en `label` y en `toString`; y `profileStatusOf`, el estado del perfil de un `profile_hash` y si esta versión de la librería lo conoce, como `StatusOf`. Quicknet está activo. DateKeys no publica todavía el registro firmado del §71, así que un cambio de estado llega con una versión nueva de la librería. El escritor no lo consulta: lo aplica la app, como lo aplica la CLI de Go. + + `lib/datekeys.dart` exporta `recoveryAnnex`, `recoveryAnnexSuffix`, `ProfileStatus` y `profileStatusOf`. +- **`annex/`.** `tool/sync_testdata.dart` copia también `annex` del mismo commit de Go en `annex/`, con un `SOURCE.json` del formato del de `testdata/`, y `check` escribe una tercera línea, la de `annex`. `testdata` y `wordlists` pasan a `aefc8f6`: solo cambia el commit de sus `SOURCE.json`. `.gitattributes` guarda los bytes exactos de `annex/`, como los de `testdata/` y `wordlists/`. +- **Pruebas.** 7 nuevas en la VM y 4 en Node.js: `test/annex_test.dart`, también compilado a JavaScript, con lo que `TestRecoveryAnnex` de Go comprueba del texto (el título del §79, su apartado 79.8, la versión `0.15`, que acaba en «conforme.» y un salto de línea y que no tiene ningún CR), su primera línea, el SHA-256 de sus bytes UTF-8 y el sufijo; `test/annex_vm_test.dart`, que la constante es `annex/recovery.md` byte a byte y que el fichero es el §79 de la especificación bajo el título y el párrafo de `TestRecoveryAnnex`, con la especificación del commit de `annex/SOURCE.json`; `test/testdata_test.dart`, con la línea de `annex`; y `test/formats_objects_test.dart`, con los casos de `TestStatus`. En total, 2250 en la VM y 716 en Node.js. +- **Fallos inyectados**, uno a uno y revertidos: un carácter cambiado en la constante lo detectan la prueba de los bytes y la del SHA-256, también en Node.js; Quicknet `readOnly` en la tabla, también en Node.js, y otro nombre para `readOnly`, los casos de `TestStatus`. El escape del programa se probó aparte sobre un texto con todo lo que escapa (una primera línea de espacios, la barra invertida, el dólar, las comillas, CR, tabulador, U+00A0, U+00AD, U+2028, U+202E, U+FEFF, U+0000, U+007F, U+0085, un emoji y una comilla y una barra al final): el string compilado da sus bytes, byte a byte. +- **Diferencias con Go.** `profileStatusOf` devuelve un registro, `(status, known)`, y toma los bytes del hash, donde Go toma un `[32]byte`: uno de otra longitud es un perfil que no conoce. Un perfil que no conoce está `active`, el `Status` cero de Go, con `known` a falso. El `unknown` de `Status.String`, el de un `Status` fuera de rango, no tiene equivalente en un enum. + ### Los dados de la llave de palabras (07-10-2026) - `datekeys-go` añade en `92e7154`, en la rama `v0.15`, los dados que decidió el autor, para quien no se fía de los números al azar de un ordenador: cinco dados por palabra, leídos en un orden fijo, dan un número de 11111 a 66666, la posición de la palabra en una lista de 7776, el primer dado el más significativo. Son `wordkey.DiceNumber`, `DiceWord`, `DiceWords` y `DiceList`, con `encrypt -dice` y `datekeys wordlist` en la CLI. No cambia ningún formato ni ninguna derivación. diff --git a/README.md b/README.md index 26bcf3f..a2e3565 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trab - la 7a, el localizador y el sobre de `datekeys.capsule`: los datos de la extensión, la lectura y la apertura del localizador, las reglas de sus direcciones y de la IP a la que resuelve un nombre, y el resto del sobre; - la 7b, las claves de autor `dkauthor1…`, con su firma Ed25519 y su fichero cifrado con scrypt, y el sellado del localizador y la creación del sobre, sobre el escritor de `age` de la 6a; - fuera del plan, con la v0.15, las palabras al azar de la llave de palabras: las listas de palabras de `datekeys-go`, tomadas solo con su SHA-256 fijado y comprobadas con el alfabeto de su idioma, las palabras sacadas al azar de una y su fuerza en bits, y las de cinco dados cada una, con la lista numerada para los dados. +- fuera del plan, con la v0.15, lo que dice el SDK oficial al sellar: el anexo de recuperación que guarda junto a cada `.dkc` y el estado de un perfil en el registro de perfiles del §71. -La librería ya abre cápsulas reales, de los tres formatos, con todas sus credenciales, y evalúa toda su área de seguridad como Go: la firma de `alg` 1, la de `alg` 2 con certificados, el sello de `seal_type` 2 y todos los veredictos de su forma. Y ya escribe cápsulas del formato 3, con todas sus credenciales, su firma y su sello, byte a byte como Go con los mismos valores aleatorios, y Go las abre. Firma con una clave de autor y sella localizadores. Y saca al azar las palabras de una llave de palabras de las listas de `datekeys-go`, que toma solo con su SHA-256 fijado, o las da de unos dados. +La librería ya abre cápsulas reales, de los tres formatos, con todas sus credenciales, y evalúa toda su área de seguridad como Go: la firma de `alg` 1, la de `alg` 2 con certificados, el sello de `seal_type` 2 y todos los veredictos de su forma. Y ya escribe cápsulas del formato 3, con todas sus credenciales, su firma y su sello, byte a byte como Go con los mismos valores aleatorios, y Go las abre. Firma con una clave de autor y sella localizadores. Y saca al azar las palabras de una llave de palabras de las listas de `datekeys-go`, que toma solo con su SHA-256 fijado, o las da de unos dados. Y da el anexo de recuperación que el SDK oficial guarda junto a cada `.dkc` y el estado de un perfil fijado. Las etapas 2 y 3 se hicieron en paralelo, en ramas aparte desde la etapa 1, y se integraron el 5 de octubre de 2026. Las partes 4a y 4b también: la 4a, en la rama `stage4a`, se integró encima de la 4b el mismo día. La 4c se hizo después, en la rama `v0.11`, y la 5b también, en paralelo con la 5a, el lector de CMS, que se hizo en la rama `stage5a` y se integró encima de la 5b el mismo día. La 5c, que necesitaba las dos, se hizo después, en la rama `v0.11`. La 6a se hizo en la rama `v0.11`, en paralelo con la 7a, la lectura del localizador, en la rama `stage7a`. La 7a se integró encima de la 6a el mismo día. La 6b, el escritor de cápsulas, se hizo en la rama `v0.11`, en paralelo con la 7b, las claves de autor y el sellado del localizador, en la rama `stage7b`, desde `b23a0ee`. La 7b se integró encima de la 6b el mismo día. @@ -107,6 +108,22 @@ Notas: - **La licencia.** Las listas no son código: `en.txt` es material original de la EFF, con licencia CC BY 4.0, y `es.txt` es una adaptación de FrequencyWords de Hermit Dave y tiene licencia CC BY-SA 4.0, no Apache-2.0. `wordlists/README.md`, copiado de Go, recoge su fuente, su método y su licencia. - **Lo que se exporta.** `lib/datekeys.dart` exporta `readWordList`, `checkWordList`, `generateWords`, `wordBits`, `wordListSha256`, `wordListLanguages`, `defaultWordCount` y `minListSize`, y `diceNumber`, `diceWord`, `diceWords`, `diceList` y `diceListSize`, como el paquete público `wordkey` de Go. `checkWordListUtf8`, `diceWordUtf8` y `diceWordsUtf8`, sobre bytes, y `parseWordList`, `readWordList` sin el SHA-256, son internas, para las pruebas. `wordkey.dart` comparte con `wordlist.dart` la comprobación de cada runa, `checkWordRune`, también interna. +### Lo que dice el SDK oficial al sellar (v0.15) + +La regla 27 del §62.1 recomienda al SDK oficial guardar junto a cada `.dkc` el anexo de recuperación, el §79, que dice cómo abrir una cápsula sin software de DateKeys, y el §71 da a cada perfil un estado en el registro de perfiles. `datekeys-go` los añade en `aefc8f6`, en la rama `v0.15`, sin cambiar ningún formato, y la librería porta lo que no es de la CLI, con los mismos valores: + +| Módulo | Contenido | En Go | +|---|---|---| +| `lib/src/annex.dart` y `lib/src/recovery_annex.g.dart` | `recoveryAnnex`, el texto del anexo, y `recoveryAnnexSuffix`, `.recuperacion.txt`, lo que se añade al nombre del `.dkc` para nombrar el fichero del anexo: `carta.dkc.recuperacion.txt` | `RecoveryAnnex` y `RecoveryAnnexSuffix` (`annex.go`) | +| `lib/src/profile.dart` | `ProfileStatus`, el estado de un perfil: `active`, `readOnly` o `compromised`; y `profileStatusOf`, el estado del perfil de un `profile_hash` y si esta versión de la librería lo conoce | `Status` y `StatusOf` de `profile` (`status.go`) | + +Notas: +- **El anexo** es el §79 de la especificación bajo un título y un párrafo que nombran la versión de la especificación y el SHA-256 de su texto. Es el mismo para toda cápsula y no lleva nada de una. Sus bytes UTF-8 son `annex/recovery.md` de `datekeys-go`, que Go incrusta con `go:embed` y comprueba contra el §79 de la especificación de su `SpecVersion`. Está en español, como la especificación. +- **Dart no incrusta ficheros**, así que el texto es una constante generada: `dart run tool/recovery_annex_copy.dart` escribe `lib/src/recovery_annex.g.dart` desde `annex/recovery.md`, la copia que hace `tool/sync_testdata.dart` (ver «Datos de prueba»), después de comprobarla contra su `SOURCE.json`. Va en un string de varias líneas, cada línea del fichero una del código: se escapan la barra invertida, el dólar y la comilla, y, como escape Unicode, cada runa salvo el salto de línea que `strconv.IsPrint` de Go rechaza, como un CR, un tabulador, un espacio de no separación o un control de texto bidireccional, para que ningún carácter del texto sea invisible en el código y los finales de línea de una copia de trabajo no lleguen al texto. El anexo de hoy no tiene nada que escapar. Una constante no cuesta nada al cargar, y la compilación a JavaScript la deja fuera si nadie la usa. Hay que ejecutar el programa tras cada `sync` que cambie `annex/`. +- **El estado de un perfil.** DateKeys no publica todavía el registro firmado del §71, así que la librería fija el estado de los perfiles que conoce, por su `profile_hash`, como Go: Quicknet está activo, y un cambio de estado llega con una versión nueva de la librería. Con un perfil `readOnly` no se escribe ninguna cápsula nueva, y las que hay se siguen abriendo; con uno `compromised` también se abren, y el SDK oficial avisa de que su contenido pudo leerse antes de la fecha. La librería da el estado y el escritor no lo consulta: lo aplica la app, como lo aplica la CLI de Go. +- **Las diferencias con Go.** `profileStatusOf` devuelve un registro, `(status, known)`, donde Go devuelve dos valores, y toma los bytes del hash, donde Go toma un `[32]byte`: un hash de otra longitud es un perfil que no conoce. Un perfil que no conoce está `active`, el `Status` cero de Go, con `known` a falso. `ProfileStatus` es un enum de tres valores, cada uno con el nombre de `Status.String` de Go en `label` y en `toString`; el `unknown` de Go, el de un `Status` fuera de rango, no tiene equivalente. +- **Lo que se exporta.** `lib/datekeys.dart` exporta `recoveryAnnex`, `recoveryAnnexSuffix`, `ProfileStatus` y `profileStatusOf`. `recoveryAnnexSha256`, el SHA-256 de los bytes del anexo que escribe también el programa, es interno, para las pruebas compiladas a JavaScript. + La parte 4a de la etapa 4 porta `internal/pathrule` y `wordkey` de `datekeys-go` en `c531e93`, la cabeza de la rama `v0.12`, que sigue la v0.11 con las correcciones de la revisión del 2 de octubre; los dos paquetes no cambian desde el tag `spec-v0.11`. Tienen las mismas comprobaciones, en el mismo orden y con los mismos textos de error: | Módulo | Contenido | En Go | @@ -596,30 +613,33 @@ tool/check.sh - `dart analyze --fatal-infos`; - `dart test`; - `dart test -p node`: las pruebas que no leen ficheros corren también compiladas a JavaScript, en Node.js, para comprobar que los enteros son exactos en la web. Las que leen ficheros llevan `@TestOn('vm')`. Por eso el gate necesita Node.js, como `datekeys-ts`; lo decidió el autor el 5 de octubre de 2026; -- las copias de `testdata/` y `wordlists/` frente al repositorio de Go, que debe estar al lado, en `../datekeys-go`. +- las copias de `testdata/`, `wordlists/` y `annex/` frente al repositorio de Go, que debe estar al lado, en `../datekeys-go`. ## Datos de prueba -`testdata/` es una copia de `datekeys-go/testdata` en un commit fijo. `testdata/SOURCE.json` registra el commit y el SHA-256 de cada fichero, igual que en `datekeys-ts`. La copia actual es la de `datekeys-go` en `92e7154`, la rama `v0.15` después del tag `spec-v0.15`, cuyo `testdata/` es el del tag, `fe50885`, byte a byte: 142 ficheros, con `release.json` y `releases/`, el objeto release, el JSON de drand y el archivo de releases de la v0.15, con el campo `source` de `mutations.json`, con `tlock_steps.json`, los pasos 10 y 11 de Quicknet valor a valor, con `resolved_ip.json`, con `wordkey.json`, los vectores compartidos de la llave de palabras, y los veredictos y las líneas de la v0.12. Cada fichero dice `"spec": "0.15"`, como el `SpecVersion` de Go. +`testdata/` es una copia de `datekeys-go/testdata` en un commit fijo. `testdata/SOURCE.json` registra el commit y el SHA-256 de cada fichero, igual que en `datekeys-ts`. La copia actual es la de `datekeys-go` en `aefc8f6`, la rama `v0.15` después del tag `spec-v0.15`, cuyo `testdata/` es el del tag, `fe50885`, byte a byte: 142 ficheros, con `release.json` y `releases/`, el objeto release, el JSON de drand y el archivo de releases de la v0.15, con el campo `source` de `mutations.json`, con `tlock_steps.json`, los pasos 10 y 11 de Quicknet valor a valor, con `resolved_ip.json`, con `wordkey.json`, los vectores compartidos de la llave de palabras, y los veredictos y las líneas de la v0.12. Cada fichero dice `"spec": "0.15"`, como el `SpecVersion` de Go. `wordlists/` es la copia de `wordkey/lists` del mismo commit: las listas de palabras de `wordkey.Generate`, hoy `en.txt`, la de la EFF, y `es.txt`, y su `README.md`, con la fuente, el método y la licencia de cada lista, y el SHA-256 de cada lista numerada para los dados. `wordlists/SOURCE.json` tiene el formato de `testdata/SOURCE.json`, y `.gitattributes` guarda sus bytes exactos, como los de `testdata/`: `lib/src/wordlist.dart` fija el SHA-256 de cada lista. +`annex/` es la copia de `annex` del mismo commit: `recovery.md`, el anexo de recuperación de `datekeys.RecoveryAnnex`, el §79 de la especificación bajo su título. `annex/SOURCE.json` tiene el formato de `testdata/SOURCE.json`, y `.gitattributes` guarda sus bytes exactos: `lib/src/recovery_annex.g.dart` es su texto, que escribe `dart run tool/recovery_annex_copy.dart` tras cada `sync` que lo cambie. + ```bash -dart run tool/sync_testdata.dart sync --commit 92e7154 +dart run tool/sync_testdata.dart sync --commit aefc8f6 ``` ```bash dart run tool/sync_testdata.dart check --against ../datekeys-go ``` -- `sync` copia los dos árboles del mismo commit y lee los ficheros con git, así que nunca entran cambios sin commit del repositorio de Go. Lo lee todo antes de tocar las copias, y un árbol que falta en el commit es un error. -- `check` comprueba los dos árboles con las mismas reglas, sin ficheros que falten, sobren o cambien, y escribe una línea por árbol, la de `testdata` primero. -- Los ficheros de `testdata/` y de `wordlists/` no se editan ni se generan aquí. -- En cada `dart test`, `test/testdata_test.dart` comprueba las dos copias, y que cada fichero de `testdata/` nombre `specVersion`. `test/wordlist_vm_test.dart` lee las dos listas de `wordlists/` con `readWordList`, y numera las dos para los dados. +- `sync` copia los tres árboles del mismo commit y lee los ficheros con git, así que nunca entran cambios sin commit del repositorio de Go. Lo lee todo antes de tocar las copias, y un árbol que falta en el commit es un error. +- `check` comprueba los tres árboles con las mismas reglas, sin ficheros que falten, sobren o cambien, y escribe una línea por árbol: la de `testdata`, la de `wordlists` y la de `annex`. +- Los ficheros de `testdata/`, de `wordlists/` y de `annex/` no se editan ni se generan aquí. +- En cada `dart test`, `test/testdata_test.dart` comprueba las tres copias, y que cada fichero de `testdata/` nombre `specVersion`. `test/wordlist_vm_test.dart` lee las dos listas de `wordlists/` con `readWordList`, y numera las dos para los dados. - `test/tlock_steps_vm_test.dart` recorre `tlock_steps.json` valor a valor con el código de la librería: M, H(M), la ecuación del emparejamiento, las partes del stanza, e(firma, U), H2, sigma, H4, la file key, cada intento de H3 y r, r·G2 = U, y las comprobaciones negativas del generador de Go (otro DST, la ronda sin SHA-256 y H3 que borra solo el bit más alto). `test/tlock_steps_test.dart` hace lo mismo compilado a JavaScript, sobre `test/vectors/tlock_steps.g.dart`, la copia del fichero que escribe `dart run tool/tlock_steps_copy.dart` tras cada `sync`; la prueba de la VM comprueba que es el fichero byte a byte. - `test/release_object_vm_test.dart` recorre `release.json` caso a caso, con el resultado y el texto de Go: los objetos, con sus capas y el paso 10, el release que dice cada uno y su codificación otra vez byte a byte; el JSON de drand; y las búsquedas en el archivo. Comprueba además cada fichero de `releases/` y los textos de `provider.Archive` sobre archivos editados (`test/vectors/release_archive_texts.json`). `test/release_object_test.dart` hace lo mismo compilado a JavaScript, sobre `test/vectors/release.g.dart`, que escribe `dart run tool/release_copy.dart` tras cada `sync`. - `test/open_corpus_test.dart` abre cada caso de `mutations.json` con su `source`: `supplied`, el objeto release de su `release` en `OpenOptions.release`, o `network`, una fuente que verifica el release y lo descarta si no cumple el paso 10, como `testkit` de Go. - `test/errors_spec_test.dart` lee el §69 de la especificación en el commit de `testdata/SOURCE.json`. +- `test/annex_vm_test.dart` comprueba que `recoveryAnnex` es `annex/recovery.md` byte a byte, y que el fichero es el §79 de la especificación bajo el título y el párrafo de `TestRecoveryAnnex` de Go, que nombran su versión y su SHA-256, con la especificación en el commit de `annex/SOURCE.json`, como `test/errors_spec_test.dart`. `test/annex_test.dart` comprueba, también compilado a JavaScript, lo que comprueba Go del texto del anexo y el SHA-256 de sus bytes UTF-8. `test/vectors/` tiene los vectores de las primitivas, de la lectura y la escritura de `age`, de BLS12-381, de tlock, de las reglas de rutas y textos, de la llave de palabras y de sus listas, de los formatos, de la apertura, del área de seguridad, de la firma con certificados y el sello, del lector de CMS, del localizador, de las claves de autor y de la firma Ed25519, y del sellado del localizador y del sobre. Los escribe Go, con las librerías de la caché de módulos que usa `datekeys-go` (`x/crypto`, `filippo.io/age`, `kilic/bls12-381`, `drand/kyber`, `kyber-bls12381` y `tlock`) y sus paquetes, como `provider`, `agewrap`, `capsule`, `internal/pathrule`, `internal/testkit` y `wordkey`; ningún valor esperado se escribe a mano. Los generadores van en `tool/`, con `//go:build ignore`, y se ejecutan en el contexto del módulo de `datekeys-go`, sin cambiar nada en él; los de las rutas, de la apertura, del lector de CMS, del escritor de `age` y de las listas de palabras, en una exportación suya, y el último, `tool/cms_go_vectors_test.go`, como una prueba de Go. Los de BLS12-381 y tlock dan la misma salida con el tag `spec-v0.11` y con el borrador v0.12, y leen ficheros congelados de `datekeys-ts`, cuya carpeta en esta máquina se llama todavía `App`: @@ -868,3 +888,5 @@ rm -rf "$tmp" Apache-2.0 (`LICENSE`), como `datekeys-go` y `datekeys-ts`. La especificación tiene su propia licencia, CC-BY-4.0. Las listas de palabras de `wordlists/`, copiadas de `datekeys-go`, no son código y tienen su propia licencia: `es.txt` es una adaptación de FrequencyWords de Hermit Dave, con licencia CC BY-SA 4.0. `wordlists/README.md` recoge la fuente, el método y la licencia de cada lista. + +El anexo de `annex/`, copiado de `datekeys-go`, y su texto en `lib/src/recovery_annex.g.dart` son el §79 de la especificación bajo un título: texto de la especificación, no código. diff --git a/lib/datekeys.dart b/lib/datekeys.dart index 45a5a1a..6afdb72 100644 --- a/lib/datekeys.dart +++ b/lib/datekeys.dart @@ -43,7 +43,11 @@ /// SHA-256 that [wordListSha256] pins and checks against the alphabet of /// their language, the words that [generateWords] draws from one, and their /// strength in bits, [wordBits]; and the words of five dice each, -/// [diceWords], with the list numbered for dice, [diceList]. +/// [diceWords], with the list numbered for dice, [diceList]. And what the +/// official SDK says when it seals: the recovery annex of spec §79 that it +/// saves next to each .dkc, [recoveryAnnex], in a file whose name ends with +/// [recoveryAnnexSuffix], and the state of a pinned profile in the registry +/// of spec §71, [profileStatusOf]. /// /// DER, the primitives, the reading and the writing of age, the recipients /// and the random sources, agewrap, the curve arithmetic, the IBE of tlock @@ -55,6 +59,7 @@ library; export 'src/accesskey.dart'; +export 'src/annex.dart'; export 'src/author.dart'; export 'src/authorkey.dart' hide parseAuthorPublicUtf8, parseAuthorSecretUtf8; export 'src/bls12381_curve.dart' diff --git a/lib/src/annex.dart b/lib/src/annex.dart new file mode 100644 index 0000000..3c038a1 --- /dev/null +++ b/lib/src/annex.dart @@ -0,0 +1,15 @@ +/// The recovery annex that the official SDK saves next to each .dkc (spec +/// §62.1, rule 27), as annex.go of datekeys-go: [recoveryAnnex], its text, +/// and [recoveryAnnexSuffix], the end of the name of its file. +/// +/// Dart has no go:embed: the text is the constant of recovery_annex.g.dart, +/// which tool/recovery_annex_copy.dart writes from annex/recovery.md, the +/// copy of annex/ of datekeys-go that tool/sync_testdata.dart vendors. +library; + +export 'recovery_annex.g.dart' show recoveryAnnex; + +/// What the official SDK appends to the name of a .dkc to name the file of +/// its recovery annex, `carta.dkc.recuperacion.txt`, as RecoveryAnnexSuffix +/// of Go. The annex is in Spanish, as the specification. +const recoveryAnnexSuffix = '.recuperacion.txt'; diff --git a/lib/src/profile.dart b/lib/src/profile.dart index 97f9c0e..2b84c34 100644 --- a/lib/src/profile.dart +++ b/lib/src/profile.dart @@ -1,8 +1,9 @@ /// Provider Profiles (spec §10 to §13), as package profile of datekeys-go and /// profile.ts of datekeys-ts: their Deterministic CBOR, profile_hash, their /// validation by the rules of spec §12.1 in their order, with the codes and -/// texts of Go, and the registry of pinned profiles that is the root of -/// trust (spec §13). +/// texts of Go, the registry of pinned profiles that is the root of trust +/// (spec §13), and the state of a pinned profile in the registry of profiles +/// of DateKeys (spec §71). library; import 'dart:typed_data'; @@ -684,3 +685,43 @@ ProfileRegistry defaultRegistry() { } return _default = newRegistry([Pin(q, fromHex(quicknetProfileHash))]); } + +// --------------------------------------------------------------------------- +// The state of a profile (spec §71) + +/// The state of a Provider Profile in the registry of profiles of DateKeys +/// (spec §71), as Status of the profile package of Go. +enum ProfileStatus { + /// Capsules are written and opened with the profile. + active('active'), + + /// No new capsule is written with the profile, because its provider + /// announces its end or is suspected; the capsules that exist still open. + readOnly('read-only'), + + /// There is proof that the confidentiality of the profile failed. The + /// capsules that exist still open, and the official SDK warns that their + /// content may have been read before their date. + compromised('compromised'); + + const ProfileStatus(this.label); + + /// The name of Go's Status.String: active, read-only or compromised. + final String label; + + @override + String toString() => label; +} + +// The states of the profiles that this release of the library pins, by +// profile_hash, as statuses of Go: DateKeys does not publish the signed +// registry of spec §71 yet, so a change of state comes with a new release. +const _statuses = {quicknetProfileHash: ProfileStatus.active}; + +/// The state of the profile whose profile_hash is [hash], and whether this +/// release of the library knows it, as StatusOf of Go. A profile it does not +/// know is [ProfileStatus.active], the zero Status of Go, and not known. +({ProfileStatus status, bool known}) profileStatusOf(List hash) { + final status = _statuses[toHex(hash)]; + return (status: status ?? ProfileStatus.active, known: status != null); +} diff --git a/lib/src/recovery_annex.g.dart b/lib/src/recovery_annex.g.dart new file mode 100644 index 0000000..be86719 --- /dev/null +++ b/lib/src/recovery_annex.g.dart @@ -0,0 +1,180 @@ +// Generated by tool/recovery_annex_copy.dart from annex/recovery.md, +// the recovery annex of datekeys-go. Do not edit: sync annex/ and run +// the tool again. + +/// The text that the official SDK saves next to each .dkc (spec §62.1, +/// rule 27), as RecoveryAnnex of datekeys-go: the informative annex of +/// the specification on how to open a capsule without DateKeys software +/// (spec §79), in Spanish, under a title that names the version of the +/// specification and the SHA-256 of its text. It is the same for every +/// capsule and holds nothing of one. Its UTF-8 bytes are +/// annex/recovery.md of the Go reference, which checks it against §79 of +/// its specification; the official SDK writes them as they are, to the +/// name of the .dkc followed by recoveryAnnexSuffix. +const recoveryAnnex = ''' +# 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.15, cuyo texto tiene el SHA-256 45105e693be4187af4dd30f4d254402612587b6427c746f5d29f07a541c1e3f3. Es el mismo para toda cápsula: no lleva ningún dato de esta. + +## 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. + +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`. `scripts/recovery_check.sh` abre con él una cápsula `time_only` y otra `time_and_key` de los fixtures oficiales. + +### 79.1 Parámetros de Quicknet + +Son los de §12, y pueden no estar ya en ningún otro sitio: + +```text +chain_hash 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 +clave pública (G2, 96 bytes) + 83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a +genesis_time 1692803367 (segundos Unix de la ronda 1) +period 3 segundos +round_time(r) = genesis_time + (r − 1)·3 +q 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001 +DST BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_ +``` + +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): + +```json +{"version":1,"network":"datekeys:quicknet:v1","round":1000} +``` + +`round` es la ronda de la cápsula. + +### 79.3 El release + +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//rounds/ +``` + +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: + +```text +-> tlock + +``` + +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: + +```text +age-encryption.org/v1 +-> + +--- + +``` + +Con la file key FK, de 16 bytes: + +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 más corto. 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 §38.1. + +`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 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.8 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. +'''; + +/// The SHA-256 of the UTF-8 bytes of [recoveryAnnex], for the tests +/// that run compiled to JavaScript, where no file can be read. +/// +/// Internal: lib/datekeys.dart does not export it. +const recoveryAnnexSha256 = + 'c8c9b8815708d963ca6a3d688003bdc5046c499ab034a2d8c98a18c9811d3bd3'; diff --git a/test/annex_test.dart b/test/annex_test.dart new file mode 100644 index 0000000..5a023b3 --- /dev/null +++ b/test/annex_test.dart @@ -0,0 +1,44 @@ +// The recovery annex of lib/src/annex.dart, as TestRecoveryAnnex of +// datekeys-go: recoveryAnnex holds §79 of the specification of specVersion, +// from its title to its subsection 79.8, names the version, and ends as the +// specification does, without a CR; and its UTF-8 bytes have the SHA-256 that +// tool/recovery_annex_copy.dart took from annex/recovery.md, so that the +// constant compiled to JavaScript is the file too. It reads no file: it runs +// on the VM and compiled to JavaScript. test/annex_vm_test.dart compares the +// constant with the file and the file with §79 of the specification. +library; + +import 'dart:convert'; + +import 'package:datekeys/datekeys.dart'; +import 'package:datekeys/src/recovery_annex.g.dart' show recoveryAnnexSha256; +import 'package:datekeys/src/sha256.dart'; +import 'package:test/test.dart'; + +void main() { + test('the annex is §79 of the specification $specVersion', () { + for (final s in [ + '## 79. Anexo informativo: recuperación sin software DateKeys', + '### 79.8 Lo que el anexo no comprueba', + 'DateKeys v$specVersion,', + ]) { + expect(recoveryAnnex, contains(s)); + } + expect(recoveryAnnex, endsWith('conforme.\n')); + expect(recoveryAnnex, isNot(contains('\r'))); + expect( + recoveryAnnex, + startsWith( + '# Cómo abrir una cápsula DateKeys sin software de DateKeys\n\n', + ), + ); + }); + + test('its UTF-8 bytes are annex/recovery.md, by their SHA-256', () { + expect(toHex(sha256(utf8.encode(recoveryAnnex))), recoveryAnnexSha256); + }); + + test('its file is the name of the .dkc and .recuperacion.txt', () { + expect(recoveryAnnexSuffix, '.recuperacion.txt'); + }); +} diff --git a/test/annex_vm_test.dart b/test/annex_vm_test.dart new file mode 100644 index 0000000..e39688b --- /dev/null +++ b/test/annex_vm_test.dart @@ -0,0 +1,77 @@ +// recoveryAnnex is annex/recovery.md byte for byte, the copy that +// tool/recovery_annex_copy.dart writes after each sync of annex/. And +// annex/recovery.md is §79 of the specification of specVersion under the +// title and the paragraph of TestRecoveryAnnex of datekeys-go, which names the +// version and the SHA-256 of the specification: the specification is read +// from the Go repository next to this one, at the commit that annex/SOURCE.json +// pins, as test/errors_spec_test.dart reads it. +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:datekeys/datekeys.dart'; +import 'package:datekeys/src/recovery_annex.g.dart' show recoveryAnnexSha256; +import 'package:datekeys/src/sha256.dart'; +import 'package:test/test.dart'; + +import '../tool/sync_testdata.dart' as vendored; + +void main() { + final root = Directory.current; + final repo = Directory('../datekeys-go'); + + test('recoveryAnnex is annex/recovery.md byte for byte', () { + final file = File('${root.path}/annex/recovery.md').readAsBytesSync(); + expect(utf8.encode(recoveryAnnex), file); + expect(toHex(sha256(file)), recoveryAnnexSha256); + }); + + test( + 'annex/recovery.md is §79 of the specification, as TestRecoveryAnnex', + () { + final commit = vendored.check(root, tree: vendored.annexTree).commit; + final path = + '$commit:spec/DateKeys_Protocol_Specification_v$specVersion.md'; + final r = Process.runSync( + 'git', + ['-C', repo.path, 'show', path], + stdoutEncoding: null, + stderrEncoding: utf8, + ); + expect(r.exitCode, 0, reason: '${r.stderr}'); + final spec = r.stdout as List; + expect( + File('${root.path}/annex/recovery.md').readAsStringSync(), + annexOf(spec), + ); + }, + skip: repo.existsSync() + ? false + : '../datekeys-go is missing: the spec is read from it', + ); +} + +// The text of RecoveryAnnex for the specification spec, as recoveryAnnex of +// annex_test.go of Go: its §79, from its title to the end of the document, +// under a title of its own and a paragraph that names the version and the +// SHA-256 of spec. +String annexOf(List spec) { + final text = utf8.decode(spec); + final i = text.indexOf('\n## 79. '); + if (i < 0) throw StateError('the specification $specVersion has no §79'); + // strings.TrimRight(text[i+1:], " \n") + var end = text.length; + while (end > i + 1 && (text[end - 1] == ' ' || text[end - 1] == '\n')) { + end--; + } + return '# Cómo abrir una cápsula DateKeys sin software de DateKeys\n\n' + '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 v$specVersion, cuyo texto tiene ' + 'el SHA-256 ${toHex(sha256(spec))}. Es el mismo para toda cápsula: no ' + 'lleva ningún dato de esta.\n\n' + '${text.substring(i + 1, end)}\n'; +} diff --git a/test/formats_objects_test.dart b/test/formats_objects_test.dart index cfaf538..e7c4a47 100644 --- a/test/formats_objects_test.dart +++ b/test/formats_objects_test.dart @@ -3,7 +3,8 @@ // header.test.ts, control.test.ts, accesskey.test.ts and profile.test.ts of // datekeys-ts: values of a fixed seed that encode and decode back to // themselves, the copies of the secrets and their wiping, the texts that -// hide them, and the pinned registry. The values and texts of Go are in the +// hide them, and the pinned registry, with the state of its profiles of spec +// §71, as TestStatus of Go. The values and texts of Go are in the // differential (formats_header.json, formats_control.json, // formats_framing.json, formats_profile.json and formats_encode.json). It // reads no file: it runs on the VM and compiled to JavaScript. @@ -256,6 +257,32 @@ void main() { expect(quicknet(), isNot(copy)); }); + test('Quicknet is active in the registry of §71, as TestStatus of Go', () { + expect(profileStatusOf(profileHash(q)), ( + status: ProfileStatus.active, + known: true, + )); + // A profile that this release does not know: the zero Status of Go. + for (final h in [Uint8List(32)..[0] = 1, Uint8List(0)]) { + expect(profileStatusOf(h), ( + status: ProfileStatus.active, + known: false, + )); + } + expect( + {for (final s in ProfileStatus.values) s: '$s'}, + { + ProfileStatus.active: 'active', + ProfileStatus.readOnly: 'read-only', + ProfileStatus.compromised: 'compromised', + }, + ); + expect( + [for (final s in ProfileStatus.values) s.label], + ['active', 'read-only', 'compromised'], + ); + }); + test('a network "default", or none, is left out of the chain hash', () { final d = q.copyWith(network: 'default'); expect(chainInfoHash(d), chainInfoHash(q.copyWith(network: ''))); diff --git a/tool/recovery_annex_copy.dart b/tool/recovery_annex_copy.dart new file mode 100644 index 0000000..1a39a19 --- /dev/null +++ b/tool/recovery_annex_copy.dart @@ -0,0 +1,93 @@ +// Writes lib/src/recovery_annex.g.dart: the text of annex/recovery.md, the +// recovery annex that the official SDK saves next to each .dkc (spec §62.1, +// rule 27), as the Dart constant recoveryAnnex, since Dart has no go:embed; +// and its SHA-256, recoveryAnnexSha256, for the tests compiled to +// JavaScript. test/annex_vm_test.dart checks that the constant is the file +// byte for byte. Run it after every sync of annex/: +// +// dart run tool/recovery_annex_copy.dart +// +// It writes only an annex/ that matches its SOURCE.json, the check of +// tool/sync_testdata.dart. The text goes in a multi-line string, each line of +// the file a line of the source: a backslash, a dollar sign and a quote are +// escaped, and so is, as a Unicode escape, every rune but the line feed that +// Go's strconv.IsPrint rejects, such as a CR, a tab, a no-break space or a +// control of bidirectional text, so that no character of the text is +// invisible in the source and the line ends of a checkout never reach it. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:datekeys/src/bytes.dart' show isPrint; + +import 'sync_testdata.dart' as vendored; + +const _lineFeed = 0x0a; +const _backslash = 0x5c; +const _escaped = {_backslash, 0x24, 0x27}; // \ $ ' + +void main() { + final root = File(Platform.script.toFilePath()).parent.parent; + final r = vendored.check(root, tree: vendored.annexTree); + if (r.errors.isNotEmpty) { + throw StateError('annex/ does not match its SOURCE.json: ${r.errors}'); + } + final bytes = File('${root.path}/annex/recovery.md').readAsBytesSync(); + final text = utf8.decode(bytes); + if (!_sameBytes(utf8.encode(text), bytes)) { + throw StateError('annex/recovery.md does not decode to its own bytes'); + } + File('${root.path}/lib/src/recovery_annex.g.dart').writeAsStringSync( + '// Generated by tool/recovery_annex_copy.dart from annex/recovery.md,\n' + '// the recovery annex of datekeys-go. Do not edit: sync annex/ and run\n' + '// the tool again.\n' + '\n' + '/// The text that the official SDK saves next to each .dkc (spec §62.1,\n' + '/// rule 27), as RecoveryAnnex of datekeys-go: the informative annex of\n' + '/// the specification on how to open a capsule without DateKeys software\n' + '/// (spec §79), in Spanish, under a title that names the version of the\n' + '/// specification and the SHA-256 of its text. It is the same for every\n' + '/// capsule and holds nothing of one. Its UTF-8 bytes are\n' + '/// annex/recovery.md of the Go reference, which checks it against §79 of\n' + '/// its specification; the official SDK writes them as they are, to the\n' + '/// name of the .dkc followed by recoveryAnnexSuffix.\n' + "const recoveryAnnex = '''\n" + "${_literal(text)}''';\n" + '\n' + '/// The SHA-256 of the UTF-8 bytes of [recoveryAnnex], for the tests\n' + '/// that run compiled to JavaScript, where no file can be read.\n' + '///\n' + '/// Internal: lib/datekeys.dart does not export it.\n' + 'const recoveryAnnexSha256 =\n' + " '${sha256.convert(bytes)}';\n", + ); +} + +// The body of a multi-line Dart string, without its quotes, whose value is +// text. +String _literal(String text) { + final out = StringBuffer(); + for (final r in text.runes) { + if (_escaped.contains(r)) { + out + ..writeCharCode(_backslash) + ..writeCharCode(r); + } else if (r == _lineFeed || isPrint(r)) { + out.writeCharCode(r); + } else { + out + ..writeCharCode(_backslash) + ..write('u{${r.toRadixString(16)}}'); + } + } + return out.toString(); +} + +bool _sameBytes(List a, List b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; +} diff --git a/tool/sync_testdata.dart b/tool/sync_testdata.dart index 80e5491..fe21741 100644 --- a/tool/sync_testdata.dart +++ b/tool/sync_testdata.dart @@ -44,7 +44,9 @@ const testdataTree = Tree('testdata', 'testdata'); /// lib/src/wordlist.dart pins. const wordlistsTree = Tree('wordlists', 'wordkey/lists'); -/// annex/, the recovery annex of datekeys.RecoveryAnnex. +/// annex/, the recovery annex of datekeys.RecoveryAnnex, which +/// tool/recovery_annex_copy.dart writes as the constant of +/// lib/src/recovery_annex.g.dart. const annexTree = Tree('annex', 'annex'); /// The trees, in the order of the lines of check.