diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a13df..23b132a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Cambios notables de la librería TypeScript y de la página. El proyecto usa versionado semántico; mientras sea 0.x, no hay promesa de estabilidad. La sección «Versiones» del [README](README.md) explica qué cubre cada número. +## 0.6.0 — sin publicar + +- **El recordatorio en el calendario** (§62.1, regla 26, MAY), como `encrypt -reminder` de `datekeys-go` (`7e6a03d`): tras crear la cápsula, «Descargar el recordatorio» da `.recordatorio.ics`, un evento de iCalendar (RFC 5545) a la hora de la ronda, con una alarma y lo que hará falta para abrirla. `reminder.ts` escribe los mismos bytes que Go, y su prueba los compara con un texto calculado aparte, a partir del RFC: CRLF, líneas plegadas a 75 octetos sin cortar un carácter y el nombre escapado. El UID es un UUID al azar, que no nombra la cápsula; la página dice que un calendario que se sincroniza con un servidor sabrá el nombre y la fecha. + ## 0.5.0 — 7 de octubre de 2026 La especificación 0.16, el tag `spec-v0.16` de `datekeys-go`: la revisión de Astra de la v0.15, con el sello sin `accuracy` y el JSON de drand estricto; y las palabras al azar de la llave de palabras, de una lista inglesa y una española, con el ordenador o con dados, y lo que dice el SDK oficial al sellar. El autor la cerró el 7 de octubre de 2026, con el tag `v0.5.0`. diff --git a/README.md b/README.md index ca32d51..c6aa994 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Hay tres números de versión, cada uno con su significado, como en la referenci `version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos. +En desarrollo está la `0.6.0` (`0.6.0-dev` en `package.json`): el recordatorio en el calendario de `/create` (§62.1, regla 26). Lo que cambia está en el [CHANGELOG](CHANGELOG.md). + La versión actual es la `0.5.0`, del 7 de octubre de 2026, con el tag `v0.5.0`. Cubre: - la especificación 0.16 (el tag `spec-v0.16` de `datekeys-go`): lee los formatos de cápsula 1 a 3 y escribe el 3, y el 2 solo como generador de vectores (§62.1, regla 1); - un sello sin `accuracy` no prueba nada antes de la fecha de apertura: los veredictos dicen por qué, y el escritor devuelve los del área que escribe (§29.7, §29.11, §62.1 regla 19); @@ -232,6 +234,7 @@ Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una | `src/lib/inspector/create-files.ts` | La lista de ficheros de `/create`, sin tablas: lo elegido y lo soltado, carpetas recorridas incluidas, los ficheros de un sistema fuera por defecto como en `collect.go`, las rutas editadas y lo que la cápsula guarda | | `src/lib/inspector/create-check.ts` | Las reglas de las rutas y de los textos (§29.5, §29.6) como las explica `/create`: todos los problemas de todas las rutas, en español, con los de `pathrule.ts`. Se carga bajo demanda, con las tablas | | `src/lib/inspector/create-input.ts` | El formulario de `/create`, sin DOM, reloj ni writer: `planCapsule` comprueba los campos en su orden y da lo que se muestra antes de cifrar, con los ficheros medidos una vez (`chooseFiles`); los destinatarios y los nombres de los ficheros | +| `src/lib/inspector/reminder.ts` | El recordatorio que `/create` ofrece junto a la cápsula (§62.1, regla 26, MAY): un evento de iCalendar (RFC 5545) a la hora de la ronda, con una alarma y lo que hará falta para abrirla, byte a byte como `encrypt -reminder` de Go; el nombre del fichero, el de la cápsula con `.recordatorio.ics` | | `src/lib/inspector/annex.ts` | El anexo de recuperación que `/create` ofrece junto a la cápsula: `annex/recovery.md`, que Vite publica con un nombre con hash, y el nombre de su fichero, el de la cápsula con `.recuperacion.txt`, como `RecoveryAnnexSuffix` de Go | | `src/lib/inspector/create-words.ts` | La lista de las palabras al azar de `/create`: `wordListLoader` descarga una vez `wordlists/es.txt`, que Vite publica con un nombre con hash, y la lee con `readWordList`, con su SHA-256 fijado; un fallo no se guarda y la siguiente llamada lo vuelve a intentar | | `src/lib/inspector/creator.ts` | La escritura, cargada bajo demanda: `encryptFiles` con los ficheros, el comentario y el autor hacia el fichero temporal o la memoria, con el progreso de las dos lecturas, la cancelación y la cuota, y los pasos 1 a 8 de lo escrito | diff --git a/package-lock.json b/package-lock.json index b9d9ec2..94958b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "datekeys-ts", - "version": "0.5.0", + "version": "0.6.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datekeys-ts", - "version": "0.5.0", + "version": "0.6.0-dev", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "2.4.0", diff --git a/package.json b/package.json index 3d9ba1c..e8f0a4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "datekeys-ts", - "version": "0.5.0", + "version": "0.6.0-dev", "private": true, "description": "DateKeys in TypeScript: canonical CBOR codec, DKC1/DKK1 parsers, capsule inspector library and its static inspector page; later, browser encryption and decryption.", "license": "Apache-2.0", diff --git a/src/lib/dkc/version.ts b/src/lib/dkc/version.ts index dbac856..412e305 100644 --- a/src/lib/dkc/version.ts +++ b/src/lib/dkc/version.ts @@ -12,9 +12,10 @@ * 0.4.0 implements spec 0.15: the release object and a release in hand; * 0.5.0 implements spec 0.16: a seal without accuracy proves nothing before * the opening date and drand's JSON is read strictly; it also draws the - * random words of a key of words, from a computer or from dice. + * random words of a key of words, from a computer or from dice; 0.6.0-dev is + * what comes after it. */ -export const VERSION = '0.5.0'; +export const VERSION = '0.6.0-dev'; /** * The version of the DateKeys Protocol Specification that this library diff --git a/src/lib/inspector/reminder.test.ts b/src/lib/inspector/reminder.test.ts new file mode 100644 index 0000000..119d1fd --- /dev/null +++ b/src/lib/inspector/reminder.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import { REMINDER_SUFFIX, newUID, reminderICS, reminderName } from './reminder.ts'; + +// The calendar file of the reminder of /create, byte for byte the one of +// datekeys encrypt -reminder (cmd/datekeys/reminder_test.go of datekeys-go): +// CRLF, lines folded at 75 octets without cutting a character, and a name +// whose semicolon, comma and backslash are escaped (spec §62.1 rule 26, RFC +// 5545). The expected text was computed apart from this code, from the RFC. +describe('reminderICS', () => { + const base = { + name: 'carta; de, mamá\\.dkc', + round: 1000, + unlockMs: Date.UTC(2023, 7, 23, 15, 59, 24), + timeAndKey: true, + uid: '0f1e2d3c-4b5a-4697-8877-665544332211', + stampMs: Date.UTC(2026, 9, 7, 12, 0, 0), + }; + + it('writes the event of Go byte for byte', () => { + expect(reminderICS(base)).toBe( + 'BEGIN:VCALENDAR\r\n' + + 'VERSION:2.0\r\n' + + 'PRODID:-//DateKeys//datekeys-go//ES\r\n' + + 'CALSCALE:GREGORIAN\r\n' + + 'BEGIN:VEVENT\r\n' + + 'UID:0f1e2d3c-4b5a-4697-8877-665544332211\r\n' + + 'DTSTAMP:20261007T120000Z\r\n' + + 'DTSTART:20230823T155924Z\r\n' + + 'DTEND:20230823T162924Z\r\n' + + 'SUMMARY:Ya se puede abrir la cápsula DateKeys «carta\\; de\\, mamá\\\\.dkc»\r\n' + + 'DESCRIPTION:Desde este momento se puede abrir «carta\\; de\\, mamá\\\\.dkc».\r\n' + + ' Hace falta el fichero .dkc y una de sus llaves\\, y la firma de drand de l\r\n' + + ' a ronda 1000\\, que drand publica ahora: si un día ya no la sirve\\, un arc\r\n' + + ' hivo de firmas o un servicio de caché tiene que haberla guardado. Las ins\r\n' + + ' trucciones para abrirla sin DateKeys están en «carta\\; de\\, mamá\\\\.dkc.\r\n' + + ' recuperacion.txt».\r\n' + + 'TRANSP:TRANSPARENT\r\n' + + 'BEGIN:VALARM\r\n' + + 'ACTION:DISPLAY\r\n' + + 'TRIGGER:PT0S\r\n' + + 'DESCRIPTION:Ya se puede abrir la cápsula DateKeys «carta\\; de\\, mamá\\\\.d\r\n' + + ' kc»\r\n' + + 'END:VALARM\r\n' + + 'END:VEVENT\r\n' + + 'END:VCALENDAR\r\n', + ); + }); + + it('folds every line at 75 octets', () => { + for (const line of reminderICS(base).split('\r\n')) { + expect(new TextEncoder().encode(line).length).toBeLessThanOrEqual(75); + } + }); + + it('asks for a credential only with a key', () => { + const ics = reminderICS({ ...base, timeAndKey: false, name: 'carta.dkc' }); + // Unfolded, a line of RFC 5545 is its pieces without the CRLF and the space. + expect(ics.replaceAll('\r\n ', '')).toContain('Hace falta el fichero .dkc\\, y la firma'); + expect(ics).not.toContain('una de sus llaves'); + expect(ics).toContain('DTEND:20230823T162924Z\r\n'); + }); + + it('escapes a line feed and drops a carriage return of a name', () => { + const ics = reminderICS({ ...base, name: 'a\r\nb.dkc' }); + expect(ics).toContain('SUMMARY:Ya se puede abrir la cápsula DateKeys «a\\nb.dkc»\r\n'); + }); + + it('folds before a character that the 75th octet would cut', () => { + // SUMMARY and the start of the text take 49 octets: with 25 more, ñ + // takes octets 75 and 76, and the line folds before it, at 74. + const ics = reminderICS({ ...base, name: `${'x'.repeat(25)}ñ.dkc` }); + expect(ics).toContain(`SUMMARY:Ya se puede abrir la cápsula DateKeys «${'x'.repeat(25)}\r\n ñ.dkc»\r\n`); + }); + + it('names the file after the capsule', () => { + expect(reminderName('carta.dkc')).toBe('carta.dkc.recordatorio.ics'); + expect(REMINDER_SUFFIX).toBe('.recordatorio.ics'); + }); + + it('makes a random UUID of version 4', () => { + const a = newUID(); + expect(a).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + expect(newUID()).not.toBe(a); + }); +}); diff --git a/src/lib/inspector/reminder.ts b/src/lib/inspector/reminder.ts new file mode 100644 index 0000000..03b78d9 --- /dev/null +++ b/src/lib/inspector/reminder.ts @@ -0,0 +1,100 @@ +// The local reminder that /create offers next to a capsule, the MAY of spec +// §62.1 rule 26: an iCalendar event (RFC 5545) at round_time, which any +// calendar imports, with what opening the capsule will take. It is +// cmd/datekeys/reminder.go of datekeys-go, byte for byte. It holds the name +// of the capsule and its date, and nothing secret; a calendar that syncs +// with a server learns both. + +/** What is appended to the name of the .dkc to name its reminder: carta.dkc.recordatorio.ics. */ +export const REMINDER_SUFFIX = '.recordatorio.ics'; + +/** The name of the reminder of the capsule saved as `dkcName`. */ +export function reminderName(dkcName: string): string { + return `${dkcName}${REMINDER_SUFFIX}`; +} + +/** A reminder: the capsule, its round and round_time, and the event. */ +export interface Reminder { + /** The file name of the capsule. */ + readonly name: string; + readonly round: number; + /** round_time, in milliseconds since the epoch. */ + readonly unlockMs: number; + /** Whether opening needs one of its credentials too. */ + readonly timeAndKey: boolean; + /** A random UUID, so that the event names no capsule. */ + readonly uid: string; + /** When the event was made, in milliseconds since the epoch. */ + readonly stampMs: number; +} + +/** A random UUID of version 4 (RFC 9562). */ +export function newUID(): string { + const b = crypto.getRandomValues(new Uint8Array(16)); + b[6] = (b[6]! & 0x0f) | 0x40; + b[8] = (b[8]! & 0x3f) | 0x80; + const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join(''); + return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`; +} + +// A TEXT value of RFC 5545 (3.3.11): a backslash, a semicolon, a comma and +// a line feed escaped; a carriage return goes. +function calendarText(s: string): string { + return s.replace(/[\\;,\n\r]/g, (c) => (c === '\n' ? '\\n' : c === '\r' ? '' : `\\${c}`)); +} + +// A DATE-TIME of RFC 5545 in UTC, with a Z: 20230823T155924Z. +function calendarTime(ms: number): string { + return new Date(ms).toISOString().replace(/\.\d+Z$/, 'Z').replace(/[-:]/g, ''); +} + +// A content line ended with CRLF and folded at 75 octets (RFC 5545, 3.1): +// each continuation starts with a space, and no UTF-8 sequence is cut. +function foldLine(line: string): string { + let rest = new TextEncoder().encode(line); + const parts: Uint8Array[] = []; + let limit = 75; + while (rest.length > limit) { + let cut = limit; + while (cut > 0 && (rest[cut]! & 0xc0) === 0x80) cut--; + parts.push(rest.subarray(0, cut)); + rest = rest.subarray(cut); + limit = 74; + } + parts.push(rest); + const decoder = new TextDecoder(); + return `${parts.map((p) => decoder.decode(p)).join('\r\n ')}\r\n`; +} + +/** The calendar file of the reminder, in Spanish, as the annex is. */ +export function reminderICS(r: Reminder): string { + const summary = `Ya se puede abrir la cápsula DateKeys «${r.name}»`; + const needs = r.timeAndKey ? 'el fichero .dkc y una de sus llaves' : 'el fichero .dkc'; + const description = + `Desde este momento se puede abrir «${r.name}». Hace falta ${needs}, y la firma de drand de la ronda ${r.round}, ` + + 'que drand publica ahora: si un día ya no la sirve, un archivo de firmas o un servicio de caché tiene que haberla guardado. ' + + `Las instrucciones para abrirla sin DateKeys están en «${r.name}.recuperacion.txt».`; + return [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//DateKeys//datekeys-go//ES', + 'CALSCALE:GREGORIAN', + 'BEGIN:VEVENT', + `UID:${r.uid}`, + `DTSTAMP:${calendarTime(r.stampMs)}`, + `DTSTART:${calendarTime(r.unlockMs)}`, + `DTEND:${calendarTime(r.unlockMs + 30 * 60 * 1000)}`, + `SUMMARY:${calendarText(summary)}`, + `DESCRIPTION:${calendarText(description)}`, + 'TRANSP:TRANSPARENT', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'TRIGGER:PT0S', + `DESCRIPTION:${calendarText(summary)}`, + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ] + .map(foldLine) + .join(''); +} diff --git a/src/routes/create/+page.svelte b/src/routes/create/+page.svelte index e01160c..09c031d 100644 --- a/src/routes/create/+page.svelte +++ b/src/routes/create/+page.svelte @@ -44,6 +44,7 @@ } from '$lib/inspector/create-input.ts'; import { wordListLoader, WORDS_LANGUAGE } from '$lib/inspector/create-words.ts'; import { ANNEX_URL, annexName } from '$lib/inspector/annex.ts'; + import { newUID, reminderICS, reminderName } from '$lib/inspector/reminder.ts'; import { escapeInvisible, formatByteCount, formatDateTime, formatInteger, formatRelative, unexpectedProblem, viewerTimeZone } from '$lib/inspector/format.ts'; import { isTimeZone, localParts, supportedTimeZones, timeZoneList, UTC } from '$lib/inspector/localtime.ts'; import { buildReport, type Report } from '$lib/inspector/report.ts'; @@ -438,6 +439,23 @@ return url; } + // The local reminder of spec §62.1 rule 26 (MAY): a calendar event at the + // round time, with what opening will take. Its UID is random, so that it + // names no capsule. + function saveReminder(): void { + if (result === undefined) return; + const name = downloadName(dkcName, '.dkc', result.plan.names.dkc); + const ics = reminderICS({ + name, + round: result.plan.dateKey.round, + unlockMs: result.plan.effectiveMs, + timeAndKey: result.plan.policy === TIME_AND_KEY, + uid: newUID(), + stampMs: Date.now(), + }); + save(new Blob([ics], { type: 'text/calendar' }), reminderName(name)); + } + function saveCapsule(): void { if (result === undefined || capsuleGone) return; save(result.capsule, downloadName(dkcName, '.dkc', result.plan.names.dkc)); @@ -1417,10 +1435,12 @@

Guarda con la cápsula las instrucciones para abrirla sin DateKeys, por si ya no existe. Son las mismas para toda cápsula y - no dicen nada de la tuya. + no dicen nada de la tuya. El recordatorio es un evento para tu calendario a esa hora, con el nombre de la cápsula: si el + calendario se sincroniza con un servidor, este sabrá el nombre y la fecha.

Descargar las instrucciones +
diff --git a/vitest.config.ts b/vitest.config.ts index 404e7eb..849fecb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -51,6 +51,7 @@ export default defineConfig({ 'src/lib/inspector/create-input.ts': { 100: true }, 'src/lib/inspector/create-words.ts': { 100: true }, 'src/lib/inspector/annex.ts': { 100: true }, + 'src/lib/inspector/reminder.ts': { 100: true }, 'src/lib/inspector/creator.ts': { 100: true }, // Format 3 (plan of format 3 in datekeys-ts): the rules of the paths // and texts of the head, the codec of BODY, of the security area and of