diff --git a/CHANGELOG.md b/CHANGELOG.md index c93d16c..9c6f5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,37 @@ 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 7b: las claves de autor y el sellado del localizador (06-10-2026) + +- **Las claves de autor** (`lib/src/authorkey.dart`), port del paquete `authorkey` de `datekeys-go` en `c531e93`, el borrador v0.12 (§29.9, §29.12), con las mismas comprobaciones en el mismo orden y los mismos textos de error: + - `AuthorKey`: `generate`, con la `RandomSource` de la 6a, `fromSeed`, `publicKey`, `publicString`, `sign`, `clear` y `secret`, y un `toString` que oculta la clave secreta, como `String` y `GoString` de Go; + - las cadenas: `authorPublicString`, `parseAuthorPublic`, con una clave que el perfil estricto podría aceptar, y `parseAuthorSecret`, también sobre los bytes de un string de Go; + - el fichero: `marshalAuthorKey`; `encryptAuthorKey`, con `ScryptRecipient` y `ageEncrypt` de la 6a y un factor de trabajo de 16; y `readAuthorKey`, en claro o cifrado, con el lector de `age` y un máximo de 16, de 64 KiB como mucho y con las líneas de `bufio.Scanner`. + + Los errores son una `AuthorKeyException` con el texto de Go. La caja de una cadena y los espacios de una línea son los de `strings.ToLower`, `strings.ToUpper` y `strings.TrimSpace` de Go, con sus tablas de Unicode 15.0.0 (`lib/src/go_unicode.dart`, que escribe `tool/go_unicode_tables.go`), así que cualquier entrada, también la que no es UTF-8, da el error de Go. +- **La firma Ed25519** (`lib/src/ed25519_sign.dart`, interno), el `crypto_sign` de TweetNaCl en su versión de JavaScript, con el SHA-512 de `package:crypto`: el cuerpo de `curve25519.dart`, copiado con el producto como un bucle, y `modL` en 64 limbs de 8 bits, exacto en la VM y en la web. El escalar secreto y el nonce no pasan por `BigInt` ni por una rama, y se borran tras usarlos; nada promete tiempo constante. +- **El sellado del localizador y el sobre** (`lib/src/locator_seal.dart`), `Seal` y `NewEnvelope` del paquete `locator`: `sealLocator` escribe el texto del localizador, crea el recipient tlock de la ronda y cifra, en el orden de Go; `newEnvelope` saca I_SOBRE, cifra el `.dkc` con `ageEncrypt` y lo corta con `splitEnvelope` de la 7a. +- **Una diferencia con Go, a propósito:** una clave borrada con `clear` lanza un `StateError` en cada uso, donde Go da los valores de una clave a ceros y una firma que nunca verifica. +- **Algo de Go que se conserva:** el texto de `PublicString` con una clave de otra longitud tiene los números al revés, «a public key has 32 bytes, not 31». +- **`lib/datekeys.dart`** exporta `authorkey.dart`, salvo `parseAuthorPublicUtf8` y `parseAuthorSecretUtf8`, y `locator_seal.dart`. La documentación de `locator.dart` remite al sellado. +- **Vectores de Go**, en el módulo de `datekeys-go` y sin cambiarlo, mientras `crypto/rand` lee el keystream de `SeededRandomSource`: + - `tool/authorkey_go_vectors.go` escribe `authorkey.json`: las firmas de `crypto/ed25519` sobre líneas de `sign.input`, con los tests 1, 2, 3 y 1024 de la RFC 8032, TEST SHA(abc), semillas de una semilla con mensajes de hasta 1 MiB y otras claves públicas; los escalares de `math/big`; y `authorkey` entero con sus textos: 1 288 cadenas, 1 620 runas de los bordes de los conjuntos de Go y 111 ficheros. `Generate` lee `crypto/rand.Reader` gracias a `//go:debug cryptocustomrand=1`. `authorkey.g.dart` lleva una parte, para Node.js; + - `tool/locator_seal_go_vectors.go` escribe `locator_seal.json`: `Seal` de localizadores de uno a tres bloques para rondas de 1 a la última de Quicknet, sus rechazos, `NewEnvelope` de `.dkc` de 0 bytes a 1 MiB y el camino entero. + + Con la misma semilla, esta librería saca los mismos valores y escribe los mismos bytes que Go en cada caso, y da los mismos textos. +- **En la otra dirección:** `tool/seal_interop_dart_samples.dart` escribe, de las recetas de `test/seal_interop_support.dart`, localizadores sellados de las rondas 1000, 1001, 1004 y 2000 con sus sobres, ficheros de clave en claro y cifrados y firmas de 0 a 70 000 bytes, y `tool/seal_interop_go_verdicts.go` los abre con `locator.Open`, `OpenEnvelope`, `authorkey.Read` y `crypto/ed25519`. Go lee los quince, y `seal_interop.json` guarda sus veredictos y el SHA-256 de cada fichero, que las pruebas escriben otra vez. +- **Pruebas:** 171 nuevas en la VM y 160 en Node.js: 1884 y 592 en total. En Node.js corren las firmas marcadas, una de cada ocho runas y todo lo demás, salvo cuatro ficheros con scrypt de logN 16 o 17 y los sobres de más de 64 KiB. +- **Fallos inyectados**, uno a uno y revertidos, con las pruebas de la parte en la VM. Las pruebas detectan los veinte: la firma sin el bit 254 del escalar, sin el bit 0 de la escalera, con R y A en otro orden en el hash o sin el signo de x en R; `modL` sin el redondeo de sus acarreos o sin su última resta de ℓ; un byte que no es UTF-8 que no cambia la caja; los espacios de Unicode no recortados por la izquierda, y solo el espacio y el tabulador por la derecha; el token de `bufio.Scanner` un byte más largo; un factor de trabajo de 17 aceptado al leer, y uno de 15 al escribir; el comentario mirado antes de recortar la línea; una runa aceptada al final de una línea sin que acabe en ella; una clave de orden pequeño aceptada; la posición del separador comprobada con un byte menos; una semilla de 33 bytes sacada al generar; el recipient tlock creado antes de escribir el localizador; el sobre cifrado con otra fuente de lo aleatorio; y las tablas de Go sin el paso de sus rachas. Dos se escapaban al principio, la runa al final de una línea y el separador, y el generador escribe ahora sus casos. Otros dos fallos eran equivalentes, sin efecto posible: el bit 255 de la escalera, siempre cero en un escalar recortado o reducido, y un límite de tres bytes en `DecodeLastRune`, porque ningún espacio mide cuatro. +- **`tool/authorkey_bench.dart`** mide la parte. En la VM, una firma de 99 bytes tarda 4,5 ms, como la verificación estricta, y en Node.js 9 ms; el fichero de una clave, con scrypt de logN 16, 0,57 s al escribirlo o leerlo, y entre 0,8 y 1,0 s en Node.js; y sellar un localizador, 30 ms y 0,52 s. Las cifras, en el README. +- **Integración con la 6b:** la 7b se puso encima de la 6b con `rebase`. La interfaz del enganche de la firma de `alg` 1, que la 6b llamaba `AuthorKey`, pasa a llamarse `AuthorSigner`, y la `AuthorKey` de la 7b la implementa. Las pruebas del escritor firman ahora las cápsulas de `alg` 1 con la `AuthorKey` de la semilla de Go, no con la firma grabada, y escriben los mismos bytes que Go. Con las dos partes, el gate pasa con 2059 pruebas en la VM y 665 en Node.js. + ### Etapa 6b: el escritor de cápsulas del formato 3 (06-10-2026) - **El escritor** (`lib/src/encrypt3.dart`), port de `EncryptFiles`, `sealer` y el área de seguridad de `encrypt.go` y `encrypt3.go` de `datekeys-go` en `c531e93`, con las mismas comprobaciones en el mismo orden, los mismos códigos y textos de error y los mismos valores aleatorios, sacados en el mismo orden: - `encryptFiles` escribe en un `ByteSink` los ficheros de sus `FileSource`, cada uno leído dos veces en streaming, con su SHA-256 comprobado en la segunda lectura. El sink se cierra cuando la cápsula está completa y comprobada, y se aborta ante cualquier fallo; - `time_only` y `time_and_key`, con recipients X25519, una `.dkk` nueva y la llave de palabras, de 1 a 16 credenciales, en los 16 huecos de `INNER_ACCESS_AGE` con señuelos y en un orden al azar; - el head, con los ficheros en el orden de los bytes de sus rutas, su mtime, el comentario y el autor declarado; la nota pública; las extensiones de cada objeto, con la regla del §72; y el relleno; - - el área de seguridad de 32 KiB, la de 64 KiB con `largeArea` y la de prueba con `testAreaLen`: vacía, o con la firma de `AuthorKey` (`alg` 1) o de `CmsSigner` (`alg` 2) y el sello de `Sealer`, hechos antes de escribir nada y evaluados por el lector de esta librería; + - el área de seguridad de 32 KiB, la de 64 KiB con `largeArea` y la de prueba con `testAreaLen`: vacía, o con la firma de `AuthorSigner` (`alg` 1) o de `CmsSigner` (`alg` 2) y el sello de `Sealer`, hechos antes de escribir nada y evaluados por el lector de esta librería; - las autocomprobaciones del §62.1: `PUBLIC_HEADER`, `CONTROL_CBOR`, el head, la trama de `BODY`, `INNER_ACCESS_AGE` con su `.dkk`, `PAYLOAD_AGE` y el área de seguridad; - `EncryptOptions.now`, obligatorio, es el reloj; - los errores son una `DateKeysException` con su código, un `ArgumentError` para las opciones que Go rechaza sin código, y una `CapsuleWriteException` para los demás. @@ -26,7 +50,6 @@ Cambios notables de la librería Dart. El proyecto usa versionado semántico; mi - **Pruebas:** 175 nuevas en la VM y 73 en Node.js, 1888 y 505 en total. Además de los vectores: nada llega al sink antes de la firma; el sink recibe primero el PRELUDE, y cada trozo es suyo; un sink que falla para la escritura y se aborta, y uno que no se cierra no se aborta; una fuente o un enganche que lanza una `DateKeysException` conserva su código; un `String` mal formado no es UTF-8 válido para Go; una fuente se lee en streaming; y, en la VM, dos cápsulas del CSPRNG difieren y se abren. - **Fallos inyectados**, uno a uno y revertidos. Las pruebas detectan los veinte: los valores del sellado de prueba sin sacar; los huecos sin permutar; las rutas ordenadas por unidades UTF-16; un mtime anterior a 1970 guardado; un CR suelto del comentario conservado; el área de 64 KiB siempre que se permite; los veredictos del área sin comprobar; el SHA-256 de la segunda lectura sin comparar; una fuente que da un byte de más en un trozo, aceptada; la nota pública perdida; un recipient repetido aceptado; un `credential_id` a ceros; la llave de palabras salada con la ronda siguiente; el sink sin abortar y sin cerrar; un área de prueba con `largeArea`; un instante igual al reloj aceptado; el relleno con un byte de menos; la sal del head con un byte a cero; y `SEALED_CONTROL_LEN` con uno de más. - **`tool/encrypt3_bench.dart`** mide el escritor: una cápsula pequeña tarda 48 ms en la VM y 0,71 s en Node.js, 0,12 s y 0,75 s con `time_and_key` y una `.dkk`; un fichero de 64 MiB, 4,7 s en la VM, y uno de 16 MiB, 1,95 s en Node.js. Las cifras, en el README. -- **Para después de integrar la 7b:** la firma de `alg` 1 con la clave de autor de la 7b, que implementa `AuthorKey`. ### Etapa 7a: el localizador y el sobre de `datekeys.capsule` (06-10-2026) diff --git a/README.md b/README.md index d6ce735..39cdaeb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Es la tercera implementación de la especificación, después de la de referenci ## Estado -Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trabajo), las partes 6a y 6b de la etapa 6 y la parte 7a de la etapa 7: +Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trabajo), las partes 6a y 6b de la etapa 6 y la etapa 7: - 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 2, las primitivas y la lectura de `age`; @@ -19,13 +19,15 @@ Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trab - la 5b, los compromisos de lo que firma un autor y sella un sello, `SECURITY_CBOR`, la firma de `alg` 1 y los veredictos con sus textos y sus líneas; - la 5a, el lector de las firmas CMS y de los sellos RFC 3161 con el perfil de certificado del borrador v0.12, ECDSA y RSA. Es interno; - la 5c, la firma de `alg` 2 y el sello de `seal_type` 2 en los veredictos, que une el lector de la 5a a la evaluación de la 5b; -- de la etapa 6, la parte 6a: la escritura de `age`, con la fuente inyectable de lo aleatorio, los recipients X25519, scrypt y tlock y las longitudes de un fichero antes de escribirlo. Es interna: la usa el escritor de la cápsula, la parte 6b, y la usarán las claves de autor, la etapa 7b. +- de la etapa 6, la parte 6a: la escritura de `age`, con la fuente inyectable de lo aleatorio, los recipients X25519, scrypt y tlock y las longitudes de un fichero antes de escribirlo. Es interna: la usan el escritor de la cápsula, la parte 6b, y las claves de autor, la parte 7b. - la parte 6b de la etapa 6, el escritor de cápsulas del formato 3: `time_only` y `time_and_key`, los 16 huecos con señuelos, la `.dkk`, la llave de palabras, la nota pública, el área de seguridad con la firma y el sello, el relleno y las autocomprobaciones del §62.1, con las fuentes leídas en streaming. -- la parte 7a de la etapa 7, 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. Sellar un localizador y crear un sobre necesitan el escritor de `age` de la etapa 6: son de la parte 7b. +- la etapa 7, en dos partes: + - 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. -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. +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. -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 después en la rama `v0.11`, en paralelo con la 7b, las claves de autor, en la rama `stage7b`. +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. 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: @@ -221,7 +223,7 @@ La parte 6b de la etapa 6 porta el escritor de cápsulas del formato 3 de `datek | Módulo | Contenido | En Go | |---|---|---| -| `lib/src/encrypt3.dart` | `encryptFiles`, con `EncryptOptions`, `FileSource`, `EncryptResult` y `CapsuleWriteException`; y los enganches de la firma y del sello: `AuthorKey` (`alg` 1), `CmsSigner` (`alg` 2) y `Sealer` (`seal_type` 2) | `EncryptFiles`, `EncryptOptions`, `Source`, `Result`, `AuthorKey`, `CMSSigner` y `Sealer` de `capsule` | +| `lib/src/encrypt3.dart` | `encryptFiles`, con `EncryptOptions`, `FileSource`, `EncryptResult` y `CapsuleWriteException`; y los enganches de la firma y del sello: `AuthorSigner` (`alg` 1), que es la `AuthorKey` de la 7b, `CmsSigner` (`alg` 2) y `Sealer` (`seal_type` 2) | `EncryptFiles`, `EncryptOptions`, `Source`, `Result`, `AuthorKey`, `CMSSigner` y `Sealer` de `capsule` | Notas de la parte 6b: - **Lo que escribe:** @@ -241,7 +243,7 @@ Notas de la parte 6b: - la trama de `BODY` y el final del head frente a `CONTENT`; - el área de seguridad se evalúa con el lector de esta librería, en el contexto de la cápsula, y debe dar los veredictos que se pidieron. - **La firma y el sello:** - - `AuthorKey` firma `AUTHOR_MESSAGE` con `alg` 1, y la firma se comprueba con el perfil estricto antes de escribir nada; + - `authorKey`, un `AuthorSigner` como la `AuthorKey` de la 7b, firma `AUTHOR_MESSAGE` con `alg` 1, y la firma se comprueba con el perfil estricto antes de escribir nada; - `CmsSigner` recibe `AUTHOR_MESSAGE` y devuelve la firma CMS, que debe estar completa, con un sello por firmante (F6); - `Sealer` sella `SEAL_SUBJECT` después de la firma; un sello S5 vale también, porque el reloj del escritor y el de la autoridad pueden diferir. @@ -279,6 +281,26 @@ Notas de la parte 7a: - **Los tipos de Go.** Un `Locator` tiene claves y resúmenes de 32 bytes, y ningún tamaño ni desplazamiento negativo, como los arrays y los `uint64` de Go: lo contrario es un `ArgumentError`. `CapsuleInfo.openLocator` usa el registro de perfiles por defecto, Quicknet, si no se le da uno, como la apertura de una cápsula; Go lo exige. - **Lo que se exporta.** `lib/datekeys.dart` exporta `locator.dart` entero, como el paquete público `locator` de Go. `ipaddr.dart` es interno. +La parte 7b de la etapa 7 porta el paquete `authorkey` de `datekeys-go` en `c531e93`, el borrador v0.12 (§29.9, §29.12), y `Seal` y `NewEnvelope` del paquete `locator` (§44.1), con las mismas comprobaciones en el mismo orden y los mismos textos de error. Con los mismos valores aleatorios escribe los mismos bytes que Go: + +| Módulo | Contenido | En Go | +|---|---|---| +| `lib/src/authorkey.dart` | `AuthorKey`, con `generate`, `fromSeed`, `publicKey`, `publicString`, `sign`, `clear`, `secret` y un `toString` que la oculta; `authorPublicString`, `parseAuthorPublic` y `parseAuthorSecret`, también sobre los bytes de un string de Go (`parseAuthorPublicUtf8`, `parseAuthorSecretUtf8`); `marshalAuthorKey`; `encryptAuthorKey`, con scrypt de logN 16; `readAuthorKey`; y `AuthorKeyException` | `authorkey` | +| `lib/src/ed25519_sign.dart` | La firma Ed25519 (RFC 8032, 5.1.5 y 5.1.6): `ed25519PublicKey` y `ed25519Sign`, el `crypto_sign` de TweetNaCl con el SHA-512 de `package:crypto`, y la reducción módulo ℓ de TweetNaCl, `modL` | `crypto/ed25519` | +| `lib/src/go_unicode.dart` | Los puntos de código que cambian `unicode.ToLower` y `unicode.ToUpper` de Go 1.26.8, Unicode 15.0.0, y los espacios de `unicode.IsSpace`, que escribe `tool/go_unicode_tables.go`; no se edita | `unicode` | +| `lib/src/locator_seal.dart` | `sealLocator`, `Seal` de Go, y `newEnvelope`, `NewEnvelope` de Go, sobre `TimeRecipient`, `generateX25519Identity` y `ageEncrypt` de la 6a y `splitEnvelope` de la 7a | `Seal` y `NewEnvelope` de `locator.go` | + +Notas de la parte 7b: +- **La firma** es la de TweetNaCl en su versión de JavaScript: el cuerpo de `curve25519.dart`, dieciséis limbs de 16 bits en un `Float64List`, con el producto como un bucle, porque `curve25519.dart` guarda su aritmética como privada y la parte solo crea ficheros nuevos; y los escalares módulo ℓ en 64 limbs de 8 bits, con los acarreos de `modL` como divisiones por 256 redondeadas hacia abajo, que son los desplazamientos de TweetNaCl para sus valores, por debajo de 2^34. El escalar secreto y el nonce no pasan nunca por `BigInt` ni por una rama. Como Go, `ed25519Sign` mete en el hash la clave pública que recibe, sin derivarla otra vez de la semilla. +- **Las cadenas y las líneas de un fichero** se leen como las lee Go, como bytes: la mayúscula o la minúscula de una clave es la de `strings.ToUpper` y `strings.ToLower` de Go, y los espacios alrededor de una línea los de `strings.TrimSpace`, con las tablas de `go_unicode.dart`, ni las de la plataforma ni las de las rutas (Unicode 18.0.0); un byte que no es UTF-8 es U+FFFD, como en Go. Un string que no es ASCII no llega al Bech32 de la etapa 2, cuya comprobación de «mixed case» usa las mayúsculas de la plataforma: el error de Go se calcula aquí, sobre las runas. Las líneas son las de `bufio.Scanner`: una de 64 KiB o más da `bufio.Scanner: token too long`, sin el prefijo `authorkey:`, como en Go. +- **El fichero de una clave** se escribe con `ScryptRecipient` y `ageEncrypt` de la 6a, con un factor de trabajo de 16, y se lee con el lector de `age` de la etapa 2, con un máximo de 16, como `SetMaxWorkFactor(16)`: un fichero de 64 KiB como mucho, en claro o cifrado. +- **Los errores** son una `AuthorKeyException` con el texto de Go, sin código normativo. Una semilla o una clave pública de otra longitud son un `ArgumentError` con el texto de Go: un error del llamador. El de `PublicString` de Go tiene los dos números al revés, «a public key has 32 bytes, not 31», y aquí también. +- **Una diferencia con Go, a propósito:** tras `clear`, una clave lanza un `StateError` en cada uso; en Go, `Clear` deja a ceros la semilla y la clave pública, y la clave sigue dando sus valores y firmando con ellos, una firma que nunca verifica. +- **El material de la clave.** `clear` borra la semilla y la clave pública de la clave, y las funciones borran sus copias, también el texto de un fichero de clave y el del localizador sellado. Dart no puede prometer más: el recolector puede haber copiado un búfer, `package:crypto` guarda los suyos, y un `String`, como el de `AuthorKey.secret` o una contraseña, no se puede borrar. Nada es de tiempo constante: ni la VM ni un motor de JavaScript lo prometen. +- **El sellado** es el de Go, en su orden: `sealLocator` escribe primero el texto del localizador, con las comprobaciones de `Locator.marshal`, después crea el recipient tlock de la ronda, con las de `TimeRecipient`, y cifra; un fallo de cualquiera de los dos llega antes de sacar ningún valor aleatorio. `newEnvelope` saca primero I_SOBRE y después lo que saca `age`, como `GenerateX25519Identity` y `age.Encrypt` en Go. El cifrado tlock no es de tiempo constante (parte 6a). +- **Con la parte 6b:** `AuthorKey` implementa `AuthorSigner`, el enganche de la firma de `alg` 1 del escritor, que es la interfaz `AuthorKey` del paquete `capsule` de Go, con `Public()` y `Sign(msg)`. Las pruebas del escritor firman las cápsulas de `alg` 1 con la `AuthorKey` de la semilla de Go, y salen los mismos bytes. +- **Lo que se exporta.** `lib/datekeys.dart` exporta `authorkey.dart`, salvo las dos funciones sobre bytes, como el paquete público `authorkey` de Go, y `locator_seal.dart`. La firma Ed25519 y las tablas de Unicode de Go son internas. + 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`; @@ -291,19 +313,16 @@ Los enteros son exactos en la VM y en la web. El `int` de Dart tiene 64 bits con - al escribir `age`, el contador de 11 bytes del nonce del STREAM se incrementa byte a byte, las longitudes son sumas y cocientes por debajo de 2^53, y `randomIndex` lee sus cuatro bytes como mucho multiplicando por 256, sin un desplazamiento que la web truncaría. - en el escritor de cápsulas, el tamaño de cada fichero, su suma, L y P son un `int` hasta L_MAX, por debajo de 2^53, y el escritor rechaza unos ficheros que pasan de L_MAX antes de sumar el siguiente; - en el localizador, una IPv6 son 16 bytes y sus bloques se comparan byte a byte, nunca como un entero de 64 o 128 bits, y los varint de un CID, de hasta 63 bits, son `BigInt`; el tamaño del resto y los desplazamientos son un `int` hasta 2^53-1. +- en el localizador, una IPv6 son 16 bytes y sus bloques se comparan byte a byte, nunca como un entero de 64 o 128 bits, y los varint de un CID, de hasta 63 bits, son `BigInt`; el tamaño del resto y los desplazamientos son un `int` hasta 2^53-1; +- en la firma Ed25519, los escalares módulo ℓ son 64 limbs de 8 bits en un `Float64List`, sumas de productos de bytes por debajo de 2^21 que `modL` reduce con valores por debajo de 2^34, y sus acarreos son divisiones redondeadas hacia abajo, no desplazamientos. -**`BigInt` no es de tiempo constante.** En las primitivas se 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 de las primitivas (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í. +**`BigInt` no es de tiempo constante.** En las primitivas se 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 de las primitivas (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í. La firma Ed25519 de la parte 7b sigue la misma regla: el escalar secreto y el nonce van en limbs, también al reducirlos módulo ℓ, nunca en `BigInt`. ECDSA y RSA hacen con `BigInt` toda su aritmética, y solo verifican: la clave de un certificado, la firma y el mensaje son públicos. La librería no firma con ninguno de los dos. BLS12-381, en cambio, hace con `BigInt` toda su aritmética. Verificar un release y descifrar un stanza tlock solo manejan datos públicos: la firma de la ronda lo es desde que drand la publica, y el stanza va en la cápsula. Cifrar no: sigma y r son secretos, y el tiempo del código depende de ellos. El autor aceptó el 6 de octubre de 2026 que el escritor cifre así el stanza tlock (parte 6a); una capa del cuerpo de limbs fijos, sin ramas que dependan de los datos, lo evitaría. -Las etapas siguientes traen el resto del protocolo en este orden: - -| Etapa | Contenido | -|---|---| -| 6 | Escritor: la parte 6b, la cápsula de formato 3, `time_only` y `time_and_key`, con la llave de palabras y la nota pública | -| 7 | El sellado del localizador y el sobre nuevo (parte 7b), y las claves de autor | +Con la 6b y la 7b están hechas todas las etapas del plan, también la firma de `alg` 1 en el escritor con la clave de autor de la 7b. ## Rendimiento @@ -462,6 +481,33 @@ Lo aleatorio sale del CSPRNG de la plataforma, y el sink solo cuenta lo que reci Una cápsula pequeña cuesta sobre todo el cifrado del stanza tlock, el mismo de la 6a; con `time_and_key`, también los 16 stanzas X25519 y la `.dkk`, que se comprueba abriendo `INNER_ACCESS_AGE`. En un fichero grande manda el SHA-256, que se calcula tres veces, como en Go: el de cada lectura del fichero y el de la cápsula para la `.dkk`, a unos 62 MiB/s cada uno en la VM. Con una llave de palabras se suman los 600 000 pasos de PBKDF2. +### Las claves de autor y el sellado del localizador + +`dart run tool/authorkey_bench.dart` mide la parte 7b en la VM. Compilado a JavaScript: + +```bash +dart compile js -O2 -o authorkey_bench.js tool/authorkey_bench.dart +``` + +```bash +node -e "globalThis.self = globalThis; require('./authorkey_bench.js')" +``` + +Lo aleatorio sale del CSPRNG de la plataforma, como en producción. Son medianas de entre cinco y treinta y una ejecuciones, el 6 de octubre de 2026: + +| Operación | VM | Node.js | +|---|---|---| +| `AuthorKey.generate` | 4,5 ms | 9 ms | +| Una firma Ed25519 de 99 bytes, lo que mide `AUTHOR_MESSAGE` | 4,5 ms | 9 ms | +| La verificación estricta de esa firma, para comparar | 4,6 ms | 9 ms | +| Una firma Ed25519 de 1 MiB | 26 ms | 0,92 s | +| `encryptAuthorKey`, scrypt con logN 16 | 0,57 s | 0,77 s | +| `readAuthorKey` de ese fichero | 0,57 s | 1,0 s | +| `sealLocator` para la ronda 1000 | 30 ms | 0,52 s | +| `newEnvelope` de un `.dkc` de 1 MiB | 56 ms | 42 ms | + +Una firma es una multiplicación escalar del punto base y dos SHA-512; la verificación, dos multiplicaciones con el producto desenrollado, tarda lo mismo. En un mensaje grande manda el SHA-512 de `package:crypto`, lento compilado a JavaScript, donde trabaja con enteros de 64 bits emulados; la firma de `alg` 1 firma siempre los 99 bytes de `AUTHOR_MESSAGE`. El fichero de una clave es casi todo scrypt, y sellar un localizador, el cifrado tlock. + ### ECDSA, RSA y CMS `dart run tool/cms_bench.dart` mide ECDSA, RSA y el lector de CMS en la VM. Compilado a JavaScript: @@ -501,7 +547,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. Hoy la librería usa de `package:crypto` SHA-512, en Ed25519, SHA-256, en BLS12-381 y tlock, y SHA-1, SHA-256, SHA-384 y SHA-512, en CMS. Las primitivas tienen además su propio SHA-256 con HMAC-SHA256, porque PBKDF2 los necesita con los estados de la clave calculados una vez. +- **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` SHA-512, en Ed25519, al verificar y al firmar, SHA-256, en BLS12-381 y tlock, y SHA-1, SHA-256, SHA-384 y SHA-512, en CMS. Las primitivas tienen además su propio SHA-256 con HMAC-SHA256, 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. @@ -540,7 +586,7 @@ 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, 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, de los formatos, de la apertura, del área de seguridad, de la firma con certificados y el sello, del lector de CMS y del localizador. 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 y del escritor de `age`, 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`: +`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, 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 y del escritor de `age`, 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`: ```bash cd ../datekeys-go && go run ../datekeys-dart/tool/gen_primitive_vectors.go -out ../datekeys-dart/test/vectors @@ -638,6 +684,22 @@ cp tool/age_writer_go_vectors.go "$tmp" rm -rf "$tmp" ``` +Los de la parte 7b corren en el módulo de `datekeys-go`, porque solo importan paquetes públicos: `authorkey`, `locator`, `profile`, `provider` y `codec/bech32`, cuyo `createChecksum` el primero alcanza con `go:linkname` para escribir cadenas Bech32 con cualquier relleno. Mientras escriben cada caso, `crypto/rand.Reader` lee el keystream de `SeededRandomSource`, como en el generador del escritor de `age`; `authorkey.Generate` lo lee también, gracias al `//go:debug cryptocustomrand=1` del generador, sin el que `ed25519.GenerateKey` de Go 1.26 no mira `crypto/rand.Reader`: + +```bash +cd ../datekeys-go && go run ../datekeys-dart/tool/authorkey_go_vectors.go -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata -out ../datekeys-dart/test/vectors +``` + +```bash +cd ../datekeys-go && go run ../datekeys-dart/tool/locator_seal_go_vectors.go -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata -out ../datekeys-dart/test/vectors +``` + +Las tablas de Unicode de Go de `lib/src/go_unicode.dart` las escribe otro programa, con la versión de Go de `datekeys-go`: + +```bash +cd ../datekeys-go && go run ../datekeys-dart/tool/go_unicode_tables.go -out ../datekeys-dart/lib/src/go_unicode.dart +``` + En la otra dirección, este repositorio escribe los ficheros y Go los abre: `tool/age_interop_dart_samples.dart` escribe los de las recetas de `test/age_interop_support.dart`, y `tool/age_interop_go_verdicts.go` los abre con `age` y las identities de `agewrap` y escribe sus veredictos: ```bash @@ -651,6 +713,15 @@ cp tool/age_interop_go_verdicts.go "$tmp" rm -rf "$tmp" ``` +Los de la parte 7b, igual: `tool/seal_interop_dart_samples.dart` escribe los ficheros de las recetas de `test/seal_interop_support.dart`, y `tool/seal_interop_go_verdicts.go` los abre en el módulo de `datekeys-go`: + +```bash +tmp=$(mktemp -d) +dart run tool/seal_interop_dart_samples.dart "$tmp" +(cd ../datekeys-go && go run ../datekeys-dart/tool/seal_interop_go_verdicts.go -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata -samples "$tmp" -out ../datekeys-dart/test/vectors) +rm -rf "$tmp" +``` + Los del escritor de cápsulas corren como una prueba de Go en una exportación, como los del lector de CMS, porque los enganches de la firma y del sello usan `internal/cms/cmstest`, cuyas claves y firmas solo fija `testing/cryptotest.SetGlobalRandom`. Mientras escribe cada receta, `crypto/rand.Reader` lee el keystream de `SeededRandomSource`, así que `capsule.EncryptFiles` saca de él todos sus valores, y la salida es la misma en cada ejecución: ```bash @@ -723,9 +794,15 @@ rm -rf "$tmp" | `capsule_writer.g.dart` | Los casos de `capsule_writer.json` marcados `node`, como constante de Dart | | `capsule_interop.json` | Las cápsulas que escribe este repositorio con las recetas de `test/capsule_interop_support.dart`, seis con `SeededRandomSource` y seis con el CSPRNG de la plataforma: su longitud y su SHA-256, `capsule.Inspect`, `capsule.Open` con cada credencial, todas juntas y ninguna, si sus capas y su `.dkk` se codifican otra vez igual, y si `capsule.EncryptFiles` escribe los mismos bytes con la misma semilla | | `capsule_interop.g.dart` | Las muestras de `capsule_interop.json` que no están marcadas `node` a falso, como constante de Dart | +| `authorkey.json` | Las firmas de `crypto/ed25519`: líneas de `sign.input`, cuyas 0, 1, 2 y 1023 son los tests 1, 2, 3 y 1024 de la RFC 8032, TEST SHA(abc), semillas de una semilla con mensajes de 0 bytes a 1 MiB, y claves privadas cuya segunda mitad es otra clave pública; los escalares de `math/big`, x mod ℓ y (a·b + c) mod ℓ, en sus esquinas y al azar; y `authorkey`: `NewFromSeed`, `Public`, `PublicString`, `Secret`, `Marshal`, `String` y sus errores, `Generate` y `Encrypt` con cada valor que sacan, `ParsePublic` y `ParseSecret` sobre cadenas válidas, en otra caja, de otras longitudes, con cada error de Bech32, otros prefijos y rellenos, claves fuera del perfil estricto y bytes que no son UTF-8, y sobre las runas de cada borde de los conjuntos de Go; y `Read` de ficheros en claro y cifrados, con sus límites, cada espacio de Go y sus vecinos, y factores de trabajo de 1 a 31. Cada caso con el resultado o el texto de Go | +| `authorkey.g.dart` | `authorkey.json` con solo las firmas marcadas para Node.js y una de cada ocho runas, como constante de Dart | +| `locator_seal.json` | `Seal` de localizadores de uno a tres bloques para rondas de 1 a la última de Quicknet, que Go abre con el release de su ronda cuando lo tiene; sus rechazos; `NewEnvelope` de `.dkc` de 0 bytes a 1 MiB; y el camino entero, del sobre a la apertura del `.dkc`. Con lo que saca `crypto/rand` en cada caso | +| `locator_seal.g.dart` | El mismo JSON como constante de Dart | +| `seal_interop.json` | Lo que hace Go con lo que escribe este repositorio en la parte 7b, con las recetas de `test/seal_interop_support.dart`: localizadores sellados de cuatro rondas con sus sobres, que abre con `locator.Open` y `OpenEnvelope`; ficheros de clave, en claro y cifrados, que lee con `authorkey.Read`; y firmas de 0 bytes a 70 000, que comprueba con `crypto/ed25519`. Con el SHA-256 de cada fichero y el veredicto de Go | +| `seal_interop.g.dart` | El mismo JSON como constante de Dart | - Los fixtures que leen los generadores son los de `testdata/` de este repositorio, la copia sincronizada. -- `primitives.json`, los cuatro ficheros de BLS12-381 y tlock, `pathrule_vectors.json`, `wordkey_vectors.json`, los de los formatos, `security_vectors.json`, `securitycms_vectors.json`, los del lector de CMS, los del escritor de `age`, los del localizador y `capsule_writer.json` salen iguales en cada ejecución, los del lector de CMS, los del localizador y `capsule_writer.json` con Go 1.26.8. `capsule_interop.json` cambia en cada ejecución en sus seis muestras del CSPRNG, que es lo que comprueban. Lo aleatorio de tlock, los ciphertexts de kyber con su sigma y los de `datekeys-ts`, se lee de los ficheros congelados de `datekeys-ts` en `289fe71`, y Go los descifra otra vez. `age`, en cambio, 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. `age.json` no lee `testdata/` y es el congelado de la etapa 2; `age_fixtures.json` se escribió otra vez con el `testdata/` de la v0.12, y fuera de los dos fixtures nuevos y del de `seal_type` 4294967295 sale igual. +- `primitives.json`, los cuatro ficheros de BLS12-381 y tlock, `pathrule_vectors.json`, `wordkey_vectors.json`, los de los formatos, `security_vectors.json`, `securitycms_vectors.json`, los del lector de CMS, los del escritor de `age`, los del localizador, los de la parte 7b y `capsule_writer.json` salen iguales en cada ejecución, los del lector de CMS, los del localizador y `capsule_writer.json` con Go 1.26.8. `capsule_interop.json` cambia en cada ejecución en sus seis muestras del CSPRNG, que es lo que comprueban. Lo aleatorio de tlock, los ciphertexts de kyber con su sigma y los de `datekeys-ts`, se lee de los ficheros congelados de `datekeys-ts` en `289fe71`, y Go los descifra otra vez. `age`, en cambio, 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. `age.json` no lee `testdata/` y es el congelado de la etapa 2; `age_fixtures.json` se escribió otra vez con el `testdata/` de la v0.12, y fuera de los dos fixtures nuevos y del de `seal_type` 4294967295 sale igual. - 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. - `locator_vectors.json` guarda un valor editado como ediciones de una base, con una forma más: `[at, 0, byte, n]` inserta n copias del byte. Una dirección con una racha de 32 bytes iguales o más va como `[antes, byte, n, después]`, y un texto de error, como su índice en `texts`. Los ficheros cuyo texto pasa de 1 MiB van como su cabecera, su nonce, su file key y la lista de sus chunks: la prueba los escribe otra vez y comprueba su SHA-256 antes de abrirlos. - Los casos de `open_cases.json` que editan un fixture lo sellan otra vez como el `testkit` de la referencia, con las file keys y los nonces del fixture, así que salen iguales en cada ejecución, y se guardan como ediciones del fixture. diff --git a/lib/datekeys.dart b/lib/datekeys.dart index 5ac1bde..98b1b8b 100644 --- a/lib/datekeys.dart +++ b/lib/datekeys.dart @@ -28,11 +28,13 @@ /// Go, and what it points to, the locator sealed for the date and the /// envelope of the .dkc: reading and opening a locator, the rules of its /// addresses and of the IP that a name resolves to, and the rest of the -/// envelope. Sealing a locator and making an envelope come with part 7b. +/// envelope. And part 7b: sealing a locator and making an envelope, and the +/// author keys of alg 1, dkauthor1… and DKAUTHOR-SECRET-KEY-1…, with their +/// Ed25519 signature and their key file encrypted with scrypt. /// /// And part 6b of stage 6: the writer of capsules of format 3, /// [encryptFiles], with its options, its sources read in streaming, its -/// result and the hooks of the signature and the seal, [AuthorKey], +/// result and the hooks of the signature and the seal, [AuthorSigner], /// [CmsSigner] and [Sealer]; and what its options take: the X25519 /// recipients and the source of the random values. /// @@ -40,12 +42,14 @@ /// and the random sources, agewrap, the curve arithmetic, the IBE of tlock /// and its stanza, the rules of paths and texts on the Unicode tables, the /// frame of BODY, the digest of a capsule, the steps of the opening, the -/// reader of CMS with its ECDSA and RSA, and the parser of IP addresses are -/// internal, as in the Go reference and datekeys-ts. +/// reader of CMS with its ECDSA and RSA, the parser of IP addresses, the +/// Ed25519 signing and the Unicode sets of Go are internal, as in the Go +/// reference and datekeys-ts. library; export 'src/accesskey.dart'; export 'src/author.dart'; +export 'src/authorkey.dart' hide parseAuthorPublicUtf8, parseAuthorSecretUtf8; export 'src/bls12381_curve.dart' show BlsGroup, PointVerdict, checkCompressedPoint; export 'src/bytes.dart' @@ -71,6 +75,7 @@ export 'src/inspect.dart' inspectedLength, maxAccessKeyRead; export 'src/locator.dart'; +export 'src/locator_seal.dart'; export 'src/note.dart'; export 'src/open.dart' show OpenOptions, Opened, openCapsule, openCapsuleSource; export 'src/padding.dart'; diff --git a/lib/src/authorkey.dart b/lib/src/authorkey.dart index 34b8927..b602b1d 100644 --- a/lib/src/authorkey.dart +++ b/lib/src/authorkey.dart @@ -35,6 +35,7 @@ import 'bech32.dart'; import 'bytes.dart'; import 'curve25519.dart' show canonical, onCurve, smallOrder; import 'ed25519_sign.dart'; +import 'encrypt3.dart' show AuthorSigner; import 'go_unicode.dart'; import 'random.dart'; import 'recipient.dart'; @@ -75,12 +76,12 @@ final class AuthorKeyException implements Exception { String toString() => message; } -/// A secret key of an author: Go's authorkey.Key. Its [publicKey] and -/// [sign] are what the writer of a capsule needs to sign with `alg` 1. +/// A secret key of an author: Go's authorkey.Key. It is the [AuthorSigner] +/// that the writer of a capsule needs to sign with `alg` 1. /// /// Unlike Go, a cleared key refuses to be used: Go gives the values of a /// key of zeros after Clear, and a signature that never verifies. -final class AuthorKey { +final class AuthorKey implements AuthorSigner { AuthorKey._(this._seed, this._public); /// A new key from [random], the CSPRNG of the platform by default, as @@ -119,6 +120,7 @@ final class AuthorKey { } /// The public key A, 32 bytes, as Public of Go. + @override Uint8List get publicKey { _check(); return Uint8List.fromList(_public); @@ -128,6 +130,7 @@ final class AuthorKey { String get publicString => authorPublicString(publicKey); /// The Ed25519 signature of [message], 64 bytes, as Sign of Go. + @override Uint8List sign(List message) { _check(); return ed25519Sign(_seed, _public, message); diff --git a/lib/src/encrypt3.dart b/lib/src/encrypt3.dart index 90c21bd..31ab9cd 100644 --- a/lib/src/encrypt3.dart +++ b/lib/src/encrypt3.dart @@ -39,6 +39,7 @@ import 'age.dart'; import 'age_writer.dart'; import 'agewrap.dart'; import 'author.dart'; +import 'authorkey.dart' show AuthorKey; import 'body.dart'; import 'bytes.dart'; import 'control.dart'; @@ -66,10 +67,11 @@ import 'wordkey.dart'; // The hooks /// The author key that signs a capsule with alg 1, a strict Ed25519 -/// signature (spec §29.9), as AuthorKey of Go: [encryptFiles] signs -/// AUTHOR_MESSAGE with it and checks the signature with the strict profile -/// before it writes anything. -abstract interface class AuthorKey { +/// signature (spec §29.9), as the interface AuthorKey of package capsule of +/// Go: [encryptFiles] signs AUTHOR_MESSAGE with it and checks the signature +/// with the strict profile before it writes anything. [AuthorKey], the key +/// of package authorkey, is one. +abstract interface class AuthorSigner { /// The public key A, 32 bytes. Uint8List get publicKey; @@ -235,7 +237,7 @@ final class EncryptOptions { /// The key that signs the capsule with alg 1 (spec §29.9); null for no /// signature. Exclusive with [cmsSigner]. - final AuthorKey? authorKey; + final AuthorSigner? authorKey; /// The signer of alg 2 (spec §29.10); null for none. Exclusive with /// [authorKey] and [sealer]: with it the seal goes inside each signature. diff --git a/lib/src/locator.dart b/lib/src/locator.dart index b28ece9..335e367 100644 --- a/lib/src/locator.dart +++ b/lib/src/locator.dart @@ -21,9 +21,10 @@ /// bytes of the rest, and gives them to [Locator.openEnvelope], which checks /// their SHA-256, decrypts the .dkc and checks its SHA-256 (spec §44.1). /// -/// Sealing a locator, Seal of Go, and making an envelope, the age encryption -/// of NewEnvelope, need the writer of age: [splitEnvelope] is the rest of -/// NewEnvelope, the split of the age file of the envelope. +/// Sealing a locator, Seal of Go, and making an envelope, NewEnvelope, are +/// in locator_seal.dart, over the writer of age: [splitEnvelope] is the +/// part of NewEnvelope after the encryption, the split of the age file of +/// the envelope. /// /// The errors of the locator carry no normative code, as in Go: a locator /// that does not read or does not open, or an address that breaks the rules @@ -1189,10 +1190,10 @@ Uint8List _readLimited(Uint8List file, AgeOpened opened) { /// and the length of the rest and the SHA-256 of the .dkc, without /// addresses; and the rest, the nonce and the STREAM, with no mark, which is /// what the person keeps outside. It is the part of NewEnvelope of Go after -/// the encryption, which needs the writer of age; [ageFile] is not -/// decrypted. A caller adds the addresses where it stored the rest -/// ([Locator.withAddresses]), alone or inside another file ([hideRest]), -/// and then seals the locator. +/// the encryption, which newEnvelope of locator_seal.dart makes first; +/// [ageFile] is not decrypted. A caller adds the addresses where it stored +/// the rest ([Locator.withAddresses]), alone or inside another file +/// ([hideRest]), and then seals the locator (sealLocator). /// /// The header ends at the line feed after the first line that starts with /// `--- `: no line of the header before it starts so, and the lines of the diff --git a/test/capsule_writer_support.dart b/test/capsule_writer_support.dart index 0acf739..0a87860 100644 --- a/test/capsule_writer_support.dart +++ b/test/capsule_writer_support.dart @@ -166,15 +166,45 @@ final class Hooks { final Json recorded; final Json asked = {}; - AuthorKey? get authorKey => - recipe['author_key'] == null ? null : _ReplayAuthor(this); + AuthorSigner? get authorKey => switch (recipe['author_key']) { + null => null, + // A key that Go broke on purpose: what Go's gave. + {'bad': _} => _ReplayAuthor(this), + // The key of the seed, signing as Go's did: Ed25519 is deterministic. + final k => _RecordingAuthor( + this, + AuthorKey.fromSeed(hexOf((k as Json)['seed'])), + ), + }; CmsSigner? get cmsSigner => recipe['cms'] == null ? null : _ReplayCms(this); Sealer? get sealer => recipe['sealer'] == null ? null : _ReplaySealer(this); } -final class _ReplayAuthor implements AuthorKey { +final class _RecordingAuthor implements AuthorSigner { + _RecordingAuthor(this.h, this.key); + + final Hooks h; + final AuthorKey key; + + @override + Uint8List get publicKey { + final pub = key.publicKey; + h.asked['author_public'] = toHex(pub); + return pub; + } + + @override + Uint8List sign(Uint8List message) { + h.asked['author_message'] = toHex(message); + final sig = key.sign(message); + h.asked['author_signature'] = toHex(sig); + return sig; + } +} + +final class _ReplayAuthor implements AuthorSigner { _ReplayAuthor(this.h); final Hooks h; diff --git a/test/encrypt3_test.dart b/test/encrypt3_test.dart index f1448ba..39f9ed6 100644 --- a/test/encrypt3_test.dart +++ b/test/encrypt3_test.dart @@ -23,7 +23,7 @@ EncryptOptions small({ RandomSource? random, AccessPolicy policy = AccessPolicy.timeOnly, bool portable = false, - AuthorKey? authorKey, + AuthorSigner? authorKey, CmsSigner? cmsSigner, Sealer? sealer, String comment = '', @@ -69,7 +69,7 @@ final class FailingSink implements ByteSink { /// The author key of the case of alg 1, which records whether anything of /// the capsule had reached the sink when it was asked to sign. -final class WatchingKey implements AuthorKey { +final class WatchingKey implements AuthorSigner { WatchingKey(this.hooks, this.sink); final Json hooks;