diff --git a/CHANGELOG.md b/CHANGELOG.md index b95761a..a257c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ Cambios notables de la librería Dart. El proyecto usa versionado semántico; mi ## Especificación 0.11, en la rama `v0.11` — sin versión +### Etapa 2: primitivas y `age` (05-10-2026) + +- **Primitivas, de código propio.** Son internas: `lib/datekeys.dart` no las exporta. + - SHA-256 con su compresión, y HMAC-SHA256 con los estados interior y exterior de la clave calculados una vez. Sobre ellos, HKDF-SHA256 (RFC 5869) y PBKDF2-HMAC-SHA256 (RFC 8018), cuyas iteraciones son dos compresiones sobre palabras: 600 000 iteraciones tardan 1 s en la VM, frente a unos 3,5 s con el HMAC de `package:crypto`. + - scrypt (RFC 7914) con Salsa20/8, con las comprobaciones y los textos de `scrypt.Key` de Go. + - ChaCha20, Poly1305 en diez limbs de 13 bits, y ChaCha20-Poly1305 (RFC 8439), con el tag comparado en tiempo constante. + - X25519 (RFC 7748) sobre el cuerpo de TweetNaCl en doubles, con clamping y el secreto todo a ceros rechazado con el texto de `crypto/ecdh`. + - Ed25519 estricto (§29.9), port de `internal/ed25519strict`: `verifyStrict`, `canonical`, `smallOrder` y `onCurve`. La aritmética de los puntos es la de TweetNaCl; los escalares módulo ℓ y `onCurve` usan `BigInt`, que no es de tiempo constante, solo con datos públicos. + - El Base64 de Go, con su modo estricto y el offset de sus errores, y el Bech32 de `age`, con sus textos. + - De `package:crypto` solo se usa SHA-512, en Ed25519. +- **Lectura de `age` v1**, port de `filippo.io/age` v1.3.2 (`lib/src/age.dart`): + - la cabecera y sus límites de `internal/format`, con los textos de error de Go; + - el MAC de la cabecera, sobre la cabecera escrita otra vez como la escribe `age`; + - los stanzas X25519 y scrypt. La identity de scrypt rechaza por defecto un factor de trabajo por encima de 16, como `authorkey` de Go; el de `age` es 22; + - la clave del payload y el STREAM de `internal/stream`, que se descifra según llega el texto cifrado, con los mismos casos de fin de fichero que el lector de Go: sin chunk final, chunk que no se autentica, último chunk vacío y datos tras el final; + - cada fallo es una `AgeException` con el texto de Go y su fase, la cabecera o el payload, para que la etapa 4 dé las razones fijas de `capsule.classify`. La `DateKeysException` de una identity pasa sin cambios. +- **`agewrap`** (`lib/src/agewrap.dart`): las reglas de stanzas de `OUTER_TIME_AGE`, `PAYLOAD_AGE` e `INNER_ACCESS_AGE`, la lectura de los stanzas sin secretos, `PayloadIdentity` y `AccessIdentity`, con los textos y los códigos de Go. `checkTimeStanzas` recibe la ronda, el chain hash y el id del perfil, que llegará con la etapa 4. +- **Vectores de Go** en `test/vectors/`, que escriben `tool/gen_primitive_vectors.go` y `tool/gen_age_vectors.go` con las librerías de la caché de módulos, en el contexto del módulo de `datekeys-go` y sin cambiar nada en él: + - `primitives.json` y su copia en Dart, `primitives.g.dart`: RFC 5869, 7748, 7914, 8032 (por `sign.input` de Go) y 8439, PBKDF2 con el vector del §38.1, los puntos de orden pequeño, Base64 y Bech32. Incluye dos mensajes con los que una suma de productos de Poly1305 pasa de 2^32: un acarreo tomado con un desplazamiento fallaría en la web, y una prueba lo detecta en Node; + - `age.json`: ficheros X25519 y scrypt, sus cortes y manipulaciones, un corpus de cabeceras contra la gramática del §28.1 y el límite de 2 MiB, y las reglas e identities de `agewrap`, con el texto de Go en cada caso; + - `age_fixtures.json`: el `PAYLOAD_AGE` de los 24 fixtures con su `payload_identity`, y el `INNER_ACCESS_AGE` de los 6 `time_and_key`, sacado de `OUTER_TIME_AGE` con el release del fixture. +- **Pruebas.** 296 en la VM y una aplazada, 127 más que en la etapa 1; 83 en Node.js, 27 más. Las de las primitivas y las de `age` sin ficheros corren también compiladas a JavaScript, sin los casos largos (PBKDF2 a 600 000 iteraciones, scrypt con logN 16, X25519 iterada 1000 veces). +- **Fallos inyectados**, uno a uno y revertidos: 46 en las primitivas, `age` y `agewrap`. Las pruebas detectan todos los que cambian un resultado. Cuatro no lo cambian: la máscara de una mitad de rotación de SHA-256 cuyos bits altos solo llegan a sumas enmascaradas, el redondeo de `_car` cuando sus entradas nunca son negativas, el de un acarreo del producto del cuerpo, que conserva el valor, y el acarreo del limb 1 de Poly1305 tomado con desplazamiento, cuya suma no pasa de 2^32. +- **`tool/bench.dart`** mide las primitivas en la VM y compilado a JavaScript; las cifras, en el README. + ### El gate con Node.js (05-10-2026) - `tool/check.sh` corre también `dart test -p node`: las pruebas que no leen ficheros, compiladas a JavaScript, comprueban en cada commit que los enteros son exactos en la web. Lo decidió el autor. diff --git a/README.md b/README.md index 3c64023..9f7b671 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ Librería Dart del protocolo DateKeys: DateKey, DateKeyCap (`.dkc`) y DateKeys Access Key (`.dkk`). Es Dart puro, sin Flutter ni plataforma, para que la app Flutter la use como dependencia de ruta (`datekeys: { path: ../datekeys-dart }`). -Es la tercera implementación de la especificación, después de la de referencia en Go (`datekeys-go`) y la de TypeScript (`datekeys-ts`). Como la de TypeScript, no genera datos de prueba propios: se contrasta con el `testdata/` de `datekeys-go`. +Es la tercera implementación de la especificación, después de la de referencia en Go (`datekeys-go`) y la de TypeScript (`datekeys-ts`). Como la de TypeScript, no genera datos de prueba propios: se contrasta con el `testdata/` de `datekeys-go`, y con los vectores de `test/vectors/`, que calcula Go con los programas de `tool/` (ver «Datos de prueba»). ## Estado -Están hechas las etapas 0 y 1 del plan (`docs/PLAN_dart.md` del espacio de trabajo): +Están hechas las etapas 0, 1 y 2 del plan (`docs/PLAN_dart.md` del espacio de trabajo): - la etapa 0, el paquete, sus herramientas y `testdata/` sincronizado; -- la etapa 1, los errores normativos, los bytes, el perfil CBOR del §58 y el DER estricto, también el de los tiempos. +- la etapa 1, los errores normativos, los bytes, el perfil CBOR del §58 y el DER estricto, también el de los tiempos; +- la etapa 2, las primitivas y la lectura de `age`. La etapa 1 porta tres ficheros de `datekeys-go` en `601e6d2`, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error: @@ -19,23 +20,59 @@ La etapa 1 porta tres ficheros de `datekeys-go` en `601e6d2`, con las mismas lec | `lib/src/cbor.dart` | `CborEncoder`, `CborDecoder`, `unmarshalCbor`, `peekSchema`, `checkSchema` y `walkCbor` | `codec/codec.go` | | `lib/src/der.dart` | `check`, `split`, `content`, `setOfSorted` y `parseTime`, con UTCTime y GeneralizedTime exactos al nanosegundo. Es interno, como en Go | `internal/der/der.go`, ya del borrador v0.12 | -`lib/datekeys.dart` exporta los errores, el CBOR y las funciones de bytes que necesita quien use la librería. +La etapa 2 trae las primitivas, todas de código propio salvo SHA-512, y la lectura de los ficheros `age` de `filippo.io/age` v1.3.2. Son internas, como en Go y en `datekeys-ts`: `lib/datekeys.dart` no las exporta. + +| Módulo | Contenido | En Go | +|---|---|---| +| `lib/src/sha256.dart` | SHA-256 con su compresión; HMAC-SHA256 con los estados interior y exterior de la clave calculados una vez; HKDF-SHA256 (RFC 5869); PBKDF2-HMAC-SHA256 (RFC 8018), cuyas iteraciones son dos compresiones sobre palabras, sin bytes entre medias | `crypto/sha256`, `crypto/hmac`, `x/crypto/hkdf`, `x/crypto/pbkdf2` | +| `lib/src/scrypt.dart` | scrypt (RFC 7914) con Salsa20/8, con las comprobaciones y los textos de `scrypt.Key` | `x/crypto/scrypt` | +| `lib/src/chacha20poly1305.dart` | ChaCha20, Poly1305 y ChaCha20-Poly1305 (RFC 8439); el tag se compara en tiempo constante | `x/crypto/chacha20poly1305` | +| `lib/src/curve25519.dart` | X25519 (RFC 7748) con clamping y el secreto todo a ceros rechazado; la verificación estricta de Ed25519 del §29.9, con `canonical`, `smallOrder` y `onCurve` | `x/crypto/curve25519`, `internal/ed25519strict` | +| `lib/src/base64.dart` | El Base64 de Go, con su modo estricto y el offset de sus errores | `encoding/base64` | +| `lib/src/bech32.dart` | El Bech32 de `age`, con sus textos de error | `codec/bech32` | +| `lib/src/age.dart` | La cabecera y sus límites (2 MiB, 1024 stanzas, un tipo y 128 argumentos por stanza), el MAC de la cabecera, los stanzas X25519 y scrypt, la clave del payload y el STREAM, con los mismos casos de fin de fichero que Go. Cada fallo es una `AgeException` con el texto de Go y su fase: la cabecera (`age.Decrypt`) o el payload (la lectura del STREAM) | `filippo.io/age`, `internal/format`, `internal/stream` | +| `lib/src/agewrap.dart` | Las reglas de stanzas de `OUTER_TIME_AGE`, `PAYLOAD_AGE` e `INNER_ACCESS_AGE`, la lectura de los stanzas sin secretos y las identities del payload y del acceso, con los textos y los códigos de Go | `agewrap` | + +Notas de la etapa 2: +- **La identity de scrypt** rechaza por defecto un factor de trabajo por encima de 16, el de los ficheros de clave de autor (§29.12), como `SetMaxWorkFactor(16)` en el `authorkey` de Go. El de `age` es 22: 4 GiB de memoria, que un fichero hostil podría pedir a un móvil. `ScryptIdentity(maxWorkFactor: …)` lo cambia. +- **El STREAM** se descifra según llega el texto cifrado (`AgePayloadDecryptor`), como el lector de Go: entrega el texto de cada chunk en cuanto se autentica, y un fallo que revela un byte posterior llega en la llamada siguiente. +- **La identity de `OUTER_TIME_AGE`** necesita tlock y llega con la etapa 3; la del perfil, con la etapa 4. Por eso `checkTimeStanzas` recibe la ronda, el chain hash y el id del perfil. Los enteros son exactos en la VM y en la web. El `int` de Dart tiene 64 bits con signo en la VM y en la web es un double, exacto hasta 2^53. Por eso la librería no usa un `int` por encima de 2^53-1, ni desplazamientos u operaciones de bits de más de 31 bits: - un entero de CBOR es un `int` hasta 2^53-1 y un `BigInt` por encima, como el `number | bigint` de `datekeys-ts`; -- `CborDecoder.uint` devuelve un `int`, porque todos los esquemas acotan sus enteros en 2^53-1, y `uint64` devuelve un `BigInt`. +- `CborDecoder.uint` devuelve un `int`, porque todos los esquemas acotan sus enteros en 2^53-1, y `uint64` devuelve un `BigInt`; +- SHA-256, ChaCha20 y Salsa20 trabajan con palabras de 32 bits: suman de dos a cinco y enmascaran la suma, y en cada rotación enmascaran la mitad desplazada a la izquierda, así que la VM y la web dan los mismos 32 bits; +- Poly1305 guarda el acumulador y r en diez limbs de 13 bits: cada producto queda por debajo de 2^31 y cada suma por debajo de 2^35, y sus acarreos se toman con `~/`, no con un desplazamiento, que en la web truncaría a 32 bits; +- el cuerpo de curve25519 es el de TweetNaCl en su versión de JavaScript: dieciséis limbs de 16 bits en un `Float64List`, con sumas de productos por debajo de 2^44. + +**`BigInt` no es de tiempo constante.** La librería lo usa solo con datos públicos: la reducción de escalares módulo ℓ y las comprobaciones de `onCurve` en Ed25519, que solo verifica, y el factor de trabajo de un stanza scrypt. Los secretos (el escalar de X25519, las claves de HMAC, ChaCha20 y Poly1305) van en la aritmética de limbs, sin ramas ni índices que dependan de ellos; ni la VM ni un motor de JavaScript prometen tiempo constante, aun así. Las etapas siguientes traen el resto del protocolo en este orden: | Etapa | Contenido | |---|---| -| 2 | Primitivas: HKDF, PBKDF2, scrypt, ChaCha20-Poly1305, X25519, Ed25519 estricto y `age` | | 3 | BLS12-381 y tlock | | 4 | Formatos, inspección (pasos 1 a 8) y apertura (pasos 9 a 18) | | 5 | Firma y sello, con los textos de los veredictos | | 6 | Escritor | | 7 | Localizador y claves de autor | +## Rendimiento + +`dart run tool/bench.dart` mide las primitivas en la VM; compilado a JavaScript, `dart compile js -O2 -o bench.js tool/bench.dart && node bench.js`. Las cifras del 5 de octubre de 2026, en el PC de desarrollo (Windows 11, Dart 3.13, Node.js 24.9): + +| Operación | VM | Node.js | +|---|---|---| +| X25519, un acuerdo de claves | 1,2 ms | 0,9 ms | +| Ed25519 estricto, una verificación | 4,5 ms | 9,4 ms | +| ChaCha20-Poly1305, 1 MiB, cifrar o descifrar | 21 ms | 16 ms | +| PBKDF2-HMAC-SHA256, 600 000 iteraciones (§38.1) | 1,0 s | 0,9 s | +| scrypt con logN 16 y r 8 (§29.12) | 0,55 s | 0,9 s | + +En la VM, el HMAC de `package:crypto`, que calcula otra vez los estados de la clave en cada llamada, tarda unos 3,5 s en las mismas 600 000 iteraciones. + +La app llama a PBKDF2 y a scrypt en un `Isolate` (plan, «Rendimiento»). Las cifras en un móvil de gama media se miden en la etapa 3. + ## Versiones | Número | Dónde | Hoy | @@ -47,7 +84,7 @@ La rama sigue la versión de la especificación: `v0.11`, hasta que `datekeys-go ## Dependencias -- **En ejecución:** solo `package:crypto`, para SHA-1, SHA-2 y HMAC. Todo lo demás es código propio: HKDF, PBKDF2, scrypt, ChaCha20-Poly1305, X25519, Ed25519, BLS12-381, ECDSA, RSA, DER, CMS y CBOR. +- **En ejecución:** solo `package:crypto`, para SHA-1, SHA-2 y HMAC. Todo lo demás es código propio: HKDF, PBKDF2, scrypt, ChaCha20-Poly1305, X25519, Ed25519, BLS12-381, ECDSA, RSA, DER, CMS y CBOR. Hoy la librería usa de `package:crypto` solo SHA-512, en Ed25519: SHA-256 y HMAC-SHA256 son propios, porque PBKDF2 los necesita con los estados de la clave calculados una vez. - **En desarrollo:** solo `package:test`, aprobado el 5 de octubre de 2026. No hay paquete de lints: las reglas del análisis están en `analysis_options.yaml`. - **`pubspec.lock`** va en el repositorio, para que las pruebas usen siempre las mismas versiones. @@ -86,6 +123,27 @@ dart run tool/sync_testdata.dart check --against ../datekeys-go - Los ficheros de `testdata/` no se editan ni se generan aquí. - En cada `dart test`, `test/testdata_test.dart` comprueba la copia, y que cada fichero nombre `specVersion`. +`test/vectors/` tiene los vectores de las primitivas y de `age`. Los escribe Go, con las librerías de la caché de módulos que usan `datekeys-go` y `filippo.io/age`; 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: + +```bash +cd ../datekeys-go && go run ../datekeys-dart/tool/gen_primitive_vectors.go -out ../datekeys-dart/test/vectors +``` + +```bash +cd ../datekeys-go && go run ../datekeys-dart/tool/gen_age_vectors.go -out ../datekeys-dart/test/vectors +``` + +| Fichero | Contenido | +|---|---| +| `primitives.json` | SHA-256, HMAC, HKDF (RFC 5869), PBKDF2 (con el vector del §38.1), scrypt (RFC 7914), ChaCha20, Poly1305 y ChaCha20-Poly1305 (RFC 8439), X25519 (RFC 7748, BoringSSL y los puntos de orden pequeño), Ed25519 (las 64 primeras líneas de `sign.input` de Go, cuyas tres primeras son las de la RFC 8032, y sus mutaciones), el Base64 de Go y el Bech32 de `age` | +| `primitives.g.dart` | El mismo JSON como constante de Dart, para las pruebas compiladas a JavaScript, que no leen ficheros | +| `age.json` | Ficheros con stanzas X25519 y scrypt, sus cortes y manipulaciones, un corpus de cabeceras contra la gramática del §28.1 y el límite de 2 MiB, y las reglas e identities de `agewrap`, con el texto del error de Go en cada caso | +| `age_fixtures.json` | El `PAYLOAD_AGE` de cada fixture con su `payload_identity`, y el `INNER_ACCESS_AGE` de los `time_and_key`, que Go saca de `OUTER_TIME_AGE` con el release del fixture | + +- Los fixtures que leen los generadores son los de `testdata/` de este repositorio, la copia sincronizada. +- `primitives.json` sale igual en cada ejecución. `age` saca sus claves y nonces de `crypto/rand`, así que `age.json` y `age_fixtures.json` cambian en cada ejecución; las pruebas leen lo que esté en el repositorio. +- Un fichero `age` de más de un chunk se guarda como su cabecera, su nonce y su file key: la prueba cifra otra vez el texto documentado y comprueba el SHA-256 del fichero entero antes de leerlo. Las cabeceras de megabytes se escriben como partes que se repiten. + ## Licencia Apache-2.0 (`LICENSE`), como `datekeys-go` y `datekeys-ts`. La especificación tiene su propia licencia, CC-BY-4.0. diff --git a/lib/datekeys.dart b/lib/datekeys.dart index 9a870b9..dd7d1a0 100644 --- a/lib/datekeys.dart +++ b/lib/datekeys.dart @@ -2,10 +2,11 @@ /// DateKeys Access Key (`.dkk`), as `datekeys-go` and `datekeys-ts` implement /// them, checked against the same test data. /// -/// Stages 0 and 1 of docs/PLAN_dart.md: the package and its test data, the +/// Stages 0 to 2 of docs/PLAN_dart.md: the package and its test data, the /// normative errors of spec §69, byte helpers and the CBOR profile of spec -/// §58. DER, internal in the Go reference, is internal here too. The protocol -/// arrives stage by stage. +/// §58, then the primitives and the reading of age files. DER, the +/// primitives, age and agewrap are internal, as in the Go reference. The +/// protocol arrives stage by stage. library; export 'src/bytes.dart'