The modules of the primitives and of age, the integer bounds of each, the
note that BigInt is not constant time and where it is used, the vectors of
test/vectors/ and how Go writes them, and the timings on the VM and on
Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -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.
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/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:
| 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.