The English list: the large wordlist of the EFF, and -dic en by default

wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.15
dev 10 hours ago
parent 96d89927a4
commit e671032e04

@ -26,6 +26,12 @@ opens a capsule even when the clock is before the round time.
FrequencyWords; `wordkey/lists/README.md` records its source, method and
SHA-256. It changes no format and no derivation.
- **The English list.** `wordkey/lists/en.txt` is the large wordlist of the
EFF, 7776 words, CC BY 4.0, without its dice numbers and in its order, so
that the position of a word still gives them; `-dic` takes it by default.
The alphabet of `en` is `a` to `z` and the ASCII hyphen of its four
compound words, such as `t-shirt`.
- **Approval.** `SpecVersion` is 0.15, and so is the `spec` field of every
file of `testdata`: the records and the vectors, regenerated, and the
frozen `security_cms.json` and `locator.json`, whose `spec` field alone

@ -182,9 +182,9 @@ se muestra, y los dos lo dicen (`public_note_unusable` en `inspect -json`).
palabras: al menos seis palabras distintas de tres letras o más, que la abren
con `decrypt -words-file` en lugar de una `.dkk` (spec §38.1). `-new-words
FICHERO` las sortea en su lugar, 7 por defecto, de una lista incluida
(`-dic es`, la única por ahora; `-word-count N`), las escribe en un fichero
nuevo y dice su fuerza en bits: las palabras que elige una persona son más
débiles. Una lista solo se usa si cada palabra es del alfabeto de su idioma.
(`-dic en`, la de la EFF, por defecto, o `-dic es`; `-word-count N`), las
escribe en un fichero nuevo y dice su fuerza en bits: las palabras que elige
una persona son más débiles. Una lista solo se usa si cada palabra es del alfabeto de su idioma.
Las listas y su licencia están en [`wordkey/lists`](wordkey/lists/README.md).
## Librería

@ -180,9 +180,10 @@ and both say so (`public_note_unusable` in `inspect -json`).
`-words` and `-words-file` give a `time_and_key` capsule a key of words: at
least six different words of three or more letters, which open it with
`decrypt -words-file` instead of a `.dkk` (spec §38.1). `-new-words FILE`
draws them at random instead, 7 by default, from a built-in list
(`-dic es`, the only one for now; `-word-count N`), writes them to a new
file and says their strength in bits: words a person chooses are weaker. A
draws them at random instead, 7 by default, from a built-in list (`-dic
en`, the list of the EFF, by default, or `-dic es`; `-word-count N`), writes
them to a new file and says their strength in bits: words a person chooses
are weaker. A
list is used only if each word is of the alphabet of its language. The lists
and their license are in [`wordkey/lists`](wordkey/lists/README.md).

@ -82,9 +82,10 @@ instead of a .dkk
in the shell history; -words-file reads them from a file.
-new-words FILE draws the words at random instead, and writes them to the new
file FILE: -word-count words, 7 by default, of the list -dic, es by default,
of 7776 words. Words a person chooses are weaker than random ones: whoever
holds the .dkc can try them offline once the date has come (spec §38.1).
file FILE: -word-count words, 7 by default, of the list -dic of 7776 words,
en by default, the list of the EFF, or es. Words a person chooses are weaker
than random ones: whoever holds the .dkc can try them offline once the date
has come (spec §38.1).
-note puts a public note in the capsule, in clear: anyone who has the .dkc
reads it before the date, nobody can check who wrote it, and with the date it
@ -209,7 +210,7 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
words := fs.String("words", "", "time_and_key: at least 6 words that open the capsule; they stay in the shell history")
wordsFile := fs.String("words-file", "", "time_and_key: file with the words that open the capsule")
newWords := fs.String("new-words", "", "time_and_key: new file with words drawn at random from a list, that open the capsule")
dic := fs.String("dic", "es", "list of -new-words: "+strings.Join(wordkey.Languages(), ", "))
dic := fs.String("dic", "en", "list of -new-words: "+strings.Join(wordkey.Languages(), ", "))
wordCount := fs.Int("word-count", wordkey.DefaultCount, "number of words of -new-words, 6 or more")
note := fs.String("note", "", "public note of the capsule: one line that anyone with the .dkc reads before the date, and that can identify someone with it")
sign := fs.String("sign", "", "file with the author key that signs the capsule")

