Dice: the words of five dice each, encrypt -dice and datekeys wordlist

For whoever does not trust the random numbers of a computer, as the author
decided: five dice for each word, read in a fixed order, give a number
from 11111 to 66666, the position of the word in a list of 7776 words, the
first die the most significant. wordkey.DiceNumber and DiceWord number the
words and give the word of a number; DiceWords takes several numbers, at
least 6 and never the same word twice, which is rolled again; DiceList is
the list numbered for dice, a line for each word with its dice and a tab,
as the EFF publishes its own: for en it is the file of the EFF, byte for
byte.

encrypt -dice TEXT and -dice-file FILE take the words from the dice of the
list -dic and show them: the words open the capsule, the numbers give them
only with that list. datekeys wordlist [-dic LIST] writes the numbered
list, to print it, and its SHA-256, which wordkey/lists/README.md records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.15
dev 8 hours ago
parent e671032e04
commit 92e7154c23

@ -32,6 +32,16 @@ opens a capsule even when the clock is before the round time.
The alphabet of `en` is `a` to `z` and the ASCII hyphen of its four
compound words, such as `t-shirt`.
- **Dice.** For whoever does not trust the random numbers of a computer,
as the author decided: five dice for each word give a number from 11111
to 66666, its position in a list of 7776 words. `wordkey.DiceNumber`,
`DiceWord`, `DiceWords` (at least 6 numbers, never the same word twice)
and `DiceList`, the list numbered for dice as the EFF publishes its own:
for `en` it is the file of the EFF, byte for byte. `encrypt -dice TEXT`
and `-dice-file FILE` take the words from dice and show them, and
`datekeys wordlist [-dic LIST]` writes the numbered list, to print it,
and its SHA-256, which `wordkey/lists/README.md` records.
- **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

@ -184,7 +184,12 @@ 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 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.
una persona son más débiles. `-dice` y `-dice-file` las sacan de unos dados,
para quien no se fíe del azar de un ordenador: cinco dados por palabra dan un
número del 11111 al 66666, su posición en la lista, y `datekeys wordlist`
escribe la lista numerada para los dados, para imprimirla, con su SHA-256;
la de `en` es el fichero de la EFF, byte a byte. `encrypt` muestra las
palabras de los dados: la cápsula la abren ellas, no los números. 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

@ -126,6 +126,8 @@ datekeys encrypt -at 2030-01-01T00:00:00Z -in letter.txt -out letter.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -in photos -in letter.txt -comment "For Ana" -author "Juan" -out gift.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk gift.dkk -in photos -out gift.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -words-file words.txt -in letter.txt -out letter.dkc
datekeys wordlist -dic en > words-for-dice.txt
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dice-file dice.txt -in letter.txt -out letter.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -no-mtime -in letter.txt -out letter.dkc
datekeys author keygen -out author.key -pass-file pass.txt
datekeys encrypt -at 2030-01-01T00:00:00Z -in letter.txt -note "Letters from Lisbon" -sign author.key -sign-pass-file pass.txt -out letter.dkc
@ -183,7 +185,12 @@ least six different words of three or more letters, which open it with
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
are weaker. `-dice` and `-dice-file` take them from dice instead, for
whoever does not trust the random numbers of a computer: five dice for each
word give a number from 11111 to 66666, its position in the list, and
`datekeys wordlist` writes the list numbered for dice, to print it, with its
SHA-256; for `en` it is the file of the EFF, byte for byte. `encrypt` shows
the words of the dice: they open the capsule, not the numbers. 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).

