You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/internal/testkit/wordkeyvectors.go

213 lines
8.8 KiB

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...)
}

Powered by TurnKey Linux.