package testkit import ( "encoding/hex" "errors" "fmt" "slices" "strings" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/wordkey" ) // WordKeyVectorFile is testdata/vectors/wordkey.json: the key of words of // spec v0.11, §38.1. Normalize gives the words of a text; Check, what a // writer does with them; Keys, the identity that the words derive for a // capsule, with its recipient. type WordKeyVectorFile struct { Spec string `json:"spec"` Description string `json:"description"` Normalize []WordNormalize `json:"normalize"` Check []WordCheck `json:"check"` Keys []WordKeyVector `json:"keys"` } // WordNormalize is a text and its words, as wordkey.Normalize gives them. type WordNormalize struct { Name string `json:"name"` Text string `json:"text"` Words []string `json:"words"` } // WordCheck is a text, its words and the result of wordkey.Check on them: // ResultOK, or the text of its error. type WordCheck struct { Name string `json:"name"` Text string `json:"text"` Words []string `json:"words"` Result string `json:"result"` Error string `json:"error,omitempty"` } // WordKeyVector is the identity of words for the capsule capsule_id of the // round of the chain chain_hash: key, the raw X25519 identity, and // recipient, its age1… recipient. type WordKeyVector struct { Name string `json:"name"` Words []string `json:"words"` ChainHash string `json:"chain_hash"` Round uint64 `json:"round"` CapsuleID string `json:"capsule_id"` Key string `json:"key"` Recipient string `json:"recipient"` } // cp is the string of the code point r: invisible code points are written // by number in this file, never as themselves. func cp(r rune) string { return string(r) } // The spaces of spec §38.1, step 4. var wordSpaces = []rune{ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20, 0x85, 0xa0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000, } // The six words of the vector of spec §38.1. const sixWords = "perro luna casa verde tren mar" // WordKeyVectors computes testdata/vectors/wordkey.json, and fails if a // vector does not get the words, the result or the key it is written for. func WordKeyVectors() (WordKeyVectorFile, error) { f := WordKeyVectorFile{ Spec: SpecVersion, Description: "The key of words (spec §38.1): the words of a text, after NFD, without U+0300 to U+036F, in simple lower case of Unicode 18.0.0 and split by the spaces of the list; " + "what a writer refuses; and the identity, PBKDF2-HMAC-SHA256 of 600000 rounds, for a chain hash, a round and a capsule_id. See testdata/README.md.", } var errs []error fail := func(format string, args ...any) { errs = append(errs, fmt.Errorf(format, args...)) } // Normalize. norm := []struct { name, text string want []string }{ {"the words of the vector of §38.1", sixWords, strings.Fields(sixWords)}, {"accents, upper case and runs of spaces", " Ábaco" + cp(0xa0) + "ÁRBOL" + cp(0x09) + "niño ΣΑΣ İ ", []string{"abaco", "arbol", "nino", "σασ", "i"}}, {"«Ábaco ÁRBOL» of §38.1", "Ábaco ÁRBOL", []string{"abaco", "arbol"}}, {"«abaco arbol» of §38.1", "abaco arbol", []string{"abaco", "arbol"}}, {"«ábaco árbol» of §38.1", "ábaco árbol", []string{"abaco", "arbol"}}, {"the accents written as combining marks", "a" + cp(0x301) + "baco a" + cp(0x301) + "rbol", []string{"abaco", "arbol"}}, {"punctuation counts", "Perro, luna.", []string{"perro,", "luna."}}, {"no text", "", []string{}}, {"only spaces", " " + cp(0x09) + cp(0x3000) + " ", []string{}}, {"the simple lower case of a final sigma", "ΟΔΟΣ", []string{"οδοσ"}}, {"capital sharp s, lowered by its simple mapping", "STRAẞE", []string{"straße"}}, {"a dotless i stays", "ı", []string{"ı"}}, {"the angstrom sign, whose NFD is A and a ring", cp(0x212b), []string{"a"}}, {"Hangul, decomposed into jamo", "한글", []string{"ᄒ" + "ᅡ" + "ᆫ" + "ᄀ" + "ᅳ" + "ᆯ"}}, {"a combining mark outside U+0300 to U+036F stays", "a" + cp(0x20d7), []string{"a" + cp(0x20d7)}}, {"fullwidth letters are not folded to ASCII", "ABC", []string{"abc"}}, {"U+A7CB, lowered by Unicode 18.0.0 to U+0264", cp(0xa7cb), []string{cp(0x264)}}, {"a zero width space is not a space", "a" + cp(0x200b) + "b", []string{"a" + cp(0x200b) + "b"}}, {"U+180E is not a space", "a" + cp(0x180e) + "b", []string{"a" + cp(0x180e) + "b"}}, {"a byte order mark is not a space", "a" + cp(0xfeff) + "b", []string{"a" + cp(0xfeff) + "b"}}, } for _, s := range wordSpaces { norm = append(norm, struct { name, text string want []string }{fmt.Sprintf("U+%04X is a space", s), "uno" + cp(s) + "dos", []string{"uno", "dos"}}) } for _, c := range norm { got := wordkey.Normalize(c.text) if got == nil { got = []string{} } if !slices.Equal(got, c.want) { fail("normalize %q: %q, want %q", c.name, got, c.want) } f.Normalize = append(f.Normalize, WordNormalize{Name: c.name, Text: c.text, Words: got}) } // Check. for _, c := range []struct{ name, text, want string }{ {"six words", sixWords, ResultOK}, {"six words with upper case and accents", "Perro LUNA casa verde trén mar", ResultOK}, {"three words", "uno dos tres", "not 3"}, {"only short words", "a b c d e f g h", "not 0"}, {"one word six times", "perro perro perro perro perro perro", "not 1"}, {"short words do not count", "de la casa al mar en tren verde", "not 4"}, {"five words that count and a short one", "perro luna casa verde tren y", "not 5"}, {"a word of three letters counts", "sol luna casa verde tren mar", ResultOK}, {"a word of three letters that are not ASCII counts", "ñuñ luna casa verde tren mar", ResultOK}, {"the same words in other case count once", "perro PERRO luna casa verde tren", "not 5"}, {"a zero width space, which the writer refuses", sixWords + cp(0x200b), "invisible character U+200B"}, {"a soft hyphen, ignorable", "per" + cp(0xad) + "ro luna casa verde tren mar", "invisible character U+00AD"}, {"a variation selector, ignorable", sixWords + cp(0xfe0f), "invisible character U+FE0F"}, {"a byte order mark", cp(0xfeff) + sixWords, "invisible character U+FEFF"}, {"a control character", sixWords + cp(0x01), "control character U+0001"}, {"a C1 control character", sixWords + cp(0x9f), "control character U+009F"}, {"DEL", sixWords + cp(0x7f), "control character U+007F"}, {"an unassigned code point", sixWords + cp(0x0378), "U+0378, unassigned"}, {"a private use code point is assigned", sixWords + cp(0xe000), ResultOK}, {"an emoji", "perro luna casa verde tren mar 🌍", ResultOK}, } { words := wordkey.Normalize(c.text) if words == nil { words = []string{} } v := WordCheck{Name: c.name, Text: c.text, Words: words, Result: ResultOK} if err := wordkey.Check(words); err != nil { v.Result, v.Error = "error", err.Error() } switch { case c.want == ResultOK && v.Result != ResultOK: fail("check %q: %s, want ok", c.name, v.Error) case c.want != ResultOK && !strings.Contains(v.Error, c.want): fail("check %q: %q, want %q", c.name, v.Error, c.want) } f.Check = append(f.Check, v) } // Keys. quicknet := profile.QuicknetChainHash const mainnet = "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce" const id0 = "000102030405060708090a0b0c0d0e0f" for _, c := range []struct { name, text, chain string round uint64 capsuleID, want string }{ {"the vector of §38.1", sixWords, quicknet, 1000, id0, "fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41"}, {"the next round", sixWords, quicknet, 1001, id0, ""}, {"another capsule_id", sixWords, quicknet, 1000, "000102030405060708090a0b0c0d0e0e", ""}, {"another chain hash", sixWords, mainnet, 1000, id0, ""}, {"round 2^53 - 1", sixWords, quicknet, 1<<53 - 1, id0, ""}, {"words that are not ASCII", "Ábaco ÁRBOL niño ΣΑΣ ñandú straße", quicknet, 1000, "ffeeddccbbaa99887766554433221100", ""}, } { chain, err := hex.DecodeString(c.chain) if err != nil { return f, err } capsuleID, err := hex.DecodeString(c.capsuleID) if err != nil { return f, err } words := wordkey.Normalize(c.text) raw, err := wordkey.Key(words, chain, c.round, capsuleID) if err != nil { return f, err } id, err := wordkey.Identity(words, chain, c.round, capsuleID) if err != nil { return f, err } key := hex.EncodeToString(raw) if c.want != "" && key != c.want { fail("key %q: %s, want %s", c.name, key, c.want) } for _, k := range f.Keys { if k.Key == key { fail("key %q: the same key as %q", c.name, k.Name) } } f.Keys = append(f.Keys, WordKeyVector{ Name: c.name, Words: words, ChainHash: c.chain, Round: c.round, CapsuleID: c.capsuleID, Key: key, Recipient: id.Recipient().String(), }) } return f, errors.Join(errs...) }