73 KiB
Plan: fase 3 del SDK TypeScript. Writer de cápsulas .dkc de formato 2 y claves .dkk, comprobado contra Go a nivel de cápsula, y página para crearlas
Estado: v3, 29 de septiembre de 2026. La v2 replanteó la v1 del mismo día sobre la v0.9 del spec (tag spec-v0.9 de datekeys-go), que añade el formato 2 de cápsula y hace normativas las reglas del escritor (§62.1). La v1 describía el writer de la v0.8.2 y queda en la historia de este repo.
Decisiones confirmadas por el autor el 29-09-2026 (paso 0), todas con su recomendación. En particular:
- la 3:
SEALED_CONTROL_LENcon la fórmula del §62.1, comprobada con el sellado real; - la 6: un recipient canónico del twist se acepta, como en Go;
- la 14: se cierra
0.1.0antes de empezar, y la fase 3 va en0.2.0-dev.
Un revisor crítico contrastó este borrador con el spec, con Go, con el código TypeScript y con age-encryption instalado, con sondas propias. No encontró nada bloqueante ni grave: ninguna decisión rompe un MUST del §62.1. Sus 12 correcciones menores están incorporadas, entre ellas los textos de relleno de Go, el punto del twist, las cifras del relleno, la comprobación de la cabecera del payload y los huecos de los tests frente a los de Go.
Parte de lo que había el 29-09-2026 por la noche, contrastado con el código:
- el spec v0.9 (§21, §29.1, §37 a §39, §42, §53, §55.2, §57, §61, §62, §62.1 y §70) y
datekeys.cddl; capsule/encrypt.goy la CLI dedatekeys-goen7e2d83c, la referencia que ya escribe el formato 2;datekeys-tsen0118890, que ya lee los dos formatos, conage-encryption0.3.1 instalado;- la v1 de este plan y sus 26 correcciones de revisión, que siguen valiendo donde el formato 2 no cambia nada.
Alcance: escribir en TypeScript, sin red, cápsulas .dkc de formato 2 y claves portables .dkk del perfil Quicknet, como hacen capsule.Encrypt y accesskey.Encode en Go. Esto incluye:
- dos módulos nuevos de librería,
src/lib/dkc/encrypt.tsysrc/lib/dkc/writer.ts; - un módulo sin noble para los recipients
age1…,src/lib/dkc/recipient.ts; - las piezas que faltan en
x25519.ts,digest.ts,datekey.tsytempfile.ts, y el uso decompareInstantsenopen.ts; - las guardas nuevas en
dependencies.test.tsycheck-build.mjs; - los tests y la prueba de interoperabilidad TS → Go a nivel de cápsula;
- como último paso, con decisiones de interfaz propias, una página para crear cápsulas en el navegador.
El writer no entra en index.ts ni en la primera carga de ninguna página: se carga bajo demanda, igual que la apertura. Nunca escribe el formato 1: solo un generador de vectores de prueba puede hacerlo (§62.1, regla 1, y §70), y ese generador es internal/testkit de Go.
Regla de dependencias: la de las fases anteriores. En ejecución solo están age-encryption 0.3.1 y @noble/curves, @noble/hashes y @noble/ciphers 2.4.0. Nada nuevo entra sin aprobación escrita del autor. Esta fase no necesita ningún paquete nuevo (sección 3).
Qué cambia respecto a la v1
- Formato 2 siempre.
VERSION= 2 en el PRELUDE yCONTROL_CBORde versión 2, con L y el código de relleno en las claves 6 y 7. - L se conoce antes de sellar (§62.1, regla 6). Con un
Blobo unUint8Arraysale de su tamaño; con unReadableStream, quien llama la da. La fuente tiene que entregar exactamente L bytes, oencryptfalla y la salida se aborta (decisión 2). - Relleno. El plaintext de
PAYLOAD_AGEes el contenido seguido de ceros hasta P = regla(L), conreforzadopor defecto (decisiones 2 y 16). - 16 huecos. De 1 a 16 credenciales, señuelos en el resto y un orden uniformemente aleatorio (§39, §62.1, reglas 3 y 4). Desaparecen el límite de 1 024 stanzas y "
R_ACCESSel último" (decisiones 4, 5 y 6). - Recipients no canónicos y de orden bajo. Go ya los rechaza desde la v0.9 (
agewrap.CheckX25519Recipient), así que la diferencia con la referencia que proponía la v1 desaparece: los mismos casos y los mismos textos (decisión 6). SEALED_CONTROL_LENcon la fórmula de la nota del §62.1 y comprobando el sellado real, en lugar del borrador de Go. Así hay un solo sellado de 16 stanzas y un solo wrap tlock, y sobra la caché del pairing de la v1 (decisión 3).- Autocomprobaciones de la regla 11, las mismas que Go, y dos más: la de
OUTER_TIME_AGE, que ya proponía la v1, y la cabecera dePAYLOAD_AGEantes de escribir nada, condecryptHeader(decisión 8). - Tests. Se reproducen los siete fixtures de formato 2, con la permutación fijada desde su registro. Se añaden la uniformidad del orden y los bordes del relleno (sección 8).
- Página. Hasta 16 credenciales, el aviso de privacidad del §55.2, y ninguna elección de regla de relleno (sección 9).
1. Situación de partida
Todo lo de esta tabla se ejecutó o se leyó el 29-09-2026 en Node 24.9.0, sobre datekeys-ts en 0118890 y datekeys-go en 7e2d83c. La sonda de ese día (orden de los stanzas, coste del sellado interno y orden bajo) se describe en cada punto y se reproduce con él.
| Hecho | Detalle |
|---|---|
| Encoders TypeScript que ya existen | encodeHeader (header.ts) y encodeControl(c, format) (control.ts), que ya escribe la versión 2, con payload_length y padding.marshalAccessKeyBody y encodeAccessKey (accesskey.ts); marshalAccessKeyBody ya se autocomprueba.preludeBytes (con el formato), headerBinding y dkkPreludeBytes (framing.ts).paddedLength, payloadAgeLength y MAX_PAYLOAD_LENGTH (padding.ts), exactas hasta L_MAX.ACCESS_SLOTS y checkAccessStanzas(stanzas, slots) (age.ts).newExtension, canonicalExtensions y checkDisjoint (extension.ts).resolveDateKey y roundTime (datekey.ts).timeRecipient (tlock.ts).Todos se comparan byte a byte con los fixtures y vectores de Go. encodeHeader y encodeControl no se autocomprueban: Go lo hace en capsule.Encrypt (selfCheckHeader, selfCheckControl). |
| Lo que falta | La orquestación de capsule.Encrypt.Generar identidades X25519 en bytes crudos y derivar su clave pública, también la de los señuelos. Un índice aleatorio sin sesgo y la permutación de Fisher–Yates. Leer y escribir age1…, y comprobar que un recipient es canónico y no de orden bajo.Un SHA-256 que calcule sobre lo que se escribe: sha256Stream (digest.ts) consume el stream.Un comparador de Instant exportado: before es privado en open.ts.Escribir exactamente L bytes de la fuente y después el relleno. Las comprobaciones que Encrypter no hace. |
capsule.Encrypt de Go en 7e2d83c |
Orden (encrypt.go): 1. perfil y reloj obligatorios; p.Validate(); instante posterior a Now(); Length no negativa; código 0 → Reforzado; PaddedLength(L, código), que rechaza L > L_MAX;2. datekey.Resolve y la ronda que no abre antes de lo pedido;3. accessRecipients: time_only sin credenciales; política conocida; de 1 a 16 credenciales; cada recipient X25519, canónico y no de orden bajo (CheckX25519Recipient), ninguno repetido; I_ACCESS nueva si se pide, y su recipient el último de la lista;4. capsule_id, luego I_PAYLOAD;5. fillSlots: un señuelo por hueco libre (age.GenerateX25519Identity, que se descarta), y permute, Fisher–Yates con crypto/rand.Int;6. PUBLIC_HEADER y selfCheckHeader;7. borrador de CONTROL_CBOR con binding e identidad a cero y los mismos L y código; su sellado mide SEALED_CONTROL_LEN, con el límite de 64 MiB;8. PRELUDE (formato 2), header_binding, CONTROL_CBOR real y selfCheckControl;9. sellado real: INNER_ACCESS_AGE con los 16 recipients y selfCheckInner (16 stanzas X25519 con shares distintos; I_ACCESS abre exactamente uno y da el control), después OUTER_TIME_AGE; la longitud igual a la del borrador;10. escritura de PRELUDE, cabecera y control y, en streaming, PAYLOAD_AGE por io.MultiWriter(dst, sha256): writeContent copia exactamente L bytes, comprueba que la fuente no da más, y escribe los ceros hasta P;11. selfCheckPayload: PAYLOAD_AGE mide PayloadAgeLength(P) e I_PAYLOAD abre su cabecera;12. la .dkk como objeto, con capsule_digest y un credential_id aleatorio.Result da la DateKey, el instante efectivo, capsule_id, el formato, L, el código, P y la .dkk. Los errores de dst y de src se devuelven sin tocar y sin código. |
Encrypter de age-encryption 0.3.1 |
Lo que ya anotaba la v1 sigue igual: - sin recipients escribe una cabecera sin stanzas, que todo lector rechaza; - no tiene labels; - addRecipient(string) acepta también age1pq1…, age1tag1… y age1tagpq1…;- X25519Recipient no se exporta;- un recipient no canónico produce un stanza que ninguna identidad abre; - encrypt(ReadableStream) devuelve primero la cabecera sola y cifra cada trozo de entrada de una vez, sin presión inversa;- toda su aleatoriedad sale de crypto.getRandomValues.Comprobado además el 29-09 por la noche: - escribe los stanzas en el orden de addRecipient: con 16 recipients, la identidad i abre el stanza i. Es lo que exige §62, paso 12;- los recipients de orden bajo hacen fallar el cifrado: con Web Crypto, una DOMException (OperationError); sin él, noble rechaza el secreto compartido cero. Sus coordenadas u canónicas son 0, 1, las dos de orden 8 y p − 1; p − 2, por ejemplo, no falla;- un punto canónico del twist, como u = 2, se cifra sin error, y nadie podría abrir ese stanza (§37); - Decrypter.decryptHeader(header) analiza la cabecera, desenvuelve la file key y verifica su MAC sin necesitar el payload. Del stream de encrypt, el primer trozo son los 168 bytes de la cabecera sola, y para entonces ya se han leído unos 3 × 64 KiB de la fuente. |
| Longitudes | Son las de la nota informativa del §62.1, con c(n) = max(1, ⌈n / 65 536⌉), C = |CONTROL_CBOR| y d el número de dígitos de la ronda: - PAYLOAD_AGE = 184 + P + 16·c(P);- INNER_ACCESS_AGE = 86 + 98·16 + C + 16·c(C);- OUTER_TIME_AGE = 335 + d + n + 16·c(n).Sin extensiones de control, C = 103. SEALED_CONTROL_LEN vale 458 en time_only y 2 128 en time_and_key para la ronda 1000, como en los fixtures format2_*. La sonda midió un INNER_ACCESS_AGE de 16 stanzas con C = 103 en 1 773 bytes, lo que da la fórmula. |
| Coste | Sellar INNER_ACCESS_AGE con 16 recipients: 57 ms en Node la primera vez y unos 14 ms en caliente. Cada wrap hace una multiplicación por la base, otra por la clave y una importación pkcs8 de Web Crypto.Un wrap tlock (pairing, gt^r, r·G2): de 83 a 180 ms en caliente, 363 ms el primero (v1).PAYLOAD_AGE en streaming, en trozos de 64 KiB: 70 MiB/s solo con age, 57 MiB/s con el SHA-256 en la misma pasada (v1).El relleno añade a lo cifrado P − L bytes (§29.1): 256 con L = 0 y como mucho 255 hasta 8 192 bytes, donde las dos reglas coinciden. Por encima, con reforzado, menos de L/2^S: menos de un 6,25 %, de un 3,125 % desde 65 536 bytes y de un 1,5625 % desde 2³². |
| Fixtures | Siete de formato 2. Cada registro trae capsule_id, payload_identity, payload_length, padding, padded_length, las identidades de los recipients, access_key_stanza e identity_stanzas (el hueco que abre cada credencial), los stanzas, la DateKey, el release publicado y las extensiones.Los cinco de formato 1 son de la v0.8.2 y el writer no los puede reproducir, porque no escribe el formato 1. |
| Spec v0.9 | §61 y §62 dan un orden válido. §62.1 da las reglas, normativas se siga o no ese orden: - 1, formato 2; - 2, instante posterior al reloj; - 3, de 1 a 16 credenciales X25519, canónicas, no de orden bajo y sin repetir; - 4, señuelos y orden uniforme, sin guardar sus claves ni el orden; - 5, CSPRNG para capsule_id, I_PAYLOAD, I_ACCESS, credential_id, los señuelos y el orden;- 6, L conocida antes de sellar, sin suponerla; - 7, SEALED_CONTROL_LEN exacta, con un sellado provisional o con la fórmula, y comprobada;- 8, límites y extensiones; - 9, ante un error no presentar lo escrito como cápsula. Recomendaciones (SHOULD): la 10, el código 2 por defecto; la 11, las autocomprobaciones; la 12, el borrado. Además: - §15: la ronda es la primera en el instante pedido o después; - §53: el SDK advierte en horizontes largos; - §55.2: qué oculta y qué no el formato 2; - §57: límites que también obligan al encoder; - §67 y §68: los vectores son de descifrado, y no se exige reproducir los bytes de age. |
| Guardas y página hoy | NOBLE_IMPORTERS (dependencies.test.ts) contiene digest.ts, ibe.ts, release.ts y x25519.ts.Las importaciones de age-encryption no tienen lista blanca.check-build.mjs comprueba la carga bajo demanda solo para inspect.html.tempfile.ts es propio de la apertura (datekeys-open, plaintext), y cancellable ya cancela una apertura por su salida.La CSP tiene connect-src 'self' y worker-src 'none'. |
| Repositorios | datekeys-ts: main en 0118890, que implementa el spec 0.9 y lee los dos formatos. Tiene 5 425 tests y npm run verify en verde; testdata en 7e2d83c (spec-v0.9). Versión 0.1.0-dev.datekeys-go: main en 7e2d83c, con el tag spec-v0.9. |
2. Decisiones propuestas
Confirmadas por el autor el 29-09-2026 (paso 0 de la sección 10), todas con su recomendación. Cada una conserva sus alternativas y dice si cambia respecto a la v1.
-
Módulo y API, espejo de
capsule.Encrypt. Cambia:length,paddingy los campos del resultado.src/lib/dkc/encrypt.tsexportaencrypt(src, opts): Promise<Encrypted>, y nada más que eso y sus tipos.EncryptOptionssigue campo a campo a su equivalente de Go:profile,unlockAt,policy,recipients,newPortableKey,length,padding,critical,noncritical,controlCritical,controlNoncriticalynow. Añadeoutputyprogress(decisión 2).Encryptedsigue aResult:dateKey, elunlockAtefectivo ycapsuleId;format(siempre 2),length(L),paddingypaddedLength(P);portableKey: unAccessKeysin codificar, concapsule_digest, como en Go. Quien llama lo codifica conencodeAccessKey, que se autocomprueba, y lo borra conwipeAccessKey. Añadesizey, cuando no hayoutput,dkc.
encryptes asíncrono, y quien llama podría cambiar sus entradas durante una espera. Por eso copia al empezar el perfil (cloneProfile),unlockAt, los recipients y las extensiones. El resultado denow()se valida como cualquierInstant.- No se reexporta desde
index.ts. - Alternativa descartada: devolver la
.dkkya codificada. La aplicación puede querer añadir extensiones no críticas de §44 antes de codificarla, y Go devuelve el objeto. - Recomendación: la propuesta.
-
Entrada, longitud y salida en streaming. Cambia: L conocida de antemano y el relleno.
srcpuede serUint8Array,BloboReadableStream<Uint8Array>.- L:
- con un
Uint8Arrayo unBlob, es su tamaño. Si además llegalength, tiene que ser igual, o hayTypeError; - con un
ReadableStream,lengthes obligatoria. En los tres casos se comprueba que no pase de L_MAX (PaddedLengthde Go da el texto) antes de escribir nada.
- con un
- La fuente entrega exactamente L bytes. Si da menos, o más,
encryptfalla con los textos dewriteContentde Go:capsule: the source ended after %d bytes, and EncryptOptions.Length is %d;capsule: the source delivers more than the %d bytes of EncryptOptions.Length. Solo se descubre al leerla, cuando ya se ha escrito parte del.dkc: la salida se aborta (§62.1, regla 9). Con unBlob, o unUint8Arrayque nadie cambia, no pasa; aun así se cuentan los bytes de toda fuente, porque la vista de unArrayBufferredimensionable o transferido puede encogerse durante una espera. Los trozos vacíos no cuentan como "más".
- La entrada se trocea en 64 KiB antes de entrar en
age, consubarrayy unaReadableStreamde tipo pull. AsíencryptSTREAMnunca cifra de golpe un trozo grande y la escritura tiene presión inversa. Tras los L bytes llegan los P − L ceros del relleno, en trozos de 64 KiB nuevos, nunca un búfer compartido queagepudiera retener. outputes unWritableStream. Se cierra solo cuando todo ha terminado y se aborta ante cualquier fallo, también ante unTypeErrorde opciones.- Sin
output, el.dkcse devuelve en memoria, en un buffer del tamaño exacto, que ahora siempre se conoce:16 + |PUBLIC_HEADER| + SEALED_CONTROL_LEN + payloadAgeLength(P). Como con unReadableStreamese tamaño sale de unlengthque nada respalda todavía (hasta L_MAX), la salida en memoria tiene un máximo exportado,MAX_MEMORY_DKC, que se propone de 1 GiB: por encima,TypeErrorantes de sellar nada. El buffer se reserva después deprogress(0, total), nunca antes (§57). - Nada se escribe hasta que todo lo que se puede comprobar antes del contenido está comprobado, incluida la cabecera de
PAYLOAD_AGE(decisión 8). Es una garantía más fuerte que la de Go, que escribe esa cabecera antes de comprobarla. progress(written, total)se llama conwritten = 0justo antes de la primera escritura, ytotales siempre el tamaño exacto. La página comprueba ahí la cuota; siprogresslanza, no se escribe nada.- El SHA-256 del
capsule_digestse actualiza con cada trozo antes de escribirlo; es elio.MultiWriterde Go. - La página cancela con
cancellable, como la apertura: su salida deja de aceptar datos. - Alternativa descartada: volcar a un fichero temporal un
ReadableStreamsin longitud. §62.1 lo permite (MAY), pero la librería no tiene dónde, y la página solo lee ficheros, cuyo tamaño se conoce (decisión 17). - Recomendación: la propuesta.
-
SEALED_CONTROL_LENcon la fórmula del §62.1, comprobada. Cambia: la v1 proponía el borrador de Go y una caché del pairing.- Se codifica CONTROL_CBOR con
header_bindingeI_PAYLOADa cero y con L, el código y las extensiones reales. Su longitud C no depende de esos ceros (§62.1, regla 7). Con C, las fórmulas de la sección 1 danSEALED_CONTROL_LEN, que va al PRELUDE. - Después se sella una sola vez, el control real, y se exige que el sellado mida exactamente eso. Si no, el error interno de Go:
capsule: internal error: SEALED_CONTROL is %d bytes, measured %d. Nunca sale una cápsula mal enmarcada. - Coste: un sellado de 16 stanzas y un wrap tlock por cápsula, en lugar de dos de cada.
- Alternativa: el borrador de Go, sellar dos veces. Es la otra vía del §62.1 y la de la referencia. Cuesta el doble, y con la caché del pairing de la v1 algo menos, a cambio de más código en
tlock.ts. - Recomendación: la fórmula, sin caché. La igualdad con el sellado real la hace segura, y los tests comprueban la fórmula contra el borrador.
- Se codifica CONTROL_CBOR con
-
Aleatoriedad, y determinismo en los tests. Cambia: los señuelos y el orden son valores propios del writer.
- Todo sale de
crypto.getRandomValues:- los valores propios del writer, MUST de §62.1 regla 5:
capsule_id,I_PAYLOAD,I_ACCESS,credential_id, los escalares de los señuelos y los índices de la permutación; - sigma, en
ibe.ts; - lo que extrae
age-encryption: file keys, efímeras y nonces.
- los valores propios del writer, MUST de §62.1 regla 5:
- Índice sin sesgo: para un índice en
[0, n), se sacan 32 bits y se rechaza todo valor desde⌊2³² / n⌋ · n. La permutación es Fisher–Yates con ese índice, comopermutede Go concrypto/rand.Int. - El orden de extracción es el de Go:
I_ACCESS,capsule_id,I_PAYLOAD, los señuelos, la permutación y, al final,credential_id. - El núcleo del writer está en
src/lib/dkc/writer.tsy recibe un objeto de extracciones con un método por valor.encrypt.tslo llama siempre con el decrypto.getRandomValues, y ningún módulo de producción puede pasarle otro. - Para los tests,
src/lib/dkc/testing/encrypt.tsllama al núcleo con valores fijados por nombre. La permutación se fija desdeaccess_key_stanzaeidentity_stanzasdel registro, así que cada credencial abre el hueco del fixture. Una guarda comprueba que soloencrypt.tsytesting/importanwriter.ts. - Los valores internos de
ageno se inyectan, porque §67 no lo exige. Con los valores de un registro, las secciones deterministas salen iguales byte a byte a las del fixture de Go. - La permutación y qué huecos son señuelos no salen nunca del writer: ni en el resultado, ni en un error, ni en un registro (§62.1, regla 4). Solo la variante de tests los conoce, porque se los da quien llama.
- Alternativa descartada:
Math.random, o el módulo de 32 bits sin rechazo. El segundo sesga el orden para 16 huecos, y §39 exige uniformidad. - Recomendación: la propuesta.
- Todo sale de
-
Identidades X25519 en bytes crudos; ningún secreto como cadena. Sin cambios, ampliada a los señuelos.
I_PAYLOAD,I_ACCESSy cada escalar de señuelo son 32 bytes degetRandomValues.- Su clave pública se deriva con
x25519.getPublicKeyde noble, enx25519.ts, que ya está en la lista blanca de noble. - El escalar de un señuelo se pone a cero en cuanto se deriva su clave pública, antes de sellar nada (§39, §62.1, regla 4). Los bigints en que noble lo convierte no se pueden borrar (sección 6).
- Los recipients llegan a
age-encryptioncomoage1…, escritos porrecipient.ts. El wrap lo hace elX25519Recipientdeage-encryption, igual que Go usa el deage. - Alternativa descartada:
generateX25519IdentityeidentityToRecipientdeage-encryption. Devuelven cadenas que no se pueden borrar, ygenerateIdentityavisa de que puede pasar a devolver identidades híbridas. - Recomendación: la propuesta.
-
Recipients: bytes crudos en la librería,
age1…en la aplicación, con las comprobaciones de Go. Cambia: ya no hay diferencia con la referencia, y el máximo es 16.EncryptOptions.recipientsson claves públicas X25519 de 32 bytes, como los*age.X25519Recipientde Go.src/lib/dkc/recipient.ts, sin noble, lee y escribe las cadenas:parseX25519Recipientaceptaage1…en minúsculas, HRPagey 32 bytes. No aceptaAGE1…, mayúsculas mezcladas,age1pq1…niage1tag1…;formatX25519Recipientescribeage1…. Va aparte dex25519.ts, que importa noble: la página valida las líneas mientras se escriben, y eso metería noble en su primera carga, quecheck-buildprohíbe.
checkX25519Recipient(raw)aplica la regla 3 de §62.1 comoagewrap.CheckX25519Recipient, con sus textos:- bit 255 activado →
agewrap: recipient age1… is not canonical: bit 255 is set; - u ≥ p →
… is not canonical: u is not below 2^255 - 19; - orden bajo →
… is a point of low order: the shared secret would be zero. El orden bajo se comprueba con la lista de las cinco coordenadas u canónicas de la sección 1, sin aritmética de curva. Una vez descartadas las no canónicas, es equivalente a la prueba de Go con un escalar cualquiera, y un test lo contrasta con noble.
- bit 255 activado →
encryptlos comprueba todos, en el orden deaccessRecipientsde Go y con sus textos:time_onlycon credenciales →capsule: time_only takes no recipients and no portable key;- política desconocida →
capsule: unknown access policy %d; - ninguna credencial →
capsule: time_and_key needs at least one recipient or a portable key; - más de 16 →
capsule: time_and_key takes at most 16 credentials, recipients and portable key together; %d given; - un recipient que no es canónico o es de orden bajo →
capsule: recipient %d: agewrap: …; - uno repetido →
capsule: recipient age1… listed twice; INNER_ACCESS_AGE holds one stanza per recipient.
- Una lista escrita por una persona se lee como un fichero
-Rdeage, algo más tolerante: se aceptan CRLF, se ignoran las líneas vacías y las que empiezan por#, y además se recortan los espacios de cada línea, queage1.3.2 no recorta. Comoage, una línea que empieza porAGE-se rechaza con un aviso propio: es una identidad secreta pegada por error. Un error se da por número de línea, nunca por su contenido. - Puntos del twist. §37 deja en MAY rechazar un recipient canónico que está en el twist, con el símbolo de Legendre: nadie podría abrir su stanza.
CheckX25519Recipientno lo hace, yage-encryptioncifra hacia él sin error. Se propone lo mismo que Go, aceptarlo, para mantener la paridad. Alternativa: comprobar el símbolo de Legendre conBigIntenrecipient.ts, sin noble, y proponerlo también para Go. - A
addRecipientsolo llegan cadenas escritas porformatX25519Recipient. - Alternativa descartada: aceptar cadenas en la librería. En Go las lee la CLI, no
capsule.Encrypt, y pasar aaddRecipientlo que escribe una persona dejaría entrar recipients que no son X25519. - Recomendación: la propuesta.
-
Reloj y entradas obligatorias. Sin cambios.
now: () => Instantes obligatorio y se llama una sola vez.unlockAttiene que ser estrictamente posterior anow()(§62.1, regla 2) →capsule: unlock time <RFC3339Nano> is not in the future.- Un
Instantmal formado (segundos no enteros, onanosfuera de 0 a 999 999 999) es unTypeError, porqueresolveDateKeyno lo comprueba. policyes obligatoria. En Go su valor cero estime_only; en TypeScript, omitirla es unTypeError, para que la política sea siempre explícita.compareInstantspasa adatekey.ts, yopen.tslo usa en lugar de subefore.- Recomendación: la propuesta.
-
Autocomprobaciones: las de la regla 11, como Go, y dos más. Cambia: las del formato 2.
- Dentro de
encrypt, antes de escribir nada:- PUBLIC_HEADER con
decodeHeader; - CONTROL_CBOR con
decodeControl(…, FORMAT_2); la copia deI_PAYLOADque devuelve se borra en el acto, como en Go; INNER_ACCESS_AGE:ageStanzasycheckAccessStanzas(stanzas, 16), 16 stanzas X25519 con shares distintos. Con clave portable,accessIdentity([I_ACCESS], 16)lo abre y da exactamente el control, comoselfCheckInner, y ese control descifrado se borra después.decryptAllyreadAll, que ya borran sus trozos, son privados deopen.ts: pasan a un módulo interno que comparten la apertura y el writer;OUTER_TIME_AGEconcheckTimeStanzas, las comprobaciones de los pasos 5 y 8 de §63. Go no la necesita, porque suagetiene labels;- la longitud del sellado igual a la calculada (decisión 3);
- la cabecera de
PAYLOAD_AGE, el primer trozo del stream deage:Decrypter.decryptHeaderconpayloadIdentity(I_PAYLOAD), que aplicacheckPayloadStanzas, desenvuelve la file key y verifica el MAC sin necesitar el payload. La file key que devuelve se borra en el acto, como elclear(fileKey)de Go. Si falla, se cancelan el stream deagey la fuente. Go lo comprueba después de escribir; aquí va antes.
- PUBLIC_HEADER con
- Al terminar el contenido: se entregaron a
ageexactamente P bytes yPAYLOAD_AGEmidepayloadAgeLength(P), comoselfCheckPayload. - La
.dkkse autocomprueba al codificarla, enencodeAccessKey, comoMarshalBodyen Go. No es parte deencrypt. - Como Go, el writer no vuelve a descifrar el payload que escribe.
- Con encoders correctos estas comprobaciones no fallan nunca. Como en
TestEncryptSelfCheckde Go, cada una es una función quetesting/puede llamar directamente con entradas malas, sinv8 ignore. - Alternativa: exactamente las de Go.
- Recomendación: las de Go, con la de
OUTER_TIME_AGEy la cabecera del payload antes de escribir, documentadas como diferencia.
- Dentro de
-
Errores: textos y códigos de Go donde hay equivalente. Cambia poco: menos diferencias.
- Todo error con equivalente en Go lleva su texto byte a byte y su código normativo. Cuando Go no le da código, el error TypeScript tampoco lo lleva, como
datekeys.Code. - El código de relleno y L los comprueba el writer, con los textos de
PaddedLengthde Go (capsule: padding code %d is not defined,capsule: content of %d bytes exceeds L_MAX = %d), antes de llamar apaddedLength, cuyosRangeErrortienen texto propio.paddingausente esreforzado;padding: 0es un error, donde en Go el valor cero es el de por defecto. - Las entradas que faltan o están mal formadas son
TypeError, como enopen:- sin
profileo sinnow, con los textos de Go (capsule: EncryptOptions.Profile is required,… Now is required); - sin
policy, y unInstantmal formado, con texto propio; - un recipient que no son 32 bytes (Go imprime el tipo con
%T); - un
lengthque no es un entero seguro no negativo, o que no coincide con el tamaño de unBlobo unUint8Array.
- sin
- Los errores de la fuente y de la salida se relanzan sin tocar y sin código, como en Go. No pasan a
ERR_INTEGRITYcomo enopen, porque escribir no es un paso de §63. - Los fallos de
age-encryption, Web Crypto o noble llevan un texto fijo, con el original solo encause. - El script de Go del paso 5 registra los textos de la tabla de opciones inválidas, y un test los compara.
- Recomendación: la propuesta.
- Todo error con equivalente en Go lleva su texto byte a byte y su código normativo. Cuando Go no le da código, el error TypeScript tampoco lo lleva, como
-
Extensiones sin registro en el writer (§72). Sin cambios.
- Como en Go,
encryptyencodeAccessKeyescriben las extensiones que reciben, después de las reglas de §54 que ya aplican los encoders. No reciben unExtensionRegistry: la regla de ubicación de §72 la aplica la aplicación. - La página de esta fase no escribe extensiones, así que la cumple sin más.
- Recomendación: la propuesta.
- Como en Go,
-
Interoperabilidad TS → Go a nivel de cápsula. Cambia: muestras de formato 2.
scripts/capsule-ts-samples.mjsescribe cápsulas para rondas ya publicadas (1000, 1001 y 2000), connowen el génesis, y claves e instantes fijos.scripts/capsule-go-verdicts.go, en un módulo temporal conreplacea../datekeys-goy la directivagode la referencia, las inspecciona y las abre concapsule.Openy las firmas publicadas. No puede usarinternal/testkit: usaprofile.Default,provider.ReleaseSourceFunc,capsule.ParsePrelude,capsule.DecodeControlyagewrap.- Go registra, además de abrirlas con cada credencial: el formato, L, el código y P que da
Opened, y el número de stanzas deINNER_ACCESS_AGE, que tiene que ser 16. - El resultado se congela en
src/lib/dkc/testing/capsule-vectors.json. El script se ejecuta a mano. - Recomendación: la propuesta.
-
La página, en una ruta propia y como último paso. Sin cambios.
- Tiene sus propias decisiones de interfaz (sección 9), que el autor confirma en el paso 6. Los pasos de librería no dependen de ella.
- Ruta propuesta:
/create, por coherencia con/inspect. Alternativa:/crear. - Recomendación:
/create.
-
Listas blancas de importación. Sin cambios.
age-encryptionsolo la importanopen.ts,tlock.ts,writer.tsy los tests. La de noble no cambia:encrypt.ts,writer.tsyrecipient.tsno importan noble.writer.tssolo lo importanencrypt.tsytesting/(decisión 4).- Recomendación: añadirlas.
-
Versión. Sin cambios, sigue abierta.
- Cerrar
0.1.0antes de empezar, con la inspección y la apertura de los dos formatos (spec 0.9), y llevar la fase 3 en0.2.0-dev. El writer produce ficheros que la gente guardará años y cambia la superficie de seguridad. - Alternativa: incluir la fase 3 en
0.1.0. - Recomendación: la primera. Decide el autor.
- Cerrar
-
Bucle de propiedades: 50 semillas en
verifyy 500 a mano. Sin cambios.- Cada caso hace un sellado tlock y, en
time_and_key, 16 wraps X25519, más el IBE deopen. 50 semillas en cada ejecución y 500 en una ejecución manual por paso, anotada en el HANDOFF. - Recomendación: la propuesta.
- Cada caso hace un sellado tlock y, en
-
El código de relleno. Nueva.
- La librería usa
reforzadopor defecto (§62.1, regla 10) y aceptapadding: BLOQUE256, como Go. - La página no deja elegir: siempre
reforzado(sección 9).bloque256revela más de L, y la diferencia de tamaño no compensa a quien no sabe elegir. - Alternativa: ofrecer las dos en la página, con una explicación.
- Recomendación: la propuesta.
- La librería usa
-
Contenido de longitud desconocida. Nueva.
- La librería exige L. Un
ReadableStreamsinlengthes unTypeErrorantes de leer nada. No vuelca la fuente a un fichero temporal. - La página solo cifra ficheros, cuyo tamaño da el navegador, así que no lo necesita.
- Alternativa: que
encryptvuelque unReadableStreamsin longitud a OPFS antes de sellar (§62.1, regla 6, MAY). Metería OPFS en la librería, que hoy no lo toca. - Recomendación: la propuesta.
- La librería exige L. Un
3. Dependencias de ejecución y guardas
| Paquete | Versión | Estado |
|---|---|---|
age-encryption |
0.3.1 | ya aprobada e instalada; hace las tres envolturas age |
@noble/curves |
2.4.0 | ya directa; x25519.getPublicKey, desde x25519.ts |
@noble/hashes |
2.4.0 | ya directa; SHA-256 incremental, desde digest.ts |
@noble/ciphers |
2.4.0 | ya directa; el cifrado lo hace age-encryption, y el writer solo la usa a través de x25519.ts en las autocomprobaciones |
No hace falta ningún paquete nuevo. Donde uno podría parecer útil, la alternativa con código propio es esta:
- zip para entregar
.dkcy.dkkjuntos: dos descargas separadas, que es lo recomendado, porque a menudo la.dkkdebe viajar por otro canal; - zonas horarias (polyfill de Temporal, luxon, date-fns-tz):
Intl.DateTimeFormatconformatToPartseIntl.supportedValuesOf('timeZone'), unas 40 líneas en la página; - selector de fecha: los
<input type="date">y<input type="time">nativos; - tests de propiedades (
fast-check, que sería de desarrollo): un bucle con un generador propio (splitmix64) que imprime la semilla, unas 20 líneas; - índices aleatorios sin sesgo: el rechazo de la decisión 4, unas 10 líneas.
Guardas, todas en tests que corren en cada ejecución:
- siguen las actuales: versiones exactas, lockfile, lista blanca de noble, y ningún noble,
@scure/baseniage-encryptionen la primera carga de ninguna página; - nuevas: las listas blancas de
age-encryptiony dewriter.ts(decisión 13); index.tsno reexportaencrypt.ts,writer.tsnirecipient.ts, y un test lo comprueba;check-build.mjsgeneraliza la comprobación de carga bajo demanda a una lista de páginas, cada una con los paquetes que carga después (inspect.htmly la página de crear).
La CSP (connect-src 'self'), que check-build ya exige, garantiza que la página de crear no hace ninguna petición a otro origen, y se comprueba en el navegador en el paso 7. Que el fichero no salga del dispositivo lo garantiza el código, que no lo envía a ningún sitio, no la CSP: 'self' permite peticiones al propio origen.
4. encrypt.ts y writer.ts
src/lib/dkc/encrypt.ts exporta encrypt, que llama al núcleo de writer.ts con crypto.getRandomValues. writer.ts importa Encrypter y Decrypter de age-encryption y los módulos propios. No importa noble directamente.
export interface EncryptOptions {
readonly profile: Profile; // required, as Go
readonly unlockAt: Instant; // requested instant, after now()
readonly policy: Policy; // required: TIME_ONLY or TIME_AND_KEY
readonly recipients?: readonly Uint8Array[]; // raw 32-byte X25519 public keys
readonly newPortableKey?: boolean; // fresh I_ACCESS, never an existing one (§38)
readonly length?: number; // L: required for a ReadableStream
readonly padding?: Padding; // REFORZADO when omitted (§62.1 rule 10)
readonly critical?: readonly Extension[]; // PUBLIC_HEADER
readonly noncritical?: readonly Extension[];
readonly controlCritical?: readonly Extension[]; // CONTROL_CBOR
readonly controlNoncritical?: readonly Extension[];
readonly now: () => Instant; // required, called once
readonly output?: WritableStream<Uint8Array>;
readonly progress?: (written: number, total: number) => void;
}
export interface Encrypted {
readonly dateKey: DateKey;
readonly unlockAt: Instant; // effective round time
readonly capsuleId: Uint8Array;
readonly format: 2;
readonly length: number; // L
readonly padding: Padding;
readonly paddedLength: number; // P
readonly portableKey?: AccessKey; // the caller encodes and wipes it
readonly size: number;
readonly dkc?: Uint8Array; // only without output
}
export function encrypt(src: Uint8Array | Blob | ReadableStream<Uint8Array>, opts: EncryptOptions): Promise<Encrypted>;
Flujo, en el orden de capsule.Encrypt. Cada condición lleva el texto y el código de Go, salvo las diferencias de la decisión 9. Ante cualquier fallo, output se aborta y nunca se cierra.
- Entradas: falta
profile,nowopolicy,unlockAtno es unInstantválido, o L no se puede fijar (decisiones 2 y 17):TypeError. - Copias de las entradas (decisión 1).
validateProfile(profile), con los textos y códigos dep.Validate().now(), una sola vez. SiunlockAtno es posterior:capsule: unlock time <RFC3339Nano> is not in the future, sin código.- Código: el de
padding, oREFORZADO.paddedLength(L, código)con los textos dePaddedLength: un código que no es 1 ni 2, o L > L_MAX. resolveDateKeycon los textos dedatekey.Resolve, yunlock = roundTime(round). Siunlockes anterior a lo pedido (§15):capsule: resolved round %d opens before the requested time→ERR_ROUND_MISMATCH. Es defensivo e inalcanzable, y llevav8 ignorejustificado.- Credenciales, como
accessRecipients(decisión 6). Con clave portable:I_ACCESSy su clave pública, añadida la última de la lista de credenciales. capsule_id(16 bytes) eI_PAYLOAD(32 bytes); deI_PAYLOADse derivaR_PAYLOAD.- En
time_and_key, los 16 huecos: las credenciales y un señuelo por hueco libre, cuyo escalar se borra al derivar su clave pública; después, la permutación (decisión 4). encodeHeader, que ya aplica el máximo de §57, ydecodeHeadercomo autocomprobación →capsule: self-check: the reader rejects this PUBLIC_HEADER: …, con el código del decoder.encodeControlcon binding e identidad a cero y los L, código y extensiones reales, en formato 2. De su longitud C, las fórmulas danSEALED_CONTROL_LEN(decisión 3). Si pasa de 64 MiB →capsule: SEALED_CONTROL of %d bytes exceeds 67108864→ERR_INTEGRITY, sin sellar nada. Este control provisional se borra en cuanto se mide: lleva L y las extensiones de control, ocultas hasta la fecha (§55.2).preludeBytes({ format: 2, … })yheaderBinding(prelude, header), sobre los bytes exactos (§26).encodeControlreal, ydecodeControl(…, FORMAT_2)como autocomprobación →capsule: self-check: the reader rejects this CONTROL_CBOR: …; la copia decodificada se borra en el acto. Su longitud es C.- Sellado:
- con
time_and_key, unEncryptercon los 16 recipients en su orden, sobre el control:INNER_ACCESS_AGE. Después, las autocomprobaciones de la decisión 8, con los textos deselfCheckInner; - en los dos casos, un
Encrypterdistinto contimeRecipientsolo, sobre el control o sobreINNER_ACCESS_AGE:OUTER_TIME_AGE. PasacheckTimeStanzas. Cadaencryptdeagegenera su propia file key, así que las tres son independientes (§62, MUST). Si la longitud no es la calculada →capsule: internal error: SEALED_CONTROL is %d bytes, measured %d. Se borran los bytes de CONTROL_CBOR y el control descifrado por la autocomprobación deINNER_ACCESS_AGE;I_PAYLOADsigue hasta el paso 15.
- con
PAYLOAD_AGE: unEncrypterconR_PAYLOAD, sobre la entrada troceada seguida del relleno (decisión 2). Su primer trozo es la cabeceraagesola:decryptHeaderconpayloadIdentity(I_PAYLOAD)la abre, y la file key que devuelve se borra (decisión 8). Después se borraI_PAYLOAD. Si falla, se cancelan el stream deagey la fuente.progress(0, total), contotal = 16 + |PUBLIC_HEADER| + SEALED_CONTROL_LEN + payloadAgeLength(P). Si lanza, no se escribe nada. Sinoutput, el buffer detotalbytes se reserva ahora.- Escritura de PRELUDE, PUBLIC_HEADER, SEALED_CONTROL, la cabecera de
PAYLOAD_AGEy el resto de sus trozos, cada uno por el hash antes de escribirse. Si la fuente da menos o más de L bytes, el error de la decisión 2. - Autocomprobación final: P bytes entregados a
ageypayloadAgeLength(P)escritos. Después se cierra la salida. .dkk: uncredential_idaleatorio yAccessKey { capsuleId, type: 'x25519', material: copia de I_ACCESS, verification: { capsuleDigest } }. El writer borra su copia deI_ACCESSen unfinally.
Si age-encryption falla durante un sellado, el error lleva un texto fijo y no tiene código normativo (decisión 9).
5. Piezas de apoyo
recipient.ts (nuevo, sin noble):
parseX25519Recipient(s): Uint8ArrayyformatX25519Recipient(raw): string, sobrebech32.ts(decisión 6);checkX25519Recipient(raw): 32 bytes, canónico y fuera de las cinco coordenadas u de orden bajo, con los textos de Go;parseRecipientList(text): el formato de un fichero-Rdeage, con errores por número de línea que nunca citan el contenido.
x25519.ts. Se añaden:
newX25519Identity(): Uint8Array: 32 bytes degetRandomValues, sin clamping guardado, comoage.GenerateX25519Identity;x25519PublicKey(identity): Uint8Array:x25519.getPublicKey.
Ninguna de estas funciones produce la forma AGE-SECRET-KEY-1… de una identidad.
digest.ts. sha256Hasher() devuelve { update(b), digest() } sobre sha256.create(). sha256Stream pasa a usarlo.
datekey.ts. compareInstants(a, b) compara segundos y después nanosegundos; checkInstant(t) valida la forma. open.ts usa compareInstants en lugar de su before, sin cambiar ningún test de la apertura.
writer.ts. Además del núcleo, randomIndex(n, draw) y permute(items, draw) (decisión 4), y las autocomprobaciones como funciones propias (decisión 8), probados aparte.
agefile.ts (nuevo, interno). decryptAll y readAll, que hoy son privados de open.ts, para que la apertura y el writer los compartan (decisión 8). open.ts no cambia de comportamiento, y lo comprueban sus tests actuales.
page-policy.ts (nuevo, sin dependencias) o datekey.ts: el umbral de 365 días del aviso de §53. No puede estar en encrypt.ts: la página lo importa de forma estática, y con él entrarían age-encryption y noble en su primera carga, que check-build prohíbe.
tempfile.ts. La raíz y el nombre del fichero pasan a ser parámetros: datekeys-open/…/plaintext para la apertura y datekeys-create/…/capsule para crear. Cada página limpia las dos raíces al cargarse.
tlock.ts no cambia: con la decisión 3 no hace falta la caché del pairing.
6. Escritura en streaming, salida y secretos
Orden y hash.
- Se escribe PRELUDE (16 bytes), luego PUBLIC_HEADER, luego SEALED_CONTROL y después el stream de
PAYLOAD_AGE: su cabecera, el nonce de 16 bytes y trozos de 65 552 bytes. El último es más corto, salvo cuando P es múltiplo de 65 536, lo que conreforzadopasa con todo L desde 2 MiB. - Cada trozo se pasa primero por el hash y después se escribe con
await writer.write(chunk). Esa espera da presión inversa, porque la entrada llega en trozos de 64 KiB. - Un test exige que el primer trozo del stream de
agesea exactamente su cabecera: que venga sola es un detalle interno deage-encryption0.3.1.
Contenido y relleno.
- La fuente troceada entrega sus bytes y se cuentan. Al llegar a L, se lee una vez más: si da algo, es el error "more than"; si termina antes de L, el error "ended after".
- Después siguen los P − L ceros, en trozos nuevos de 64 KiB, con las cotas de la sección 1.
agerecibe exactamente P bytes, y la autocomprobación final lo confirma con la longitud dePAYLOAD_AGE.
Confirmar o abortar.
- No se escribe nada antes del paso 17 del flujo: todas las comprobaciones, el sellado y la cabecera de
PAYLOAD_AGEvan antes. - La salida se cierra solo después del último trozo y de la autocomprobación final. Ante cualquier fallo, también un
TypeErrorde opciones o una fuente de longitud distinta de L, se aborta. - Con un fichero de OPFS escrito con
createWritable, abortar descarta el fichero swap, así que no se publica nada. - Sin
output, los trozos se copian en un buffer reservado contotal, que siempre se conoce.
Secretos.
I_PAYLOADvive desde que se genera hasta la comprobación de la cabecera dePAYLOAD_AGE(paso 15). Los bytes de CONTROL_CBOR se borran tras el sellado (paso 14), y cada copia que usa una autocomprobación, en cuanto termina: la decodificada del paso 13, el control descifrado deINNER_ACCESS_AGEy la file key de la cabecera dePAYLOAD_AGE. El payload solo necesitaR_PAYLOAD.- El escalar de cada señuelo se borra en cuanto se deriva su clave pública, y nunca sale del writer.
- La copia de
I_ACCESSdel writer se borra en unfinally, tanto si todo va bien como si falla. Solo la copia deportableKey.materialsale del writer. - El control provisional del paso 11 no lleva
I_PAYLOAD, pero sí L y las extensiones de control, ocultas hasta la fecha: se borra en cuanto se mide. - Los valores que fija la variante de tests pertenecen al writer y se borran igual; así los tests pueden comprobarlo, y pasan copias.
- Ningún mensaje de error contiene bytes de secretos, ni dice qué huecos son señuelos.
No se pueden borrar, y se documenta en el README ("Secretos"):
- las file keys, la stream key y el
plaintextBufferde 64 KiB deencryptSTREAM, que conserva una copia de CONTROL_CBOR conI_PAYLOADy, enPAYLOAD_AGE, los últimos 64 KiB del fichero de la persona, hasta que actúa el recolector de basura; - la efímera y el secreto compartido del
X25519Recipient; - los bigints en que
x25519.getPublicKeyde noble convierteI_PAYLOAD,I_ACCESSy los escalares de los señuelos; - los
CryptoKeyde Web Crypto.
7. Extensiones, registro y especificación
- Reglas de §54 en los encoders. Ya las aplican: como mucho 64 extensiones por array, en orden estricto por bytes UTF-8; ids de 1 a 256 bytes; versión de 0 a 2³² − 1;
datade 1 byte a 64 MiB; ningún id en los dos arrays de un mismo objeto. Un array vacío se omite (§58.1). - Ubicación (§72). La aplica la aplicación (decisión 10).
- Extensiones de la
.dkk(§44). El writer devuelve elAccessKeysin extensiones, como Go. - Especificación. Esta fase no necesita ningún cambio normativo: la v0.9 ya da las reglas del escritor, la fórmula de las longitudes y el rechazo de recipients no canónicos y de orden bajo, que la v1 proponía como cambios.
datekeys-go. No necesita cambios: ya escribe el formato 2 con las mismas reglas.
8. Tests
-
recipient.test.ts,x25519.test.ts,digest.test.tsy las piezas dewriter.ts.- El recipient de una identidad nueva coincide con el de
identityToRecipientdeage-encryptionpara la misma identidad, y con los vectores de RFC 7748. parseX25519RecipientrechazaAGE1…, mayúsculas mezcladas,age1pq1…,age1tag1…, 31 y 33 bytes y un checksum malo, y acepta todo lo que escribeformatX25519Recipient.checkX25519Recipientrechaza con los textos de Go el bit 255, u ≥ p (p y p + 1, entre otros) y las cinco coordenadas u de orden bajo. Un test comprueba con noble que esas cinco, y ninguna otra de una muestra, dan el secreto compartido cero.randomIndexrechaza exactamente los valores desde ⌊2³²/n⌋·n, con un generador fijado que los produce: esa es la guarda real contra el sesgo.permutees uniforme, comoTestStanzaOrderIsUniformde Go: 32 000 permutaciones de 16 elementos, las posiciones del primero y del último, y χ² ≤ 60 con 15 grados de libertad. CongetRandomValuesel test puede fallar por azar con probabilidad despreciable (del orden de 10⁻⁶); con un generador con semilla, que se imprime, es determinista. Las frecuencias por posición no detectan todo sesgo (una rotación aleatoria las pasa), y por eso cuenta la guarda anterior.- El hasher da lo mismo que Web Crypto con la entrada troceada de varias formas.
- Cobertura del 100 %.
- El recipient de una identidad nueva coincide con el de
-
Reproducción de los siete fixtures de formato 2, con la variante de tests. Para cada uno, con copias de los valores del registro (el writer los borra):
- su
capsule_id, supayload_identity, su L, su código y sus extensiones; - sus recipients, derivados de las identidades del registro, y la permutación que deja cada credencial en su hueco (
access_key_stanza,identity_stanzas); - su
I_ACCESSy sucredential_id, sacados de su.dkk; - su
unlock_at, connowen el génesis, y su contenido.
Resultados exigidos:
- PRELUDE (con
VERSION= 2), PUBLIC_HEADER,header_bindingy CONTROL_CBOR (con las claves 6 y 7) iguales byte a byte; SEALED_CONTROLyPAYLOAD_AGEde la misma longitud que en el fixture;- los argumentos del stanza tlock iguales;
- cada credencial abre el hueco que le da el registro, y los demás no los abre ninguna;
- en los fixtures con
.dkk, la escrita es igual aencodeAccessKeyde la del fixture con elcapsule_digestsustituido por el SHA-256 del.dkcescrito.
Es la comparación byte a byte con Go de todo lo que es determinista (§67, §68).
- su
-
Opciones inválidas. Los casos de
TestEncrypt,TestCredentialBoundsyTestEncryptSourceLengthde Go y los propios:- sin perfil, sin reloj, sin política, un
Instantmal formado; - un instante pasado, y uno igual a ahora;
time_onlycon recipients, y con clave portable;time_and_keysin credenciales, y con 17: 16 recipients y la clave portable, o 17 recipients;- un recipient de 31 bytes, uno con el bit 255, uno con u = p, uno de orden bajo, uno repetido;
- la política 7; el código de relleno 3;
- L = L_MAX + 1; un
lengthdistinto del tamaño de unBlob; unReadableStreamsinlength; - un chain hash con un bit cambiado;
- una extensión repetida en la cabecera, y una de control en los dos arrays.
Cada caso da el texto y el código de Go, o el
TypeErrorde la decisión 9, y la salida recibe cerowritey unabort. - sin perfil, sin reloj, sin política, un
-
Ida y vuelta TS → TS, con
open.- Credenciales:
time_only;time_and_keycon 1 y con 16 credenciales, con clave portable, con recipients y con las dos cosas. Cada credencial abre sola, y todas juntas también. Ningún señuelo coincide con una credencial. - Contenidos de 0, 1, 255, 256, 257, 8 192, 8 193, 65 535, 65 536, 65 537, 78 000, 320 000 y 5 000 000 bytes, con los dos códigos:
opendevuelve el contenido y da el formato, L, el código y P. El último es el deTestPaddingAcrossChunksde Go: P = 5 111 808, un relleno de varios trozos STREAM. Los tamaños redondos en MiB no sirven para eso, porque su relleno es cero. - Entrada como
Uint8Array, comoBloby comoReadableStreamconlength, con trozos irregulares y con un solo trozo de varios MiB. - Salida en memoria y en
WritableStream. - Extensiones críticas y no críticas en la cabecera y en el control, abiertas con un
ExtensionRegistryque las conoce; y en la.dkk, recodificada. - Rondas 1000, 1001 y 2000, con su release publicado, y un instante con nanosegundos: génesis + 2 997 s + 1 ns resuelve a la ronda 1001.
- Credenciales:
-
Propiedades de Go.
- Las claves portables nunca se repiten: dos cifrados dan material,
credential_idycapsule_iddistintos. I_PAYLOADnunca se repite: dos cápsulas con el mismo contenido tienenR_PAYLOADdistintos, comoTestPayloadIdentityReusede Go.- Los señuelos son nuevos en cada cápsula: dos cápsulas con la misma credencial dan 32 shares distintos, y sin clave portable no se devuelve ninguna clave, como
TestDummyRecipientsde Go. - La
.dkkde A sobre la cápsula B daERR_ACCESS_INVALIDen el paso 9, sin ninguna petición de release. La identidad cruda de A sobre B daERR_ACCESS_INVALIDen el paso 13, después del release. - Con
now + 1 h: pedido ≤unlockAt< pedido + periodo. Abrir con esenowdaERR_RELEASE_UNAVAILABLEsin ninguna petición, einspectda la misma DateKey. - Las fórmulas de longitud se cumplen en todas las muestras, con extensiones, y coinciden con un sellado provisional como el de Go (decisión 3).
SEALED_CONTROL_LENno depende de L ni del código: dos cápsulas con L distintas y las mismas extensiones tienen el mismo, como pide §55.2.
- Las claves portables nunca se repiten: dos cifrados dan material,
-
Fallos durante la escritura. Una salida que falla en el trozo k, una fuente que falla, una fuente que da L − 1 o L + 1 bytes y un
progressque lanza:- dejan la salida abortada y nunca cerrada;
- relanzan el error de la fuente o de la salida sin tocar y sin código, y dan los textos de Go en los de longitud;
- dejan a cero los secretos que fija la variante de tests, tanto si todo va bien como si falla;
- no ponen en ningún mensaje de error bytes de
I_PAYLOAD, deI_ACCESS, de un señuelo ni de CONTROL_CBOR, ni el orden de los huecos.
-
Bucle de propiedades, con el generador propio y la semilla impresa: 50 semillas en cada ejecución y 500 a mano (decisión 15). Varía:
- la política y de 0 a 17 credenciales, a veces repetidas, de 31 bytes, no canónicas o de orden bajo;
- la clave portable;
- de 0 a 65 extensiones por array, con ids de caracteres UTF-8 de 1 a 4 bytes, versiones cerca de 2³² − 1 y
datade 0 a unos KiB; - el código de relleno, y L alrededor de los bordes de trozo y de relleno, a veces con un relleno de varios trozos (L de varios MB que no sea redondo).
Cada caso termina de una de dos formas. O falla con un error esperado, sin escribir nada. O la cápsula pasa
inspect, se abre conopencon cada credencial, cumple las fórmulas y su.dkkse decodifica y se recodifica igual. Es el "encode implica decode" deFuzzEncodeImpliesDecodede Go. -
Mezclas generadas por el writer. Con dos cápsulas A y B de la misma ronda: la cabecera de A con el resto de B; el SEALED_CONTROL de B dentro de A; el
PAYLOAD_AGEde B dentro de A; una cabeceratime_onlycon el control de unatime_and_key. TypeScript da el código y el paso que registró Go en el punto 9. -
Interoperabilidad TS → Go a nivel de cápsula (decisión 11), con claves e instantes fijos.
Muestras, con contenidos generados de forma determinista (solo se guardan las cápsulas):
time_onlycon 0, 46, 65 536 y 78 000 bytes, con los dos códigos;time_and_keycon clave portable, con 3 recipients y clave portable, y con 16 recipients;- extensiones en los tres objetos;
- un instante con nanosegundos;
- las mezclas del punto 8.
Go, para cada muestra:
- ejecuta
capsule.Inspect; - la abre con
capsule.Openy con cada credencial, y registra el SHA-256 del contenido, el formato, L, el código, P y el número de stanzas deINNER_ACCESS_AGE; - vuelve a codificar PUBLIC_HEADER, CONTROL_CBOR (abierto capa a capa) y la
.dkk, y compara los bytes; - registra código y paso de cada mezcla.
Además, el script de Go:
- ejecuta un diferencial de encoders: 500 juegos de entradas aleatorias con semilla fija, con los bytes de
EncodeHeader,EncodeControl(…, Format2)yMarshalBodycomparados con los del writer TypeScript; - da los veredictos de
age.ParseX25519Recipientyagewrap.CheckX25519Recipientsobre el corpus de cadenas y claves; - da los textos de
capsule.Encryptpara la tabla del punto 3.
Todo se congela en
capsule-vectors.json. El test comprueba en cada ejecución los veredictos de Go y queopende TypeScript da lo mismo sobre los bytes congelados. -
Guardas de la sección 3.
-
Rendimiento, informativo. El tiempo de
encrypten las dos políticas y el caudal con 64 MiB y con 1 GiB, en Node y en el navegador, con salida a OPFS. -
Autocomprobaciones con entradas malas, como
TestEncryptSelfCheckde Go: unINNER_ACCESS_AGEde 15 stanzas, conI_ACCESSdos veces, con una clave que no es recipient o con otro control dentro; unPAYLOAD_AGEun byte corto, o para otro recipient; una cabecera o un control que el lector rechaza. Cada una da su texto. Solo la ronda que abriría antes de lo pedido llevav8 ignorejustificado.
La cobertura de los módulos nuevos se fija en el 100 %.
9. Página
Propuesta para el paso 6, confirmada por el autor el 29-09-2026 con cada recomendación (ver «Decidido en el paso 6», al final de la sección):
- Ruta y navegación.
/create, con el enlace "Crear" junto a "Inspector" en+layout.svelte.- El writer se carga con
import(), igual que la apertura. La lista de recipients se valida conrecipient.ts, que no trae noble.
- Qué pide.
- El fichero, arrastrado o elegido. Se lee con
File.stream()y nunca sale del dispositivo. Su tamaño es L. Su nombre no entra en la cápsula: no hay campo core para él. - Fecha y hora, con zona horaria.
- Por defecto, la zona del dispositivo. Un selector ofrece
Intl.supportedValuesOf('timeZone')y UTC. - Una hora que no existe en la zona (cambio a horario de verano) se rechaza con un mensaje. Una hora ambigua (cambio a horario de invierno) toma la más tardía de las dos, para no abrir nunca antes de lo que la persona pudo querer.
- El límite es 9999-12-31T23:59:57Z, la última ronda de Quicknet (§15).
- Por defecto, la zona del dispositivo. Un selector ofrece
- Política: "solo fecha" (
time_only) o "fecha y clave" (time_and_key). La página explica que, contime_only, cualquiera que tenga el.dkcpuede abrirlo desde la fecha. - Con
time_and_key:- recipients
age1…, uno por línea, con el formato de un fichero-Rdeagey validados línea a línea (decisión 6): como mucho 15 con la clave portable y 16 sin ella; - la casilla "generar una clave portable (.dkk)", marcada por defecto y obligatoria si no hay recipients.
El relleno es siempre
reforzado(decisión 16), y la página no lo pregunta.
- recipients
- El fichero, arrastrado o elegido. Se lee con
- Qué muestra antes de cifrar.
- El instante pedido, en UTC y en la zona elegida.
- La ronda, el instante efectivo (el de la ronda, igual o posterior al pedido, §15) y la
dk1_: es el SHOULD de §62.1, regla 2. - La hora del dispositivo junto a la hora UTC. Con el reloj atrasado se podría sellar sin aviso hacia una ronda ya publicada, que cualquiera con el
.dkcabriría enseguida. La página no pregunta la hora a ningún servidor. - El tamaño que tendrá el
.dkc, y qué deja ver hasta la fecha (§55.2):- la fecha, la política, el
capsule_idy ese tamaño, que da el del fichero de forma aproximada: con un margen de 256 bytes hasta 8 KiB, y de menos de un 6,25 % por encima; - no deja ver el tamaño exacto ni cuántas credenciales abren la cápsula, que no es lo mismo que cuántas personas. Ocultar las credenciales se apoya en Diffie–Hellman, que no resiste un adversario cuántico futuro, como el resto del cifrado (§53).
- la fecha, la política, el
- El aviso de §53 cuando el instante efectivo está a más del umbral, antes de cifrar. La librería exporta el umbral, 365 días, porque §53 pide el aviso al SDK, desde un módulo sin dependencias (sección 5).
- Que la cápsula no guarda la zona horaria, y que se fija un instante UTC con las reglas de zona de hoy.
- "Ahora" se vuelve a leer al pulsar el botón y al volver a la pestaña.
- Qué entrega.
- El
.dkcy, si la hay, la.dkk, en dos descargas separadas. - Después, el informe de los pasos 1 a 8 del
.dkcescrito, con el componente del inspector (formato 2), y elcapsule_id. - Los nombres de los ficheros son un metadato público. Por defecto,
capsula-<fecha UTC de apertura>.dkcy.dkk, que ya es pública en la DateKey y no revela cuándo se creó; la persona puede editarlos.
- El
- Dónde queda la salida.
- El
.dkcse escribe en un fichero temporal de OPFS (datekeys-create, sección 5), con la política de borrado de la apertura. - La cuota se comprueba en la llamada a
progressconwritten = 0, con el tamaño exacto. - Sin OPFS, o si el navegador lo rechaza, se escribe en memoria, hasta 64 MiB.
- Cancelar usa
cancellable: la salida deja de aceptar datos,encryptfalla y el fichero temporal se borra. - La
.dkk(152 bytes sin extensiones) queda solo en memoria, nunca en OPFS. Sus bytes se borran al pulsar "olvidar la clave", al crear otra cápsula y enpagehide. - La URL
blob:de cada descarga se revoca pasado un plazo, no en el mismo clic. - Si la cápsula solo se abre con su
.dkky la persona intenta salir sin haberla descargado, la página avisa.
- El
- Avisos.
- §53: "Aviso: el cifrado por tiempo de Quicknet V1 no es poscuántico. El texto cifrado puede seguir guardado durante años, y su confidencialidad futura depende del proveedor y de la criptografía en que se basa."
- §50, con el mismo umbral: "Para abrirla hará falta el release de su ronda, que publica la red drand. Si en esa fecha ningún relay ni ninguna copia conservada lo ofrece, la cápsula no podrá abrirse."
- §7.4, junto a la
.dkk: "Guarda esta clave en secreto: quien la tenga podrá abrir la cápsula desde la fecha. Solo sirve para esta cápsula." - §36.1 y §55.1: ningún texto presenta la cápsula como prueba de autoría ni de fecha de creación.
- Sin red. La ronda se calcula en el dispositivo, y tlock usa solo la clave pública pinneada (§35). La CSP no cambia.
- Rendimiento. Todo corre en el hilo principal, porque
worker-src 'none'no permite workers. Barra de progreso conprogress, y botón de cancelar. - Convenciones de las páginas actuales. Textos en español, nunca
{@html}, 375 px de ancho, el foco al campo o al mensaje de error, ylicenses.txt.
Queda para que el autor decida en el paso 6:
- el nombre de la ruta;
- la política por defecto;
- el selector de zona, o solo la zona del dispositivo;
- la regla para horas ambiguas;
- los nombres de los ficheros;
- si se muestra la
.dkkcomoAGE-SECRET-KEY-1…(§38 MAY). La recomendación es no hacerlo por defecto, por el historial del portapapeles; - el umbral y los textos de §53 y §50;
- un aviso de protocolo preliminar (v0.9; los esquemas pueden cambiar antes de la v1.0, §74);
- un tamaño máximo más allá de la cuota;
- un aviso para fechas muy cercanas;
- que las extensiones y el código de relleno no se exponen en esta fase.
Decidido en el paso 6 (29-09-2026). El autor confirma la propuesta con cada recomendación:
- ruta
/create, con el enlace "Crear" junto a "Inspector"; - política por defecto "solo fecha" (
time_only): un único fichero y nada secreto que perder, con "fecha y clave" a un clic; - zona del dispositivo por defecto, con el selector de todas las zonas y UTC;
- una hora que no existe se rechaza con un mensaje, y de una hora ambigua se toma la más tardía;
- ficheros
capsula-<fecha UTC de apertura>.dkcy.dkk, editables; - la
.dkknunca se muestra comoAGE-SECRET-KEY-1…; - los avisos de §53 y §50 desde 365 días hasta el instante efectivo, con los textos de arriba, y el de §7.4 junto a la
.dkk; - un aviso breve de protocolo preliminar (v0.9, §74) antes de cifrar;
- sin tamaño máximo propio: manda la cuota de OPFS, y 64 MiB en memoria sin OPFS;
- un aviso informativo cuando el instante efectivo está a menos de una hora;
- relleno siempre
reforzado, sin extensiones en esta fase.
10. Orden de trabajo y criterios de aceptación
| Paso | Contenido | Hecho cuando |
|---|---|---|
| 0 | Confirmar las decisiones de la sección 2, y que no hace falta ninguna dependencia nueva (sección 3) | Hecho el 29-09-2026: todas confirmadas con su recomendación; el plan pasa a v3 |
| 1 | Precondición | main en verde, con testdata en 7e2d83c (spec-v0.9): ya se cumple en 0118890.La decisión 14 está aplicada; si se cierra 0.1.0, su commit y su tag van antes del paso 2.Hecho el 29-09-2026: 0.1.0 cerrada en d577283, con el tag v0.1.0, y 0.2.0-dev abierta en 8c08c97 |
| 2 | Piezas de apoyo (sección 5) y guardas nuevas (decisión 13) | Derivación del recipient igual a la de identityToRecipient y a RFC 7748.parseX25519Recipient y checkX25519Recipient rechazan los casos de la sección 8, punto 1, con los textos de Go.permute pasa la prueba de uniformidad.El hasher da lo mismo que Web Crypto. open.ts usa compareInstants sin cambiar ningún test.tempfile.ts parametrizado, con la apertura intacta.Guardas nuevas en verde. Módulos tocados al 100 %. Hecho el 29-09-2026 ( 8838c7d): recipient.ts, random.ts y agefile.ts, con los añadidos a x25519.ts, digest.ts, datekey.ts y tempfile.ts; los tres módulos nuevos, al 100 % y fijado como umbral |
| 3 | encrypt.ts y writer.ts con entrada en memoria (sección 4) |
La tabla de opciones inválidas da los textos y códigos esperados, sin escribir nada y con la salida abortada. Las secciones deterministas de los siete fixtures de formato 2 salen byte a byte, y cada credencial abre su hueco (sección 8, punto 2). Ida y vuelta con open para las dos políticas, de 1 a 16 credenciales y los dos códigos.Claves portables nunca repetidas; caso now + 1 h.Los dos módulos al 100 %, fijado como umbral. Hecho el 29-09-2026, junto con el paso 4 ( 8473fdf) |
| 4 | Streaming y salida (sección 6) | Uint8Array, Blob y ReadableStream dan el mismo contenido al abrir y las mismas longitudes, también con un trozo de entrada de varios MiB.Nada se escribe antes del paso 17 del flujo. Una salida o una fuente que fallan, una fuente de L ± 1 bytes y un progress que lanza dejan la salida abortada y nunca cerrada.capsule_digest es el SHA-256 de lo escrito.El bucle de propiedades pasa con 50 semillas, y con 500 a mano. Las mezclas dan el código y el paso esperados. Medida de rendimiento en Node, anotada en el README. Hecho el 29-09-2026 ( 8473fdf): 500 semillas del bucle de propiedades en 203 s; en Node 24.9, unos 120 ms por cápsula time_only y 50 MiB/s desde un Blob |
| 5 | Interoperabilidad TS → Go a nivel de cápsula (sección 8, punto 9) | Go inspecciona y abre todas las muestras con cada credencial, con el mismo SHA-256 del contenido y los mismos L, código y P. Go recodifica PUBLIC_HEADER, CONTROL_CBOR y .dkk a los mismos bytes.El diferencial de encoders no da ninguna diferencia. Las mezclas dan el mismo código y paso en Go y en TypeScript. Los veredictos de recipients y los textos de opciones coinciden, salvo los TypeError de la decisión 9.Todo congelado en capsule-vectors.json.README con la fila "Equivale en Go" de encrypt.ts (capsule.Encrypt, accesskey.Encode).Hecho el 29-09-2026 ( da0b39f): trece muestras, cuatro mezclas, 500 entradas de los codificadores, 22 recipients y 21 opciones inválidas, sin ninguna diferencia con Go |
| 6 | Decisiones de la página (sección 9) | el autor confirma la ruta, las entradas, los avisos y sus textos, la salida y la privacidad. Hecho el 29-09-2026: todas con su recomendación (sección 9, «Decidido en el paso 6») |
| 7 | Página | En la compilación de producción, un fichero propio se cifra a .dkc y .dkk sin ninguna petición fuera del origen, y el informe de los pasos 1 a 8 del .dkc escrito pasa, con formato 2.Una cápsula creada para dentro de dos o tres minutos se abre después en /inspect con el release pegado, y a mano con datekeys decrypt de Go, con red y con su .dkk.El aviso de §53 aparece antes de cifrar, solo pasado el umbral. Cancelar a mitad no deja fichero temporal. Sin OPFS, la escritura va a memoria. 375 px de ancho. check-build generalizado, en verde.Tamaño del bundle de la página anotado. Módulos nuevos al 100 %. Revisión adversarial con cada hallazgo contrastado. Hecho el 29-09-2026 ( 3a9d2b1): todo lo anterior, comprobado en Chromium sobre la compilación de producción; lengths.ts da el tamaño antes de escribir. La revisión adversarial encontró un fallo mayor (la .dkk única se borraba sin confirmación) y ocho menores, todos contrastados y corregidos |
Cada paso termina con npm run verify en verde y un commit en Gitea, y actualiza README, CHANGELOG, HANDOFF y la fila de esta tabla. El paso 6 puede ir en paralelo con los pasos 2 a 5. El script de Go del paso 5 puede empezarse durante el paso 4.
11. Riesgos
age-encryptionno tiene labels y no exige un mínimo de recipients. Mitigación: el recipient tlock va solo, en unEncrypterpropio; el writer comprueba las credenciales antes de sellar;checkTimeStanzasycheckAccessStanzas(…, 16)se aplican sobre lo sellado (decisión 8).addRecipientacepta recipients que no son X25519. Mitigación: la API recibe bytes, y aaddRecipientsolo llegan cadenas deformatX25519Recipient.- Recipients que nadie puede abrir. Mitigación: los no canónicos y los de orden bajo se rechazan antes de cifrar, como en Go (decisión 6). Un punto del twist se acepta, como en Go, porque §37 lo deja en MAY: si fuera la única credencial, la cápsula no se abriría nunca.
- Un orden de huecos sesgado o que se filtra. Mitigación: índice por rechazo y Fisher–Yates, con su prueba de uniformidad; ni el orden ni los señuelos salen del writer (decisión 4).
- La clave privada de un señuelo. Si se guardara, abriría la cápsula (§39). Mitigación: se borra al derivar su clave pública y no pasa por ninguna cadena ni error; sus bigints en noble no se pueden borrar (sección 6).
- Una fuente que no da exactamente L bytes. Solo se descubre al leerla, con parte de la cápsula ya escrita. Mitigación: la salida se aborta y el error lo dice (decisión 2); con ficheros no puede pasar.
- Un relleno omitido o mal calculado. Revelaría L y la cápsula fallaría en el paso 17, tras la fecha. Mitigación:
paddedLengthexacta, con sus vectores; la autocomprobación de la longitud dePAYLOAD_AGEantes de cerrar la salida (decisión 8). - Memoria con trozos grandes. Mitigación: la entrada se trocea en 64 KiB, y un test lo cubre con un trozo de varios MiB.
- Un cambio de formato en una versión futura de
age-encryption. La fórmula dejaría de medir lo mismo que el sellado real, o su primer trozo dejaría de ser la cabecera sola. Mitigación: versión exacta; la comprobación de igualdad de longitudes, que da un error interno y nunca una cápsula mal enmarcada; los tests de fórmulas, de fixtures y del primer trozo. - Entradas que cambian durante una espera. Mitigación: copias al empezar (decisión 1).
- Esquemas provisionales (§74). Una cápsula escrita hoy con horizonte largo podría no abrirse con un lector v1.0 si los esquemas cambian. Mitigación: el aviso de protocolo preliminar de la sección 9.
- Secretos que no se pueden borrar, dentro de
age-encryption, en los bigints de noble y en elBlobde la descarga. Mitigación: documentarlos, reducir al mínimo las copias y no usar nunca cadenas para secretos. - Rendimiento en el hilo principal. En Node, 57 MiB/s con el hash, más el relleno. Mitigación: streaming con presión inversa, barra de progreso y cancelación; medir en el paso 7 con 64 MiB y con 1 GiB.
- Pérdida de la
.dkkde una cápsula que solo se abre con ella. Mitigación: la página lo advierte y pide confirmación antes de descartarla. - El reloj del dispositivo. Mitigación: mostrar la hora del dispositivo junto a UTC y el instante efectivo, y exigir, como Go, que el instante sea futuro según ese reloj.
- Zonas horarias. Mitigación: mostrar el instante UTC y explicar que es el que cuenta; reglas explícitas para horas que no existen o son ambiguas.
- Muestras congeladas que dejan de reflejar el writer. Mitigación: el test de reproducción de fixtures detecta cualquier cambio en las secciones deterministas, y la cabecera del script dice cuándo se regeneran.
- Afirmaciones de autoría en la interfaz (§55.1, MUST NOT). Mitigación: revisar los textos en el paso 7.
12. Fuera de alcance y decisiones aplazadas
- Escribir el formato 1: solo lo hace el generador de vectores de Go (§62.1, regla 1).
- La fuente drand del SDK, para §48 y §49. Escribir no la necesita.
- Los schemes de drand distintos de Quicknet, y la Release API.
- Extensiones en la página, y el registro de
extension_id(§72, MAY). - Una extensión de nombre de fichero o de tipo MIME.
- Extensiones de firma y de autoría (§36.1).
- La entrega y el almacenamiento de cápsulas y
.dkk(§6). - Exportar
I_ACCESScomoAGE-SECRET-KEY-1…, salvo que el autor lo decida en la sección 9. - El zip y los códigos QR.
- Workers (lo impide la CSP).
- Volcar a un fichero temporal un
ReadableStreamsin longitud (decisión 17). - Añadir recipients a una cápsula ya escrita, o rotar sus claves: requiere una cápsula nueva.
- Cambios normativos y cambios en
datekeys-go: ninguno es necesario (sección 7). - Una
AbortSignalen la API y un registro de extensiones en el writer: aplazados.