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`, y con los vectores de `test/vectors/`, que calcula Go con los programas de `tool/` (ver «Datos de prueba»).
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:
| Módulo | Contenido | En Go |
|---|---|---|
| `lib/src/errors.dart` | Los 19 códigos del §69 en su orden (`ErrorCode`), `DateKeysException` con el mensaje `contexto: CÓDIGO`, `wrap`, `withContext` y `errorCode` | `errors.go` |
| `lib/src/bytes.dart` | Hexadecimal, comparación y concatenación; UTF-8 estricto que conserva un U+FEFF inicial; `utf8.DecodeRune` y el `%q` de Go, con la tabla de `strconv.IsPrint` de Go 1.26 | `unicode/utf8`, `strconv` |
| `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 |
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`;
- 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í.
`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 |
| 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.
- **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.
Nada nuevo sin aprobación escrita del autor.
## Comprobar
```bash
dart pub get
```
```bash
tool/check.sh
```
`tool/check.sh` es el gate local, como `scripts/check.sh` en `datekeys-go` y `npm run verify` en `datekeys-ts`. Comprueba:
-`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;
- la copia de `testdata/` 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 del tag `spec-v0.11` (`ae33434`).
```bash
dart run tool/sync_testdata.dart sync --commit spec-v0.11
```
```bash
dart run tool/sync_testdata.dart check --against ../datekeys-go
```
-`sync` lee los ficheros con git en ese commit, así que nunca entran cambios sin commit del repositorio de 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.