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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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.