@ -24,6 +24,7 @@ package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
@ -51,13 +52,14 @@ import (
)
const usage = `usage:
datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE|-new-words FILE [-dic LIST] [-word-count N]] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area]
datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE|-new-words FILE [-dic LIST] [-word-count N]|-dice TEXT|-dice-file FILE [-dic LIST]] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area]
datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... [-release FILE]
datekeys author keygen -out FILE (-pass-file FILE|-plain)
datekeys author public -key FILE [-pass-file FILE]
datekeys inspect -in FILE.dkc [-json]
datekeys datekey resolve -at TIME
datekeys profile hash [-in PROFILE.cbor]
datekeys wordlist [-dic LIST]
datekeys version
TIME is RFC 3339 with a time zone, for example 2030-01-01T00:00:00Z.
@ -87,6 +89,15 @@ 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).
-dice TEXT and -dice-file FILE take the words from dice instead, for whoever
does not trust the random numbers of a computer: five dice for each word,
read in a fixed order, give a number from 11111 to 66666, the position of a
word in the list -dic, at least 6 numbers and no word twice. datekeys
wordlist writes the list -dic numbered for dice, to print it, and its
SHA-256: for en it is the list of the EFF, byte for byte. encrypt shows the
words: keep the words, which open the capsule; the numbers give them only
with that list.
-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
can identify someone. decrypt shows it as text of the creator.
@ -153,6 +164,8 @@ func run(args []string, stdout, stderr io.Writer, now func() time.Time) error {
return errUsage
}
return profileHash(args[2:], stdout)
case "wordlist":
return wordList(args[1:], stdout, stderr)
case "version", "-version", "--version":
if len(args) != 1 {
return errUsage
@ -210,7 +223,9 @@ 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", "en", "list of -new-words: "+strings.Join(wordkey.Languages(), ", "))
dice := fs.String("dice", "", "time_and_key: numbers of five dice, one for each word of the list -dic; they stay in the shell history")
diceFile := fs.String("dice-file", "", "time_and_key: file with the numbers of five dice of -dice")
dic := fs.String("dic", "en", "list of -new-words and -dice: "+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")
@ -254,7 +269,15 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
if err != nil {
return err
}
diceText, err := flagText("encrypt", "dice", *dice, *diceFile)
if err != nil {
return err
}
if diceText != "" && (text != "" || *newWords != "") {
return errors.New("encrypt: -dice and -dice-file exclude -words, -words-file and -new-words")
}
var listSize int
var fromDice []string
if *newWords != "" {
if text != "" {
return errors.New("encrypt: -new-words excludes -words and -words-file")
@ -273,9 +296,20 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
text = strings.Join(drawn, " ")
listSize = len(list)
}
if diceText != "" {
list, err := wordkey.List(*dic)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
if fromDice, err = wordkey.DiceWords(list, diceText); err != nil {
return fmt.Errorf("encrypt: %w", err)
}
text = strings.Join(fromDice, " ")
listSize = len(list)
}
if text != "" {
if pol != capsule.TimeAndKey {
return errors.New("encrypt: -words, -words-file and -new-words need -policy time_and_key")
return errors.New("encrypt: -words, -words-file, -new-words and -dice need -policy time_and_key")
}
w := wordkey.Normalize(text)
if err := wordkey.Check(w); err != nil {
@ -339,6 +373,10 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
fmt.Fprintf(stderr, " words %s: %d words of the list %s, %d bits; keep them secret, or write them down and delete the file\n",
*newWords, *wordCount, *dic, int(wordkey.Bits(listSize, *wordCount)))
}
if fromDice != nil {
fmt.Fprintf(stderr, " words %s: the %d words of the dice in the list %s, %d bits; keep these words, which open the capsule: the numbers give them only with this list\n",
text, len(fromDice), *dic, int(wordkey.Bits(listSize, len(fromDice))))
}
if res.UnlockAt.Sub(now()) > longHorizon {
fmt.Fprintln(stderr, "warning: Quicknet V1 timelock is not post-quantum. The ciphertext may stay available for years,\n"+
" and its future confidentiality depends on the provider and on the underlying cryptography (spec §53).")
@ -542,11 +580,17 @@ func releaseInHand(path string) (provider.Supplier, io.Closer, error) {
// wordsText is the text of the words of -words or of -words-file, at most
// 4 KiB, or "" when neither is given.
func wordsText(cmd, words, file string) (string, error) {
if words != "" && file != "" {
return "", fmt.Errorf("%s: -words and -words-file are exclusive", cmd)
return flagText(cmd, "words", words, file)
}
// flagText is the text of -NAME or of the file of -NAME-file, at most 4 KiB,
// or "" when neither is given.
func flagText(cmd, name, text, file string) (string, error) {
if text != "" && file != "" {
return "", fmt.Errorf("%s: -%s and -%s-file are exclusive", cmd, name, name)
}
if file == "" {
return words, nil
return text, nil
}
f, err := os.Open(file)
if err != nil {
@ -558,11 +602,35 @@ func wordsText(cmd, words, file string) (string, error) {
return "", err
}
if len(b) > 4<<10 {
return "", fmt.Errorf("%s: %s is longer than 4 KiB: it is not a list of words", cmd, file)
return "", fmt.Errorf("%s: %s is longer than 4 KiB: it is not a list of %s", cmd, file, name)
}
return string(b), nil
}
// wordList writes the list -dic numbered for dice to stdout, as the EFF
// publishes its list, to print it, and its SHA-256 to stderr, to compare it
// with the one that wordkey/lists/README.md records.
func wordList(args []string, stdout, stderr io.Writer) error {
fs := newFlags("wordlist")
dic := fs.String("dic", "en", "list: "+strings.Join(wordkey.Languages(), ", "))
if err := parse(fs, args); err != nil {
return err
}
list, err := wordkey.List(*dic)
if err != nil {
return fmt.Errorf("wordlist: %w", err)
}
text, err := wordkey.DiceList(list)
if err != nil {
return fmt.Errorf("wordlist: %w", err)
}
if _, err := io.WriteString(stdout, text); err != nil {
return err
}
fmt.Fprintf(stderr, "the list %s numbered for dice, %d words, SHA-256 %x\n", *dic, len(list), sha256.Sum256([]byte(text)))
return nil
}
func readIdentities(path string) ([]age.Identity, error) {
f, err := os.Open(path)
if err != nil {

@ -2,6 +2,7 @@ package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
@ -641,7 +642,15 @@ func TestKeyOfWords(t *testing.T) {
{[]string{"-policy", "time_and_key", "-new-words", words}, "already exists"},
{[]string{"-policy", "time_and_key", "-new-words", filepath.Join(dir, "n.txt"), "-dic", "xx"}, `no word list for "xx"`},
{[]string{"-policy", "time_and_key", "-new-words", filepath.Join(dir, "n.txt"), "-word-count", "5"}, "at least 6 words, not 5"},
{[]string{"-new-words", filepath.Join(dir, "n.txt")}, "-words, -words-file and -new-words need -policy time_and_key"},
{[]string{"-new-words", filepath.Join(dir, "n.txt")}, "-words, -words-file, -new-words and -dice need -policy time_and_key"},
{[]string{"-dice", "11111 11112 11113 11114 11115 11116"}, "-words, -words-file, -new-words and -dice need -policy time_and_key"},
{[]string{"-policy", "time_and_key", "-dice", "11111", "-dice-file", words}, "-dice and -dice-file are exclusive"},
{[]string{"-policy", "time_and_key", "-dice", "11111", "-words", "a"}, "-dice and -dice-file exclude -words, -words-file and -new-words"},
{[]string{"-policy", "time_and_key", "-dice-file", words, "-new-words", filepath.Join(dir, "n.txt")}, "-dice and -dice-file exclude -words, -words-file and -new-words"},
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115"}, "at least 6 words, not 5"},
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115 11171"}, `"11171" is not five dice: five digits from 1 to 6`},
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115 11111"}, `the dice 11111 give "abacus" a second time; roll them again`},
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115 11116", "-dic", "xx"}, `no word list for "xx"`},
} {
args := append([]string{"encrypt", "-at", at, "-in", in, "-out", filepath.Join(dir, "x.dkc")}, tc.args...)
if _, _, err := cli(t, genesis, args...); err == nil || !strings.Contains(err.Error(), tc.want) {
@ -653,6 +662,61 @@ func TestKeyOfWords(t *testing.T) {
}
}
// The words of dice, as the list numbered for dice gives them: the capsule
// opens with them.
func TestDice(t *testing.T) {
dir := t.TempDir()
in := filepath.Join(dir, "carta.txt")
os.WriteFile(in, []byte("abierta con dados"), 0o600)
p := profile.Quicknet()
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
dkc := filepath.Join(dir, "carta.dkc")
dice := filepath.Join(dir, "dados.txt")
os.WriteFile(dice, []byte("35214 11111\n64253 11112 11113\n66666 11121\n"), 0o600)
_, stderr, err := cli(t, time.Unix(p.GenesisTime, 0), "encrypt", "-at", unlock.Format(time.RFC3339), "-policy", "time_and_key",
"-dice-file", dice, "-dic", "es", "-in", in, "-out", dkc)
if err != nil {
t.Fatalf("encrypt: %v\n%s", err, stderr)
}
const words = "glaciar abad tocar abadía abandonar útil abdomen"
if !strings.Contains(stderr, "words "+words+": the 7 words of the dice in the list es, 90 bits; keep these words") {
t.Errorf("stderr: %s", stderr)
}
out := filepath.Join(dir, "abierta")
if _, stderr, err := cli(t, later, "decrypt", "-in", dkc, "-out", out, "-words", "Glaciar abad tocar abadia abandonar util abdomen", "-relay", relay(t)); err != nil {
t.Fatalf("decrypt: %v\n%s", err, stderr)
}
if b, err := os.ReadFile(filepath.Join(out, "carta.txt")); err != nil || string(b) != "abierta con dados" {
t.Fatalf("carta.txt = %q, %v", b, err)
}
}
// datekeys wordlist writes the list numbered for dice, and its SHA-256: for
// en, the file of the EFF.
func TestWordList(t *testing.T) {
for lang, want := range map[string]string{
"en": "addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e",
"es": "611f779a33df74587e9dfb486bb0185a9dfd4d1384e75993a21247ad31cefddb",
} {
stdout, stderr, err := cli(t, time.Now(), "wordlist", "-dic", lang)
if err != nil {
t.Fatalf("wordlist -dic %s: %v", lang, err)
}
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(stdout))); got != want {
t.Errorf("wordlist -dic %s: SHA-256 %s, want %s", lang, got, want)
}
if stderr != "the list "+lang+" numbered for dice, 7776 words, SHA-256 "+want+"\n" {
t.Errorf("wordlist -dic %s: stderr %q", lang, stderr)
}
}
if stdout, _, err := cli(t, time.Now(), "wordlist"); err != nil || !strings.HasPrefix(stdout, "11111\tabacus\n11112\tabdomen\n") {
t.Errorf("wordlist: %.40q, %v", stdout, err)
}
if _, _, err := cli(t, time.Now(), "wordlist", "-dic", "xx"); err == nil || !strings.Contains(err.Error(), `wordlist: wordkey: no word list for "xx"`) {
t.Errorf("wordlist -dic xx: %v", err)
}
}
func TestNewWords(t *testing.T) {
dir := t.TempDir()
in := filepath.Join(dir, "carta.txt")

@ -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 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` |
| 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`; the dice: `DiceListSize`, `DiceNumber`, `DiceWord`, `DiceWords`, `DiceList`), 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`, `-word-count`, `-dice` and `-dice-file` of `encrypt`, the command `wordlist`, the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1), `TestBuiltInLists`, `TestCheckList`, `TestGenerate`, `TestBits`, `TestGenerateUniform`, `TestDiceNumber`, `TestDiceWord`, `TestDiceWords`, `TestDiceList`; `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`, `TestDice`, `TestWordList` |
| 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` |

@ -0,0 +1,92 @@
package wordkey
import (
"fmt"
"strings"
)
// DiceListSize is the size of a list that dice draw from: five dice, each
// from 1 to 6, give 6^5 positions. Whoever does not trust the random numbers
// of a computer rolls them, and looks the words up in the list numbered for
// dice (DiceList).
const DiceListSize = 7776
// DiceNumber returns the dice of the word at position i of a list of
// DiceListSize words: five digits from 1 to 6, each one more than a digit of
// i in base 6, the first the most significant, so that 11111 is the first
// word and 66666 the last, as in the list of the EFF.
func DiceNumber(i int) (string, error) {
if i < 0 || i >= DiceListSize {
return "", fmt.Errorf("wordkey: no dice give position %d of a list of %d words", i, DiceListSize)
}
var b [5]byte
for k := len(b) - 1; k >= 0; k-- {
b[k] = '1' + byte(i%6)
i /= 6
}
return string(b[:]), nil
}
// DiceWord returns the word of list that the dice give, as DiceNumber
// numbers the words: five digits from 1 to 6. The list must have
// DiceListSize words.
func DiceWord(list []string, dice string) (string, error) {
if len(list) != DiceListSize {
return "", fmt.Errorf("wordkey: dice draw from a list of %d words, not %d", DiceListSize, len(list))
}
if len(dice) != 5 {
return "", fmt.Errorf("wordkey: %q is not five dice: five digits from 1 to 6", dice)
}
i := 0
for k := range len(dice) {
c := dice[k]
if c < '1' || c > '6' {
return "", fmt.Errorf("wordkey: %q is not five dice: five digits from 1 to 6", dice)
}
i = i*6 + int(c-'1')
}
return list[i], nil
}
// DiceWords returns the words that dice give, in their order: one number of
// five dice for each word, separated by white space, at least MinWords of
// them, and never the same word twice, which whoever rolls rolls again.
// Fair dice draw each word as Generate does, so the words are as strong.
func DiceWords(list []string, dice string) ([]string, error) {
if len(list) != DiceListSize {
return nil, fmt.Errorf("wordkey: dice draw from a list of %d words, not %d", DiceListSize, len(list))
}
numbers := strings.Fields(dice)
if len(numbers) < MinWords {
return nil, fmt.Errorf("wordkey: a key of words needs at least %d words, not %d", MinWords, len(numbers))
}
seen := make(map[string]bool, len(numbers))
words := make([]string, 0, len(numbers))
for _, n := range numbers {
w, err := DiceWord(list, n)
if err != nil {
return nil, err
}
if seen[w] {
return nil, fmt.Errorf("wordkey: the dice %s give %q a second time; roll them again", n, w)
}
seen[w] = true
words = append(words, w)
}
return words, nil
}
// DiceList returns list numbered for dice, to print it: a line for each
// word, its dice, a tab and the word, as the EFF publishes its list. For the
// English list it is the file of the EFF, byte for byte.
func DiceList(list []string) (string, error) {
if len(list) != DiceListSize {
return "", fmt.Errorf("wordkey: dice draw from a list of %d words, not %d", DiceListSize, len(list))
}
var b strings.Builder
for i, w := range list {
n, _ := DiceNumber(i)
b.WriteString(n + "\t" + w + "\n")
}
return b.String(), nil
}

@ -0,0 +1,110 @@
package wordkey
import (
"crypto/sha256"
"fmt"
"strings"
"testing"
)
// The lists numbered for dice and their SHA-256, as lists/README.md records
// them: the English one is the file of the EFF, byte for byte.
var diceListHashes = map[string]string{
"en": "addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e",
"es": "611f779a33df74587e9dfb486bb0185a9dfd4d1384e75993a21247ad31cefddb",
}
func TestDiceNumber(t *testing.T) {
for i, want := range map[int]string{0: "11111", 1: "11112", 5: "11116", 6: "11121", 35: "11166", 36: "11211", 1295: "16666", 1296: "21111", 7775: "66666"} {
if got, err := DiceNumber(i); err != nil || got != want {
t.Errorf("DiceNumber(%d) = %q, %v, want %q", i, got, err, want)
}
}
for _, i := range []int{-1, 7776} {
if _, err := DiceNumber(i); err == nil || err.Error() != fmt.Sprintf("wordkey: no dice give position %d of a list of 7776 words", i) {
t.Errorf("DiceNumber(%d): %v", i, err)
}
}
}
func TestDiceWord(t *testing.T) {
en, _ := List("en")
es, _ := List("es")
// Every position, there and back.
for i := range en {
n, _ := DiceNumber(i)
if w, err := DiceWord(en, n); err != nil || w != en[i] {
t.Fatalf("DiceWord(%s) = %q, %v, want %q", n, w, err, en[i])
}
}
for _, c := range []struct {
list []string
dice, want string
}{
{en, "11111", "abacus"},
{en, "11112", "abdomen"},
{en, "35214", "jovial"},
{en, "66666", "zoom"},
{es, "11111", "abad"},
{es, "35214", "glaciar"},
{es, "66666", "útil"},
} {
if w, err := DiceWord(c.list, c.dice); err != nil || w != c.want {
t.Errorf("DiceWord(%s) = %q, %v, want %q", c.dice, w, err, c.want)
}
}
for _, dice := range []string{"", "1111", "111111", "11110", "11117", "a1111", "1111 ", string(rune(0xff11)) + "1111"} {
want := fmt.Sprintf("wordkey: %q is not five dice: five digits from 1 to 6", dice)
if _, err := DiceWord(en, dice); err == nil || err.Error() != want {
t.Errorf("DiceWord(%q): %v, want %q", dice, err, want)
}
}
if _, err := DiceWord(en[:2048], "11111"); err == nil || err.Error() != "wordkey: dice draw from a list of 7776 words, not 2048" {
t.Errorf("DiceWord of 2048 words: %v", err)
}
}
func TestDiceWords(t *testing.T) {
en, _ := List("en")
words, err := DiceWords(en, " 11111 11112\t11113\n11114 11115 11116 11121 ")
if err != nil || strings.Join(words, " ") != "abacus abdomen abdominal abide abiding ability ablaze" {
t.Fatalf("DiceWords = %q, %v", words, err)
}
if err := Check(Normalize(strings.Join(words, " "))); err != nil {
t.Errorf("the words of the dice: %v", err)
}
for _, c := range []struct{ dice, want string }{
{"11111 11112 11113 11114 11115", "wordkey: a key of words needs at least 6 words, not 5"},
{"", "wordkey: a key of words needs at least 6 words, not 0"},
{"11111 11112 11113 11114 11115 1116", `wordkey: "1116" is not five dice: five digits from 1 to 6`},
{"11111 11112 11113 11114 11115 11111", `wordkey: the dice 11111 give "abacus" a second time; roll them again`},
{"11111,11112 11113 11114 11115 11116 11121", `wordkey: "11111,11112" is not five dice: five digits from 1 to 6`},
} {
if _, err := DiceWords(en, c.dice); err == nil || err.Error() != c.want {
t.Errorf("DiceWords(%q): %v, want %q", c.dice, err, c.want)
}
}
if _, err := DiceWords(en[1:], "11111 11112 11113 11114 11115 11116"); err == nil || err.Error() != "wordkey: dice draw from a list of 7776 words, not 7775" {
t.Errorf("DiceWords of 7775 words: %v", err)
}
}
func TestDiceList(t *testing.T) {
for lang, want := range diceListHashes {
list, _ := List(lang)
text, err := DiceList(list)
if err != nil {
t.Fatal(err)
}
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(text))); got != want {
t.Errorf("DiceList(%s): SHA-256 %s, want %s", lang, got, want)
}
lines := strings.Split(strings.TrimSuffix(text, "\n"), "\n")
if len(lines) != DiceListSize || lines[0] != "11111\t"+list[0] || lines[7775] != "66666\t"+list[7775] {
t.Errorf("DiceList(%s): %d lines, %q … %q", lang, len(lines), lines[0], lines[len(lines)-1])
}
}
if _, err := DiceList(make([]string, 2048)); err == nil || err.Error() != "wordkey: dice draw from a list of 7776 words, not 2048" {
t.Errorf("DiceList of 2048 words: %v", err)
}
}

@ -13,7 +13,14 @@ from the normalized text of the words, whatever list they came from.
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.
last, as in the list of the EFF. `datekeys wordlist` (`wordkey.DiceList`)
writes a list numbered for dice, to print it: a line for each word, its
dice, a tab and the word, as the EFF publishes its list.
| List numbered for dice | SHA-256 |
|---|---|
| `en` | `addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e`, the file of the EFF, byte for byte |
| `es` | `611f779a33df74587e9dfb486bb0185a9dfd4d1384e75993a21247ad31cefddb` |
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.

Loading…
Cancel
Save

Powered by TurnKey Linux.