The English list of the EFF, pinned; testdata and wordlists at e671032

testdata and wordlists at datekeys-go e671032, with the same testdata:
wordlists/en.txt is the large wordlist of the EFF, 7776 words, CC BY 4.0,
in its order and without the dice numbers. WORD_LIST_SHA256 pins it, and
the alphabet of en is a to z and the hyphen of its four compound words.
/create still offers the Spanish list, and check-build.mjs wants the site
to ship that one alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 11 hours ago
parent 4057607e94
commit 13df99b4db

@ -12,6 +12,7 @@ El SHOULD de §38.1, ofrecer palabras al azar de una lista pública, como hace `
- **`wordlist.ts`**, como `wordkey.Generate`, `CheckList` y `Bits` de Go en `27a75ee`, con sus textos: `generateWords` sortea palabras distintas, 7 por defecto, con `crypto.getRandomValues`; `wordBits` da su fuerza, 90 bits para 7 de 7 776; `checkWordList` rechaza una lista de menos de 2 048 palabras, con dos que son una al normalizarlas o con un carácter que no es una letra del alfabeto de su idioma, que da el código y no la lista (para `es`, de la `a` a la `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` y `ñ`); y `readWordList` solo acepta una lista con el SHA-256 fijado para su idioma, que sea UTF-8 y que `checkWordList` acepte. `wordkey.ts` gana `wordRules`, que carga una vez las tablas de Unicode para quien lee muchas palabras.
- **La página `/create`** ofrece palabras al azar por defecto. Con la política «con llave», una casilla abre la cápsula también con unas palabras: al marcarla, la página descarga del propio sitio la lista española (`create-words.ts`, el fichero que Vite publica con un nombre con hash), la acepta solo con su SHA-256 fijado, y sortea 7 palabras que no salen del navegador. Las muestra numeradas, con su fuerza calculada con la lista cargada y un botón para sacar otras. «Las elijo yo» deja escribir las propias, con el aviso de que son más débiles. En los dos casos hay que escribirlas otra vez para comprobar que se tienen; dan igual mayúsculas y tildes. `planCapsule` recibe `wordsKind` (`none`, `random` u `own`) y pide las palabras que falten.
- **Corregido en `/create`:** el efecto que comprueba las rutas escribía `pathCheck` y lo volvía a leer, así que Svelte lo relanzaba sin fin en cuanto se cargaban las reglas, con el primer fichero, comentario o autor: `effect_update_depth_exceeded` en la consola y `checkPaths` repetido sobre toda la lista. Lo lee ahora de una variable local. Venía de `7dc88ef` (1 de octubre) y está en la `0.4.0`.
- **La lista inglesa.** `testdata` y `wordlists` pasan a `e671032` de `datekeys-go`, con el mismo `testdata`: `wordlists/en.txt` es la lista grande de la EFF, de 7 776 palabras, CC BY 4.0, en su orden y sin los números de los dados. `WORD_LIST_SHA256` fija su SHA-256, y el alfabeto de `en` es de la `a` a la `z` y el guion de sus cuatro palabras compuestas, como `t-shirt`. `/create` sigue ofreciendo la española, y `check-build.mjs` exige que la página publique solo esa.
- **`licenses.txt`** lleva el `README.md` de `wordlists/`, con el origen, el método y la licencia de la lista (CC BY-SA 4.0), y `check-build.mjs` exige que esté y que la lista se publique byte a byte.
## 0.4.0 — 7 de octubre de 2026

@ -82,7 +82,7 @@ La inspección (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `securitycms.ts` | Los veredictos de una firma de `alg` 2, F1, F2, F5 y F6, con cada firmante requerido y ajeno nombrado como el §29.7 del borrador v0.12 (su nombre si cumple las reglas del autor declarado, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no el SHA-256 del certificado; su emisor, o el SHA-256 de su `Name`; y la autoridad de su sello), y los de un sello, S1 a S5, con su autoridad y t. `encodeSigners` escribe `SIGNERS` para el escritor, ordenado y con los textos de Go | `capsule/signature2.go` |
| `note.ts` | La nota pública (§24.1): la extensión `datekeys.note` de la cabecera, de 1 a 1024 bytes de UTF-8 que cumplen las reglas del autor declarado, con los textos y el orden de `extension.CheckNote`. Una cadena con un sustituto suelto se rechaza: nunca se escribe con U+FFFD. `checkNoteData` comprueba los bytes de una nota en el orden de Go: la longitud, el UTF-8 y las reglas. `publicNote` la lee como Go, con un U+FEFF inicial que la deja inservible, y `unusableNote` distingue una nota inservible de ninguna | `extension` (`CheckNote`, `NewNote`, `Note`), `capsule` (`Header.UnusableNote`) |
| `wordkey.ts` | La llave de palabras (§38.1): `normalizeWords` (NFD con las tablas de Unicode 18.0.0, sin las marcas U+0300 a U+036F, la minúscula simple de cada punto de código, partido por los espacios de la lista), `checkWords` (al menos 6 palabras distintas de 3 letras o más, sin controles, invisibles ni puntos sin asignar), `wordRules` (las dos, con las tablas cargadas una vez) y `wordKey`, PBKDF2-SHA256 de 600 000 rondas de Web Crypto con la sal de la cadena, la ronda y `capsule_id`. `quickWords`, `hiddenCodePoint` y `countedWords` leen con las tablas de la plataforma, para un formulario que no puede esperar a las de Unicode 18.0.0 | `wordkey.Normalize`, `Check`, `Key` |
| `wordlist.ts` | Las palabras al azar de la llave de palabras (el SHOULD de §38.1), con los textos de Go: `generateWords` sortea `DEFAULT_WORD_COUNT` palabras distintas, 7, con `randomIndex` y `crypto.getRandomValues`; `wordBits` da su fuerza, 90 bits para 7 de 7 776; `checkWordList` rechaza una lista de menos de 2 048 palabras, con dos que son una al normalizarlas o con un carácter que no es una letra del alfabeto de su idioma, que da el código y no la lista (para `es`, de la `a` a la `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` y `ñ`): una letra cirílica que parece latina se volvería a escribir con la latina, y la cápsula no se abriría; y `readWordList` solo acepta una lista con el SHA-256 fijado para su idioma en `WORD_LIST_SHA256`, que sea UTF-8 y que `checkWordList` acepte. Ninguna lista se da por buena, tampoco las de DateKeys | `wordkey.Generate`, `CheckList`, `Bits`, `List` |
| `wordlist.ts` | Las palabras al azar de la llave de palabras (el SHOULD de §38.1), con los textos de Go: `generateWords` sortea `DEFAULT_WORD_COUNT` palabras distintas, 7, con `randomIndex` y `crypto.getRandomValues`; `wordBits` da su fuerza, 90 bits para 7 de 7 776; `checkWordList` rechaza una lista de menos de 2 048 palabras, con dos que son una al normalizarlas o con un carácter que no es una letra del alfabeto de su idioma, que da el código y no la lista (para `es`, de la `a` a la `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` y `ñ`; para `en`, de la `a` a la `z` y el guion de las cuatro palabras compuestas de la lista de la EFF): una letra cirílica que parece latina se volvería a escribir con la latina, y la cápsula no se abriría; y `readWordList` solo acepta una lista con el SHA-256 fijado para su idioma en `WORD_LIST_SHA256`, que sea UTF-8 y que `checkWordList` acepte. Ninguna lista se da por buena, tampoco las de DateKeys | `wordkey.Generate`, `CheckList`, `Bits`, `List` |
| `pathrule.ts`, `pathrule-tables.ts` | Las reglas de las rutas y de los textos del head (§29.5, §29.6), de R1 a R10 con R4b, R6b, R6c y R9, NFD, el pliegue y la clave de R7, con los textos de error de Go. Nunca usa `normalize`, `toLowerCase`, `localeCompare`, `Intl` ni las clases `\p{…}`, cuya versión de Unicode cambia con el motor: las tablas de Unicode 18.0.0 y WindowsBestFit las genera `datekeys-go` (`pathrule/gen -ts`), y una prueba recalcula su digest | `internal/pathrule` |
| `open3.ts`, `sink.ts` | El paso 17 del formato 3 en sus subpasos 17.2 a 17.8, con la precedencia de Go: un fallo de `age` o un texto en claro que no mide P prevalecen, manda el primer subpaso que falla, y los códigos distintos de `ERR_INTEGRITY` solo se dan tras leer `PAYLOAD_AGE` hasta el final. `Sink` (`begin`, `create`, `commit`, `abort`) recibe los ficheros y solo los publica en el paso 18; `MemorySink` los guarda en memoria, que crece con los bytes recibidos y nunca con los tamaños que declara el head | `capsule/open3.go` |
| `framing.ts` | Prelude DKC1 (16 bytes), con el formato de la cápsula, de 1 a 3, 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` |
@ -470,10 +470,10 @@ Comprueba que los ficheros coinciden con `SOURCE.json`, sin faltantes ni sobrant
`.gitattributes` marca `testdata/**` y `wordlists/**` como binarios para que git no altere ningún byte.
Copia actual: la de `testdata/SOURCE.json`, `datekeys-go` en `27a75ee`, de la rama `v0.15` después del tag `spec-v0.15`, que se sincroniza con `node scripts/sync-testdata.mjs sync --commit 27a75ee`. Su `testdata` es el del tag (`fe50885`), que añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`; y trae `wordkey/lists`, con la lista española.
Copia actual: la de `testdata/SOURCE.json`, `datekeys-go` en `e671032`, de la rama `v0.15` después del tag `spec-v0.15`, que se sincroniza con `node scripts/sync-testdata.mjs sync --commit e671032`. Su `testdata` es el del tag (`fe50885`), que añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`; y trae `wordkey/lists`, con la lista inglesa de la EFF y la española. La página solo publica la española.
## Licencia
Apache-2.0 ([LICENSE](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 núcleo IBE (`ibe.ts`), derivado de `tlock-js` (Apache-2.0 OR MIT, usado bajo MIT), y `bech32.ts`, portado de `age` (MIT). El sitio publica esos avisos y los de sus paquetes npm en `licenses.txt`.
Las listas de palabras de `wordlists/` no son código de este proyecto ni tienen su licencia: la española es CC BY-SA 4.0, una adaptación de FrequencyWords de Hermit Dave. Su `README.md`, copiado de `datekeys-go`, dice de dónde sale, cómo se hizo y su licencia.
Las listas de palabras de `wordlists/` no son código de este proyecto ni tienen su licencia: la inglesa es la de la EFF, CC BY 4.0, y la española es CC BY-SA 4.0, una adaptación de FrequencyWords de Hermit Dave. Su `README.md`, copiado de `datekeys-go`, dice de dónde sale, cómo se hizo y su licencia.

@ -38,7 +38,8 @@
// - a page loads the author keys or the signing of alg 1 (authorkey.ts,
// ed25519sign.ts and their tables of Go, gounicode.ts) with the page, or
// /inspect, which only reads signatures, loads them at all;
// - a word list of wordlists/ is not shipped byte for byte;
// - the word list of /create is not shipped byte for byte, or another list
// of wordlists/ is shipped;
// - licenses.txt lacks the notice of a package in the client bundle, the
// copyright lines kept in the header of a module of src/ derived from
// another project, the README of the word lists, or the license of the
@ -256,16 +257,20 @@ for (const f of files) {
}
// ---------------------------------------------------------------------------
// The word lists of the random words of /create: each list of wordlists/ is
// shipped byte for byte, once, since the page takes a list only with its
// pinned SHA-256; licenses.txt gives their source and license (below).
// The word lists of the random words of /create: the list that the page
// fetches (create-words.ts), the Spanish one, is shipped byte for byte, once,
// since the page takes it only with its pinned SHA-256, and the other lists
// of wordlists/ are not shipped; licenses.txt gives their source and license
// (below).
const WORDLISTS = join(ROOT, 'wordlists');
const PAGE_LISTS = ['es.txt'];
for (const name of readdirSync(WORDLISTS).filter((n) => n.endsWith('.txt')).sort()) {
const stem = name.slice(0, -'.txt'.length);
const copies = byHash.get(sha256(readFileSync(join(WORDLISTS, name)))) ?? [];
const shipped = copies.filter((p) => inBuild(p).startsWith('_app/immutable/assets/') && basename(p).startsWith(`${stem}.`));
if (shipped.length !== 1) fail(`word list ${name}: ${shipped.length} byte-exact copies under build/_app/immutable/assets, want 1`);
const want = PAGE_LISTS.includes(name) ? 1 : 0;
if (shipped.length !== want) fail(`word list ${name}: ${shipped.length} byte-exact copies under build/_app/immutable/assets, want ${want}`);
}
// ---------------------------------------------------------------------------

@ -26,12 +26,19 @@ function repeating(seed: readonly number[]): RandomWords {
}
describe('readWordList', () => {
it('reads the list es of datekeys-go, with the SHA-256 that its README records', async () => {
it('reads the lists en and es of datekeys-go, with the SHA-256 that its README records', async () => {
const readme = new TextDecoder().decode(listFile('README.md'));
expect(readme).toContain(`| \`es.txt\` | 7776 | \`${WORD_LIST_SHA256['es']}\` |`);
const words = await readWordList('es', listFile('es.txt'));
expect(words).toHaveLength(7776);
expect([words[0], words.at(-1)]).toEqual(['abad', 'útil']);
expect(Object.keys(WORD_LIST_SHA256).sort()).toEqual(['en', 'es']);
for (const lang of ['en', 'es']) expect(readme).toContain(`| \`${lang}.txt\` | 7776 | \`${WORD_LIST_SHA256[lang]}\` |`);
const es = await readWordList('es', listFile('es.txt'));
expect(es).toHaveLength(7776);
expect([es[0], es.at(-1)]).toEqual(['abad', 'útil']);
// The list of the EFF in its order: the dice 11111 give its first word
// and 66666 its last.
const en = await readWordList('en', listFile('en.txt'));
expect(en).toHaveLength(7776);
expect([en[0], en[1], en.at(-1)]).toEqual(['abacus', 'abdomen', 'zoom']);
expect(en.filter((w) => w.includes('-'))).toEqual(['drop-down', 'felt-tip', 't-shirt', 'yo-yo']);
});
it('refuses a list of another SHA-256 and a language without a list, with the texts of Go', async () => {
@ -39,7 +46,7 @@ describe('readWordList', () => {
changed[0]! ^= 1;
const refused = `wordkey: the list "es" has the SHA-256 ${await hash(changed)}, not ${WORD_LIST_SHA256['es']}`;
await expect(readWordList('es', changed)).rejects.toThrow(refused);
await expect(readWordList('xx', listFile('es.txt'))).rejects.toThrow(/^wordkey: no word list for "xx"; the lists are es$/);
await expect(readWordList('xx', listFile('es.txt'))).rejects.toThrow(/^wordkey: no word list for "xx"; the lists are en, es$/);
await expect(readWordList('es', fileOf(base), { fr: '00', de: '11' })).rejects.toThrow(/^wordkey: no word list for "es"; the lists are de, fr$/);
});
@ -87,6 +94,12 @@ describe('checkWordList', () => {
];
for (const [list, want] of cases) await expect(checkWordList('es', list)).rejects.toThrow(want);
});
it('takes the hyphen as a letter of en and not of es, and an accent as one of es and not of en, as Go', async () => {
await expect(checkWordList('en', withWord(5, 't-shirt'))).resolves.toBeUndefined();
await expect(checkWordList('es', withWord(5, 't-shirt'))).rejects.toThrow(/^line 6, "t-shirt", holds U\+002D, which is not in the alphabet of "es"$/);
await expect(checkWordList('en', withWord(5, 'palaáf'))).rejects.toThrow(/^line 6, "palaáf", holds U\+00E1, which is not in the alphabet of "en"$/);
});
});
describe('generateWords', () => {

@ -27,12 +27,16 @@ export const MIN_LIST_SIZE = 2048;
* here, so that the change is never silent.
*/
export const WORD_LIST_SHA256: Readonly<Record<string, string>> = {
en: '6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522',
es: 'ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe',
};
// The letters that a word of a list of each language may hold, as the list
// writes it, lower case and in NFC: the alphabets of Go wordkey.
// writes it, lower case and in NFC: the alphabets of Go wordkey. The English
// one has the ASCII hyphen of the four compound words of the list of the EFF,
// such as t-shirt.
const ALPHABETS: Readonly<Record<string, string>> = {
en: 'abcdefghijklmnopqrstuvwxyz-',
es: 'abcdefghijklmnopqrstuvwxyzáéíóúüñ',
};

@ -1,6 +1,6 @@
{
"module": "g.activething.com/go/DateKeys",
"commit": "27a75eefcd2ac2a3b32930d6872f3b5b6e648792",
"commit": "e671032e04a3cd0999cf073ebedf50c45b088683",
"files": {
"README.md": "d26b3f507edf5afe89600f063cb509b9ef62a908b93ed476bff964b5a443bf4b",
"fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4",

@ -7,8 +7,14 @@ from the normalized text of the words, whatever list they came from.
| List | Words | SHA-256 | Status |
|---|---|---|---|
| `en.txt` | 7776 | `6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522` | The large wordlist of the EFF, as published |
| `es.txt` | 7776 | `ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe` | Draft, not yet reviewed by a native speaker |
Each list has 7776 = 6^5 words, sorted, so that five dice give a word: each
die minus one is a digit of the position of the word in base 6, the first
die the most significant. The dice 11111 give the first word and 66666 the
last, as in the list of the EFF.
A list changes only with its hash in this file and in `generate_test.go`:
an application that downloads a list pins its SHA-256 and refuses any other.
`wordkey.CheckList` checks a list against the alphabet of its language,
@ -18,8 +24,26 @@ typed again with the letter of the keyboard, and the capsule would not open.
| Language | Alphabet |
|---|---|
| `en` | `a` to `z` and the ASCII hyphen of the four compound words of the list of the EFF (`drop-down`, `felt-tip`, `t-shirt` and `yo-yo`), in lower case |
| `es` | `a` to `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` and `ñ`, in lower case and NFC |
## `en.txt`
- **Source:** the large wordlist for passphrases of the Electronic Frontier
Foundation, by Joseph Bonneau (2016),
<https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt>, whose
SHA-256 was
`addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e` when
it was downloaded on 7 October 2026. The EFF explains it in
<https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases>.
- **License:** original material of the EFF, which its
[copyright policy](https://www.eff.org/copyright) licenses under
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), unlike the code
of this module (Apache 2.0).
- **Change:** the dice number before each word is left out, and nothing
else: the words and their order are those of the EFF, so the position of a
word still gives its number.
## `es.txt`
- **Source:** the frequencies of

@ -1,8 +1,9 @@
{
"module": "g.activething.com/go/DateKeys",
"commit": "27a75eefcd2ac2a3b32930d6872f3b5b6e648792",
"commit": "e671032e04a3cd0999cf073ebedf50c45b088683",
"files": {
"README.md": "db71bbd031a335adeeff9e0453fdc2b2378b3bacc0104907efdfbc72fc7e6d6a",
"README.md": "d73a477d2ad4a03c6f1d61c21eeb8b754ec5a758346af62e6426979934eda097",
"en.txt": "6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522",
"es.txt": "ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe"
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save

Powered by TurnKey Linux.