@ -666,27 +666,40 @@ func TestNewWords(t *testing.T) {
if err != nil {
t.Fatalf("encrypt: %v\n%s", err, stderr)
}
if !strings.Contains(stderr, "words "+words+": 8 words of the list es, 103 bits; keep them secret") {
// The list of the EFF by default.
if !strings.Contains(stderr, "words "+words+": 8 words of the list en, 103 bits; keep them secret") {
t.Errorf("stderr: %s", stderr)
}
b, err := os.ReadFile(words)
if err != nil {
t.Fatal(err)
}
list, _ := wordkey.List("es")
inList := map[string]bool{}
for _, w := range list {
inList[w] = true
}
drawn := strings.Fields(string(b))
if len(drawn) != 8 || !strings.HasSuffix(string(b), "\n") {
t.Fatalf("words file %q", b)
}
for _, w := range drawn {
if !inList[w] {
t.Errorf("%q is not in the list es", w)
inList := func(lang, file string, n int) {
t.Helper()
b, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
list, _ := wordkey.List(lang)
known := map[string]bool{}
for _, w := range list {
known[w] = true
}
drawn := strings.Fields(string(b))
if len(drawn) != n || !strings.HasSuffix(string(b), "\n") {
t.Fatalf("words file %q", b)
}
for _, w := range drawn {
if !known[w] {
t.Errorf("%q is not in the list %s", w, lang)
}
}
}
inList("en", words, 8)
// The Spanish list with -dic es.
spanish := filepath.Join(dir, "espanol.txt")
_, stderr, err = cli(t, time.Unix(p.GenesisTime, 0), "encrypt", "-at", unlock.Format(time.RFC3339), "-policy", "time_and_key",
"-new-words", spanish, "-dic", "es", "-in", in, "-out", filepath.Join(dir, "espanol.dkc"))
if err != nil || !strings.Contains(stderr, "words "+spanish+": 7 words of the list es, 90 bits;") {
t.Fatalf("encrypt -dic es: %v\n%s", err, stderr)
}
inList("es", spanish, 7)
out := filepath.Join(dir, "abierta")
if _, stderr, err := cli(t, later, "decrypt", "-in", dkc, "-out", out, "-words-file", words, "-relay", relay(t)); err != nil {
t.Fatalf("decrypt: %v\n%s", err, stderr)

@ -68,7 +68,7 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 36.1 | Authenticity semantics | documented in `README.md`, `SECURITY.md` | — (a property the protocol does not provide) |
| 37 | X25519 recipient V1; the writer rejects a recipient that is not canonical (bit 255 set, or u ≥ p) or of low order, and MAY reject a point of the twist | `age.X25519Recipient`; `agewrap.X25519IdentityFromRaw`, `agewrap.CheckX25519Recipient` (run by `capsule.Encrypt`); the twist check is not implemented | `agewrap.TestRawKeys`, `TestNonCanonicalRecipients`; `capsule.TestEncryptRejectsInvalidOptions` |
| 38 | Portable Access Key | `EncryptOptions.NewPortableKey` (fresh `I_ACCESS` per capsule; no API accepts an existing one); `accesskey.AccessKey` | `capsule.TestPortableKeysAreNeverReused` |
| 38.1 | Key of words: one more X25519 credential of `time_and_key`, among the 16; the normalization: NFD with the tables of Unicode 18.0.0, without U+0300 to U+036F, the simple lower case of each code point, split by the spaces of the list; PBKDF2-HMAC-SHA256 of 600 000 rounds, salted with the chain hash, the round and `capsule_id`, into a raw X25519 identity; the writer requires at least 6 words, counting only different words of 3 or more letters, and refuses controls, ignorables and unassigned code points; a reader may ask for the words instead of a `.dkk`; SHOULD: random words of a public list by default, at least 6 of 2048 or more | `wordkey` (`Normalize`, `Check`, `Key`, `Identity`, `Rounds`, `MinWords`, `MinLetters`; `Generate`, `List`, `CheckList` with the alphabet of each language, `Bits`, `DefaultCount`, `MinListSize`, the list `lists/es.txt`), with `pathrule.NFD`, `Lower`, `DefaultIgnorable` and `Assigned`; `capsule.EncryptOptions.Words` (`accessRecipients`; `sealer.write` derives the identity once `capsule_id` is drawn); `cmd/datekeys`: `-words` and `-words-file` of `encrypt` and `decrypt` (`wordsText`), `-new-words`, `-dic` and `-word-count` of `encrypt`, the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1), `TestBuiltInLists`, `TestCheckList`, `TestGenerate`, `TestBits`, `TestGenerateUniform`; `testdata/vectors/wordkey.json` (`internal/testkit.WordKeyVectors`, `TestVectorFilesAreCurrent`): the words of a text, what a writer refuses and the identities, the cases of §64 of v0.11; `capsule.TestEncryptFilesWords` (the words of another `capsule_id` do not open); `cmd/datekeys.TestKeyOfWords`, `TestNewWords` |
| 38.1 | Key of words: one more X25519 credential of `time_and_key`, among the 16; the normalization: NFD with the tables of Unicode 18.0.0, without U+0300 to U+036F, the simple lower case of each code point, split by the spaces of the list; PBKDF2-HMAC-SHA256 of 600 000 rounds, salted with the chain hash, the round and `capsule_id`, into a raw X25519 identity; the writer requires at least 6 words, counting only different words of 3 or more letters, and refuses controls, ignorables and unassigned code points; a reader may ask for the words instead of a `.dkk`; SHOULD: random words of a public list by default, at least 6 of 2048 or more | `wordkey` (`Normalize`, `Check`, `Key`, `Identity`, `Rounds`, `MinWords`, `MinLetters`; `Generate`, `List`, `CheckList` with the alphabet of each language, `Bits`, `DefaultCount`, `MinListSize`, the lists `lists/en.txt` and `lists/es.txt`), with `pathrule.NFD`, `Lower`, `DefaultIgnorable` and `Assigned`; `capsule.EncryptOptions.Words` (`accessRecipients`; `sealer.write` derives the identity once `capsule_id` is drawn); `cmd/datekeys`: `-words` and `-words-file` of `encrypt` and `decrypt` (`wordsText`), `-new-words`, `-dic` and `-word-count` of `encrypt`, the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1), `TestBuiltInLists`, `TestCheckList`, `TestGenerate`, `TestBits`, `TestGenerateUniform`; `testdata/vectors/wordkey.json` (`internal/testkit.WordKeyVectors`, `TestVectorFilesAreCurrent`): the words of a text, what a writer refuses and the identities, the cases of §64 of v0.11; `capsule.TestEncryptFilesWords` (the words of another `capsule_id` do not open); `cmd/datekeys.TestKeyOfWords`, `TestNewWords` |
| 39 | Recipients of INNER_ACCESS_AGE: in formats 2 and 3 from 1 to 16 credentials, a dummy in each slot left (a fresh public key whose private key is dropped at once), the 16 in a uniformly random order; which slots are dummies is recorded only in the official vectors | `capsule/encrypt.go` (`accessRecipients`, `fillSlots`, `permute`); `agewrap.AccessIdentity` | `capsule.TestInnerHasSixteenStanzas`, `TestDummyRecipients`, `TestStanzaOrderIsUniform`, `TestCredentialBounds`, `TestFixtureRecipients`, `TestEncryptRoundTripBothPolicies`; `TestConformanceFixtures` (the stanza each credential opens, `access_key_stanza` and `identity_stanzas` in the records) |
| 40 | `.dkk` framing; `BODY_LEN` in 1..16 MiB (0 is `ERR_INTEGRITY`); order of the frame checks | `accesskey.Encode`, `accesskey.Decode` (the body buffer grows with the data read; every buffer holding the body is wiped) | `accesskey.TestDecodeRejects`, `TestDecodePrecedence`, `TestDecodeShortBodyAllocatesLittle`, `TestEncodeAndDecodeLeaveNoStaleMaterial`, `FuzzDecode` |
| 41 | `.dkk` BODY_CBOR | `AccessKey.MarshalBody`, `accesskey.DecodeBody` (hand-written `bodyWire` encode and decode) | `accesskey.TestFixtures`, `TestDecodeBodyStructure` |

@ -20,6 +20,9 @@ const DefaultCount = 7
// §38.1: at least 6 words of a list of 2048 or more).
const MinListSize = 2048
//go:embed lists/en.txt
var listEN string
//go:embed lists/es.txt
var listES string
@ -27,6 +30,7 @@ var listES string
// plain UTF-8 file, one word per line; lists/README.md says where each comes
// from and its license.
var lists = map[string]string{
"en": listEN,
"es": listES,
}
@ -35,8 +39,11 @@ var lists = map[string]string{
// script that looks like one of these, such as the Cyrillic U+0430 for the
// Latin a, would be written down and typed again with the letter of the
// keyboard, and the capsule would not open. The alphabet of a list comes from
// here, never from the list.
// here, never from the list. The English one has the ASCII hyphen of the four
// compound words of the list of the EFF, such as t-shirt, kept so that every
// word keeps its number of dice.
var alphabets = map[string]string{
"en": "abcdefghijklmnopqrstuvwxyz-",
"es": "abcdefghijklmnopqrstuvwxyzáéíóúüñ",
}

@ -12,11 +12,12 @@ import (
// The built-in lists and their SHA-256, as lists/README.md records them. A
// change of a list changes the hash an app pins, so it is never silent.
var listHashes = map[string]string{
"en": "6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522",
"es": "ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe",
}
func TestBuiltInLists(t *testing.T) {
if got := strings.Join(Languages(), " "); got != "es" {
if got := strings.Join(Languages(), " "); got != "en es" {
t.Fatalf("Languages() = %q", got)
}
for lang, want := range listHashes {
@ -31,9 +32,16 @@ func TestBuiltInLists(t *testing.T) {
t.Errorf("list %s: %d words, want 7776", lang, len(words))
}
}
if _, err := List("xx"); err == nil || !strings.Contains(err.Error(), `no word list for "xx"; the lists are es`) {
if _, err := List("xx"); err == nil || !strings.Contains(err.Error(), `no word list for "xx"; the lists are en, es`) {
t.Errorf("List(xx): %v", err)
}
// The list of the EFF keeps its order, so that the word at position i
// is the one of the dice of i in base 6: 11111 is the first, and 66666
// the last.
en, _ := List("en")
if en[0] != "abacus" || en[1] != "abdomen" || en[7775] != "zoom" {
t.Errorf("list en: %q, %q, %q", en[0], en[1], en[7775])
}
}
func TestCheckList(t *testing.T) {
@ -77,6 +85,19 @@ func TestCheckList(t *testing.T) {
t.Errorf("CheckList: %v, want %q", err, c.want)
}
}
// The hyphen of the compound words of the list of the EFF is a letter of
// en and not of es; an accent is a letter of es and not of en.
if err := CheckList("en", with(5, "t-shirt")); err != nil {
t.Errorf("CheckList(en) with t-shirt: %v", err)
}
for _, c := range []struct{ lang, word, want string }{
{"es", "t-shirt", `line 6, "t-shirt", holds U+002D, which is not in the alphabet of "es"`},
{"en", "palaáf", `line 6, "palaáf", holds U+00E1, which is not in the alphabet of "en"`},
} {
if err := CheckList(c.lang, with(5, c.word)); err == nil || err.Error() != c.want {
t.Errorf("CheckList(%s) with %q: %v, want %q", c.lang, c.word, err, c.want)
}
}
}
func TestGenerate(t *testing.T) {

@ -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

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

Powered by TurnKey Linux.