|
|
2 weeks ago | |
|---|---|---|
| .claude | 2 weeks ago | |
| docs | 2 weeks ago | |
| scripts | 2 weeks ago | |
| src | 2 weeks ago | |
| static | 2 weeks ago | |
| testdata | 2 weeks ago | |
| .gitattributes | 2 weeks ago | |
| .gitignore | 2 weeks ago | |
| .npmrc | 2 weeks ago | |
| LICENSE | 2 weeks ago | |
| README.md | 2 weeks ago | |
| package-lock.json | 2 weeks ago | |
| package.json | 2 weeks ago | |
| svelte.config.js | 2 weeks ago | |
| tsconfig.json | 2 weeks ago | |
| tsconfig.lib.json | 2 weeks ago | |
| vite.config.ts | 2 weeks ago | |
| vitest.config.ts | 2 weeks ago | |
README.md
DateKeys App
Implementación en TypeScript del protocolo DateKeys v0.8.2 y página de prueba en el navegador. Sustituye al prototipo, archivado en ../AppOld (API Quicknet en Go, CLI tlock y cliente Svelte, commit 4d2b0a1).
La implementación de referencia es la librería Go g.activething.com/go/DateKeys, en ../datekeys-go. El plan de trabajo está en docs/PLAN_codec_cbor_y_pagina_svelte.md.
Contenido
| Parte | Ubicación | Paso del plan | Estado |
|---|---|---|---|
Codec CBOR del subconjunto, parsers de schema, DateKey, inspect |
src/lib/dkc/ |
4 | hecho |
| Página inspector, sin red | SvelteKit estático: src/routes/, src/lib/inspector/, src/lib/components/ |
5 | hecho |
| Cifrado y descifrado en el navegador | fase 2 | 6 | pendiente |
src/lib/dkc
Sin dependencias de ejecución. Funciona en navegadores y en Node 20+: solo usa Uint8Array, DataView, TextEncoder/TextDecoder, BigInt y crypto.subtle (SHA-256).
crypto.subtle solo existe en contextos seguros: https, o http en localhost. La página del paso 5 servida por http desde una IP de la red local (por ejemplo vite --host para probar en un móvil) no lo tiene, y inspect rechaza entonces con un Error que lo dice (SHA-256 needs Web Crypto (crypto.subtle), …) en vez de dar un veredicto. El registro por defecto no memoriza ese fallo: la siguiente llamada lo vuelve a intentar.
| Fichero | Contenido | Equivale en Go (692cf87) |
|---|---|---|
errors.ts |
DateKeysError con el código normativo de §69 (ERR_*); mensajes con la forma contexto: CÓDIGO de Go |
errors.go |
bytes.ts |
Hex, UTF-8 estricto, goQuote (el %q de Go, con la tabla de strconv.IsPrint de Go 1.26 fijada en el código) y sha256 (Web Crypto) |
strconv, unicode/utf8 |
cbor.ts |
El codec propio de la referencia, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error: Encoder con error persistente; Decoder, cursor estricto (map/key/endMap, array, uint/uint64, bstr, text, done); unmarshal (decodifica, reencodifica y compara; onReject para borrar secretos); peek/checkSchema (capa 2 de §69.1: tipo y versión antes que nada) y walk (lector genérico acotado en profundidad y longitud) |
codec |
schema.ts |
Lo que comparten los decodificadores de PUBLIC_HEADER, CONTROL_CBOR y el cuerpo de la .dkk: key N: en los errores, claves obligatorias y arrays de extensiones |
capsule/framing.go, accesskey |
extension.ts |
Arrays de extensiones, leídos con su objeto (capa 3 de §69.1): de 1 a 64, en orden estrictamente ascendente de los bytes UTF-8 de extension_id (nunca por unidades UTF-16), extension_version hasta 2³² − 1, data ausente o bstr no vacío, ningún id en los dos arrays; registros, críticas y no críticas (capa 4) |
extension |
profile.ts |
Provider Profile: CBOR exacto, profile_hash, reglas 1 a 4 de §12.1 en su orden (el límite de period de §74 en la capa del esquema; alfabetos, clave pública del grupo del scheme y fórmula de chain_hash), registro pinneado; Quicknet fijado por su CBOR y su hash |
profile |
bls12381.ts |
Pertenencia de claves públicas BLS12-381 comprimidas (G1 y G2) al subgrupo, como FromCompressed de kilic |
kyber-bls12381 |
datekey.ts |
dk1_ canónico con las reglas de lectura de §19 (CR, LF y todo carácter fuera del alfabeto fallan el paso 1; números JSON por su valor decimal exacto), ronda desde una fecha con precisión de nanosegundos y cota de 9999-12-31T23:59:59Z (§15), parser RFC 3339 equivalente a time.Parse(time.RFC3339Nano, …) |
datekey |
header.ts, control.ts, accesskey.ts |
PUBLIC_HEADER, CONTROL_CBOR y .dkk (cuerpo y trama), decodificar y codificar, con las capas de §69.1 |
capsule, accesskey |
framing.ts |
Prelude DKC1 (16 bytes) y DKK1 (12 bytes) en el orden de §23 y §40, longitudes de 1 byte hasta los límites de §57, y troceo de secciones | capsule/framing.go |
age.ts |
Parser estricto de la cabecera age v1 (§28.1) sobre los ficheros binarios, con los textos de error de age; reglas de stanzas; MAX_AGE_HEADER_LEN (2 MiB), el límite que usa la página para leer solo el prefijo de un .dkc grande |
agewrap, filippo.io/age/internal/format |
inspect.ts |
Pasos 1 a 8 de §63 y la vista JSON de datekeys inspect -json (inspectView, inspectJSON) |
capsule/inspect.go, internal/inspectview |
index.ts |
Reexporta todo | |
testing/ |
Solo para tests: lectura de testdata/ y de sus formatos (vectors.ts: ediciones, vectores), constructores de CBOR en hex, cirugía de cápsulas |
Los tests (*.test.ts) están junto a cada fichero.
Equivalencia con la referencia Go
El comportamiento se contrastó con la librería Go en 3820066 (692cf87 solo cambia la regla de UTF-8 de dk1_ descrita abajo) mediante un oráculo diferencial fuera del repositorio: 483 527 entradas de tres semillas. Son mutaciones de los cinco .dkc oficiales de todas las clases (bits, bytes, truncados, inserciones, borrados, longitudes y campos del prelude, cabeceras reescritas con cambios de CBOR, DateKeys, arrays de extensiones con y sin registro de extensiones, cabeceras age de SEALED_CONTROL y de PAYLOAD_AGE, argumentos del stanza tlock, rondas, secciones cambiadas de sitio y combinaciones de varios defectos); CBOR de cada esquema (PUBLIC_HEADER, CONTROL_CBOR, cuerpo y fichero .dkk, Provider Profile, arrays de extensiones); walk, peek y checkSchema; cabeceras age, preludes, cadenas dk1_, rondas y fechas. En todas coinciden el veredicto, el código y el paso, y también el texto del error, el valor decodificado y la salida entera de datekeys inspect -json, byte a byte.
Precedencia de errores (§69.1): primero la trama; después el tipo y la versión de esquema (checkSchema); después el perfil CBOR y el CDDL, con los límites de implementación de §74, en una sola decodificación (unmarshal con el decodificador del esquema, que ya comprueba tipos, tamaños, rangos, access_policy, extension_version, el máximo de 64 extensiones y su orden); y solo entonces los campos con código propio, en orden de clave: DateKey, perfil fijado y extensiones críticas en PUBLIC_HEADER; access_type y access_material en la .dkk; los campos y la autocomprobación de chain_hash en el Provider Profile. Entre pasos decide el orden de §63. Un PUBLIC_HEADER que rompe a la vez el CDDL y la DateKey da ERR_NON_CANONICAL_CBOR, como en la referencia de 3820066; la de afb44a3 leía access_policy y extension_version como uint64 y los acotaba después de la DateKey, y la vía wideUint que lo imitaba ya no existe.
Enteros: el decodificador lee cada entero hasta 2⁶⁴ − 1 (un bigint por encima de 2⁵³ − 1) y lo compara con el máximo de su campo, con el texto de la referencia (unsigned integer 9007199254740992 above 9007199254740991). Ningún campo de un objeto del protocolo admite más de 2⁵³ − 1; walk admite cualquier uint64, como codec.Walk.
peek lee la capa 2 de §69.1 como codec.Peek: una cabecera de mapa de longitud definida que anuncia al menos dos entradas y no más de la mitad de los bytes que la siguen, la clave 0 con un texto de hasta 64 bytes y la clave 1 con un entero de hasta 2⁵³ − 1, todo en su forma más corta. No lee nada más: una versión 2 es ERR_UNSUPPORTED_VERSION sea lo que sea lo que venga detrás, y cualquier otra cosa en esas posiciones, ERR_NON_CANONICAL_CBOR.
UTF-8 inválido en el JSON de un dk1_ hace fallar el paso 2 de §19 (ERR_DATEKEY_INVALID) en las dos implementaciones, también en un miembro que un nombre repetido reemplaza después. Hasta 692cf87 la referencia Go lo sustituía por U+FFFD (encoding/json) y en ese caso daba ERR_DATEKEY_NON_CANONICAL; el diferencial de esta implementación lo encontró y la referencia se corrigió para seguir al spec. datekey.test.ts y el vector invalid UTF-8 in a member a repeated name overwrites de dk1.json lo fijan.
goQuote escribe como Go 1.26 (Unicode 15.0.0) los textos entre comillas de los detalles de fallo: usa una tabla de strconv.IsPrint generada con Go y no \p{…}, porque cada motor JavaScript trae su propia versión de Unicode (V8 en Node 24 ya imprime runas de Unicode 16 que Go escapa, como U+31E4). bytes.test.ts fija el SHA-256 del conjunto completo de runas imprimibles. Si el toolchain de la referencia cambia de versión de Unicode, se regenera con go run scripts/go-isprint-table.go y se actualizan la tabla y el hash.
Secretos: access_material de un .dkk e I_PAYLOAD de CONTROL_CBOR se borran en todos los caminos, también cuando la decodificación falla a medias o unmarshal rechaza el valor, como el clear diferido de Go.
Página inspector
Sitio SvelteKit estático (@sveltejs/adapter-static, strict): las dos páginas se prerenderizan a HTML y no hay código de servidor.
| Ruta | Contenido |
|---|---|
/ |
Qué es el inspector y qué garantiza; enlaza con /inspect |
/inspect |
El inspector |
/inspect carga un .dkc con el selector de ficheros, soltándolo en cualquier parte de la página o desde la lista de fixtures oficiales, y ejecuta inspect (pasos 1 a 8 de §63) sin registro de extensiones, como datekeys inspect. Muestra:
- cada paso con su número, su nombre de la CLI,
superadoo el código normativo, y el detalle (los caracteres invisibles o de control se escriben como\uXXXX); - el veredicto,
capsule_id, la DateKey compacta y decodificada (red y ronda), el perfil fijado, la fecha de apertura en UTC y en la hora local del navegador,access_policy, los campos del prelude, los argumentos del stanzatlockfrente al perfil fijado y el número y tipo de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE; - las extensiones de PUBLIC_HEADER según el contrato del plan §8: id (entre comillas y escapado si tiene caracteres no imprimibles), versión, crítica o no, conocida o no, longitud y hex (plegado si pasa de 64 bytes); texto si los bytes son UTF-8 imprimible; vista CBOR con
walksi son un ítem del perfil de §58, marcada "informativo, no validado por el protocolo"; y el aviso de que son públicas, de que nada las vincula al resto de la cápsula antes del paso 15 y de que ni entonces prueban autoría (§55.1); - Copiar JSON, que copia exactamente la salida de
datekeys inspect -json(cliJSON: eljson.Encoderde Go con sangría de dos espacios,<,>,&, U+2028 y U+2029 escapados y salto de línea final).filees el nombre del fichero.
No pide ni acepta secretos. Todo el texto leído de la cápsula pasa por interpolación de texto de Svelte (nunca {@html}), y ni las entradas de mapas CBOR ni los identificadores de extensión se usan nunca como claves de objetos o Map de JavaScript.
| Fichero | Contenido |
|---|---|
src/lib/inspector/report.ts |
buildReport: el modelo de la página a partir de Inspection, sin DOM ni reloj |
src/lib/inspector/format.ts |
Nombres y glosas de pasos, códigos y políticas; texto imprimible y escapado; números y fechas en español; cliJSON |
src/lib/inspector/diagnostic.ts |
Notación de diagnóstico CBOR (RFC 8949 §8) de walk, acotada a 16 384 caracteres |
src/lib/inspector/load.ts |
Lectura por prefijo: de un .dkc grande solo se leen 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN + 2 MiB + 1 bytes, y solo 16 si los pasos 1 y 2 rechazan el prelude (otro tipo de fichero, un .dkk, longitudes fuera de §57), siempre con el mismo resultado que el fichero entero (lo comprueba load.test.ts) |
src/lib/inspector/fixtures.ts |
Los fixtures oficiales, empaquetados desde testdata/fixtures |
src/lib/components/ |
InspectionReport, StepList, ExtensionList, DataView, Mark |
src/routes/ |
Layout, portada e inspector |
Fixtures
fixtures.ts importa con import.meta.glob los .dkc de testdata/fixtures como URL (?url) y, de cada registro JSON, solo el campo description. testdata/ sigue siendo la única fuente: Vite copia cada .dkc como fichero con hash en _app/immutable/assets/ y nunca lo incrusta como data: (assetsInlineLimit: 0), y no se copia nada más. Los .dkk, los textos en claro y los demás campos de los registros (payload_identity, control_cbor…) no llegan al sitio; check-build.mjs lo comprueba. En desarrollo, server.fs.allow deja que Vite sirva testdata/fixtures.
Sin red: la Content-Security-Policy
kit.csp (svelte.config.js, modo hash) pone en cada página prerenderizada, como primer elemento que carga algo, un <meta http-equiv="content-security-policy">:
default-src 'self'; frame-src 'none'; worker-src 'none'; connect-src 'self'; font-src 'self';
img-src 'self'; manifest-src 'self'; object-src 'none'; script-src 'self' 'sha256-…';
style-src 'self'; style-src-attr 'unsafe-hashes' 'sha256-…'; base-uri 'none'; form-action 'none'
connect-src 'self':fetchsolo llega al propio origen, y solo se usa para los fixtures.script-src: los módulos del sitio y el hash SHA-256 del único script en línea, el arranque de SvelteKit (los nonces no sirven en HTML prerenderizado).style-src 'self': solo hojas de estilo del sitio; sin fuentes web ni CDN, con las fuentes del sistema.style-src-attr: solo el atributostyledel anunciador de rutas de SvelteKit, por su hash (ANNOUNCER_STYLE_HASH, válido para@sveltejs/kit2.70.3;app.csslo oculta también si el navegador bloquea el atributo).
npm run build ejecuta después scripts/check-build.mjs (postbuild; también npm run build:check), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en script-src o sobra un hash; si style-src-attr no coincide con los atributos style del bundle; si hay estilos en línea, manejadores de eventos en atributos, @import o URL a otro origen; si algún .dkc oficial no está byte a byte; o si aparece en el sitio algún secreto de los fixtures (.dkk, textos en claro, identidades, payload_identity, access_material, control_cbor).
En un hosting estático basta con servir build/. Las directivas que solo funcionan como cabecera HTTP (frame-ancestors, sandbox, report-to) quedan para el servidor que la aloje. crypto.subtle exige contexto seguro: https, o http en localhost.
Reglas
- Los fixtures y vectores del Go son la verdad. Este proyecto nunca genera fixtures propios:
testdata/es una copia exacta de un commit de la librería Go. - Dependencias de ejecución: solo
age,drand,tlocky lo que ellas arrastran. Ahora mismo no hay ninguna:package.jsonsolo tienedevDependencies. El sitio lleva compilado el runtime de cliente de Svelte y SvelteKit, el tooling que el plan elige para la página (sección 13). - Tooling de desarrollo: solo el de la lista siguiente. Cualquier otra dependencia se propone por escrito y no se instala sin aprobación.
Comandos
npm test # vitest, todos los tests
npm run coverage # tests con cobertura v8; falla por debajo de los umbrales
npm run typecheck # svelte-kit sync y tsc sobre todo y sobre la librería sin tipos de Node
npm run check # svelte-kit sync y svelte-check (componentes y rutas), falla con avisos
npm run dev # servidor de desarrollo: http://localhost:5173/inspect
npm run build # sitio estático en build/ y, después, scripts/check-build.mjs
npm run preview # sirve build/: http://localhost:4173/inspect
npm run build:check # solo la comprobación del sitio ya construido
npm run verify # check, typecheck, coverage y build (con su comprobación)
Umbrales de cobertura (vitest.config.ts): cbor.ts al 100 % en líneas, ramas, funciones y sentencias; el conjunto de src/lib/dkc al 95/90/95/95, y el de src/lib/inspector también.
vitest.config.ts es la configuración de los tests; vite.config.ts, la del sitio con el plugin de SvelteKit. Vitest prefiere la primera, así que los tests de src/lib corren sin SvelteKit, y src/lib/inspector importa la librería por rutas relativas, sin el alias $lib. tsconfig.json extiende el que genera svelte-kit sync (por eso typecheck y check lo ejecutan antes, y npm install también, con prepare).
npm run typecheck pasa dos veces: tsconfig.json (todo, con tipos de Node para los tests) y tsconfig.lib.json (solo la librería y sin tipos de Node, para que no se cuele ninguna API que no exista en el navegador).
Vectores y fixtures
testdata/fixtures/*.json: cada.dkcpasa los pasos 1 a 8 con los valores registrados (prelude, PUBLIC_HEADER, DateKey,capsule_id, política,unlock_at, extensiones con sus bytes exactos, stanzas,header_binding, CONTROL_CBOR); cada.dkkse decodifica campo a campo y se reencodifica byte a byte. Los fixtures nuevos se recogen solos.testdata/fixtures/*.inspect.json: la vista deinspectde cada.dkc, escrita coninspectJSONcomo la imprime la CLI (fileincluido), es idéntica byte a byte al fichero, también el texto de cada paso.testdata/vectors/dk1.json(con los tres vectores de los refinamientos de §19: LF dentro del Base64, CR y LF después, y la versión1.0000000000000001),quicknet_rounds.jsonyprofile_quicknet.json: se ejecutan todos.testdata/vectors/cbor.json: cada vector genérico (acceptyreject) pasa porwalkcon losmax_depthymax_lendel fichero; los enteros aceptados comparan suvalue(número o, por encima de 2⁵³ − 1,bigint), y los rechazados «above max_len» o «above max_depth» se aceptan sin ese límite. Cada vector deschemaspasa por el decodificador de su esquema (decodeProfile,decodeHeader,decodeControl,decodeAccessKeyBody) con el código exacto, y un objeto aceptado se reescribe a los mismos bytes.testdata/vectors/mutations.json: se leen los 55 casos enteros (ediciones sobre un fixture o hex congelado, release, reloj, registro, extensiones,.dkke identidades). Los 31 de los pasos 1 a 8 pasan porinspectcon su registro y sus extensiones, y dan el mismo código y el mismo paso; los 24 de los pasos 9 a 18 necesitanopen(fase 2) y se saltan uno a uno con ese motivo, y un test fija los dos recuentos. Las.dkkofrecidas se decodifican.testdata/vectors/inspect_differential.json: las 1 825 mutaciones dan el mismo veredicto, código y paso que Go; losbasesse comprueban por su SHA-256.- Todo se lee con los formatos de
testdata/README.md(testing/vectors.ts): una clave desconocida o que falta, un valor de otro tipo, un código que no es de §69 o una edición fuera de su base hacen fallar el fichero con su motivo; nada se salta en silencio. - Todo fichero de
testdata/tiene que ejecutarlo algún test: un nombre nuevo exportado por Go (otrovectors/*.json, un fichero de fixture que ningún JSON nombra) hace fallartestdata/ holds no file that no test runshasta que se le añade su bloque.
Tooling de desarrollo
| Paquete | Versión | Estado |
|---|---|---|
typescript |
5.9.3 | en package.json; ya usado en el prototipo |
vitest |
5.0.1 | en package.json; ya usado en el prototipo |
@vitest/coverage-v8 |
5.0.1 | en package.json; cobertura del 100 % del codec |
@types/node |
24.13.6 | en package.json; tests que leen testdata/ desde disco |
vite |
8.3.0 | en package.json; dependencia peer obligatoria de vitest 5.0.1 y base del paso 5; la versión del prototipo |
svelte |
5.57.1 | en package.json; paso 5 |
@sveltejs/kit |
2.70.3 | en package.json; paso 5 |
@sveltejs/vite-plugin-svelte |
7.3.0 | en package.json; paso 5 |
svelte-check |
4.7.6 | en package.json; paso 5, npm run check |
@sveltejs/adapter-static |
3.0.10 | en package.json; paso 5: la página es estática y no necesita servidor |
@noble/curves |
2.4.0 | solo desarrollo: oráculo auditado de bls12381.contrast.test.ts, que compara nuestro bls12381.ts con la referencia Go y con noble; un test falla si algo que no sea un test la importa, así que nunca llega al sitio. Arrastra @noble/hashes 2.4.0 |
Todas las versiones se fijan exactas y package-lock.json se versiona. .npmrc activa legacy-peer-deps porque npm 11.5.2 falla al resolver los peers opcionales de vitest 5.0.1 (Cannot read properties of null (reading 'edgesOut')); con esa opción npm no instala peers, así que el peer obligatorio vite está declarado explícitamente.
testdata
npm run testdata:sync
Copia testdata/ de ../datekeys-go en HEAD, leyendo los blobs con git para no arrastrar cambios sin commit, y escribe testdata/SOURCE.json con el commit completo y el SHA-256 de cada fichero. Para fijar otro commit: node scripts/sync-testdata.mjs sync --commit <rev>.
npm run testdata:check
Comprueba que los ficheros coinciden con SOURCE.json, sin faltantes ni sobrantes, y vuelve a leerlos del repositorio Go en el commit registrado. Sin la opción --against, node scripts/sync-testdata.mjs check verifica solo la copia local. Ambos comandos usan solo Node y git.
.gitattributes marca testdata/** como binario para que git no altere ningún byte.
Copia actual: la de testdata/SOURCE.json (commit 692cf87, rama v0.8.2).
Licencia
Apache-2.0 (LICENSE), como la librería Go de referencia. El código que se derive de terceros conserva su aviso de copyright y licencia en el propio fichero; el primero previsto es el núcleo IBE de la fase 2, derivado de tlock-js (Apache-2.0 OR MIT).