diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9f332..e1de1d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,14 @@ reader of v0.11 opens these capsules, and this one opens those of v0.11. 3837 completed to 4096 by a key 6 that is empty or not in its shortest form (change 7) among them. The bases 3837, 4070, 4094 and 4095 and those of the next multiple join `padding_cases`. + - `wordkey.json`, new: the key of words of §38.1, which §64 of v0.11 + asks for and was only in the tests of `wordkey`. 45 texts and their + words, among them each space of the list and three that are not; 20 + texts and what a writer does with them, the zero width space that it + refuses among them; and 6 identities with their recipients, the vector + of §38.1 and those of the next round, another `capsule_id`, another + chain hash, round 2^53 − 1 and words that are not ASCII. For + `datekeys-ts` and `datekeys-dart`. - `testdata/README.md` describes the files of v0.11 and of the draft. ## Unreleased — specification v0.11 diff --git a/docs/traceability.md b/docs/traceability.md index 29a5dbf..24be925 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -68,7 +68,7 @@ the same. A case of §64 that is not in the repository yet is marked | 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` | `wordkey` (`Normalize`, `Check`, `Key`, `Identity`, `Rounds`, `MinWords`, `MinLetters`), 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`), the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1); `capsule.TestEncryptFilesWords` (the words of another `capsule_id` do not open); `cmd/datekeys.TestKeyOfWords` | +| 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` | `wordkey` (`Normalize`, `Check`, `Key`, `Identity`, `Rounds`, `MinWords`, `MinLetters`), 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`), the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1); `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` | | 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` | diff --git a/internal/testkit/genfixtures/main.go b/internal/testkit/genfixtures/main.go index 04e4ad6..964a64a 100644 --- a/internal/testkit/genfixtures/main.go +++ b/internal/testkit/genfixtures/main.go @@ -187,7 +187,11 @@ func format3Vectors(dir string) error { if err != nil { return err } - for name, v := range map[string]any{"paths.json": paths, "path_fold.json": fold, "head_schema.json": heads, "security.json": security, "ed25519_strict.json": strict, "note.json": note} { + words, err := testkit.WordKeyVectors() + if err != nil { + return err + } + for name, v := range map[string]any{"paths.json": paths, "path_fold.json": fold, "head_schema.json": heads, "security.json": security, "ed25519_strict.json": strict, "note.json": note, "wordkey.json": words} { if err := testkit.WriteJSON(filepath.Join(dir, name), v); err != nil { return err } diff --git a/internal/testkit/testkit_test.go b/internal/testkit/testkit_test.go index 48049b9..208f000 100644 --- a/internal/testkit/testkit_test.go +++ b/internal/testkit/testkit_test.go @@ -267,6 +267,10 @@ func TestVectorFilesAreCurrent(t *testing.T) { if err != nil { t.Fatal(err) } + words, err := testkit.WordKeyVectors() + if err != nil { + t.Fatal(err) + } for _, v := range []struct { file string want any @@ -280,6 +284,7 @@ func TestVectorFilesAreCurrent(t *testing.T) { {"security.json", security, &testkit.SecurityVectorFile{}}, {"ed25519_strict.json", strict, &testkit.Ed25519StrictFile{}}, {"note.json", note, &testkit.NoteVectorFile{}}, + {"wordkey.json", words, &testkit.WordKeyVectorFile{}}, } { if err := testkit.ReadJSON("../../testdata/vectors/"+v.file, v.got); err != nil { t.Fatal(err) diff --git a/internal/testkit/wordkeyvectors.go b/internal/testkit/wordkeyvectors.go new file mode 100644 index 0000000..c24b758 --- /dev/null +++ b/internal/testkit/wordkeyvectors.go @@ -0,0 +1,212 @@ +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...) +} diff --git a/testdata/README.md b/testdata/README.md index c2ce04b..2422f03 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -56,6 +56,7 @@ Conventions for every file: | `vectors/security_cms.json` | security areas with a signature of `alg` 2 or a seal of `seal_type` 2, each with its context, verdicts, results and lines | §29.7, §29.10, §29.11 | | `vectors/ed25519_strict.json` | Ed25519 signatures and the result of the strict profile of the author signature | §29.9 | | `vectors/note.json` | the data of the public note and the result of its rules | §24.1, §29.6 | +| `vectors/wordkey.json` | the key of words: the words of a text, what a writer refuses, and the identity the words derive | §38.1, §64 | | `vectors/locator.json` | the extension `datekeys.capsule` of a `.dkk`, its envelope and its locator, and what a reader rejects and uses of them | §44.1, §64 | | `vectors/mutations.json` | the mutation corpus: the 178 mutations of §64 and further cases | §63, §64 | | `vectors/inspect_differential.json` | 5110 mutations of fourteen fixtures with the verdict of steps 1 to 8 | §63 | @@ -584,6 +585,39 @@ feed, a space at either end, U+202E, U+200B, a byte order mark, a noncharacter, a byte that is not UTF-8 and the UTF-8 of a lone surrogate, refused. +## `vectors/wordkey.json` + +The key of words of spec §38.1, the cases that §64 of v0.11 asks for, in +three lists: + +- `normalize`: a `text` and its `words`, after NFD with the tables of Unicode + 18.0.0, without U+0300 to U+036F, with the simple lower case of each code + point and split by the spaces of the list of §38.1. Among the cases: the + vector of §38.1; «Ábaco ÁRBOL», «abaco arbol» and «ábaco árbol», which give + the same words; accents written as combining marks; punctuation, which + counts; a final sigma, a capital sharp s, a dotless i, the angstrom sign, + Hangul, fullwidth letters and U+A7CB, whose lower case is that of Unicode + 18.0.0; each of the 25 spaces of the list, one by one; and U+200B, U+180E + and U+FEFF, which are not spaces. +- `check`: a `text`, its `words` and what a writer does with them: `result` + `ok`, or `error` with the text of the reference in `error`. At least six + different words of three characters or more; a control, a + Default_Ignorable code point (U+200B, the soft hyphen, a variation + selector, U+FEFF) or a code point unassigned in Unicode 18.0.0 is refused, + and a private use code point or an emoji is not. +- `keys`: the identity of `words` for the capsule `capsule_id` of `round` of + the chain `chain_hash`, PBKDF2-HMAC-SHA256 of 600000 rounds: `key`, the raw + X25519 identity, and `recipient`, its `age1…` recipient. The vector of + §38.1; the next round, another `capsule_id` and another chain hash, each a + different key; round 2^53 − 1; and words that are not ASCII. + +```json +{ "name": "a zero width space, which the writer refuses", "text": "perro luna casa verde tren mar\u200b", "words": ["perro", "luna", "casa", "verde", "tren", "mar\u200b"], "result": "error", "error": "wordkey: the words hold the invisible character U+200B" } +``` + +The texts are valid UTF-8: a reader of JSON in another language cannot hold +anything else in a string. + ## `vectors/ed25519_strict.json` Ed25519 signatures, in hexadecimal, and whether the strict profile of the diff --git a/testdata/vectors/wordkey.json b/testdata/vectors/wordkey.json new file mode 100644 index 0000000..3dfe96d --- /dev/null +++ b/testdata/vectors/wordkey.json @@ -0,0 +1,732 @@ +{ + "spec": "0.11", + "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.", + "normalize": [ + { + "name": "the words of the vector of §38.1", + "text": "perro luna casa verde tren mar", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ] + }, + { + "name": "accents, upper case and runs of spaces", + "text": " Ábaco ÁRBOL\tniño ΣΑΣ İ ", + "words": [ + "abaco", + "arbol", + "nino", + "σασ", + "i" + ] + }, + { + "name": "«Ábaco ÁRBOL» of §38.1", + "text": "Ábaco ÁRBOL", + "words": [ + "abaco", + "arbol" + ] + }, + { + "name": "«abaco arbol» of §38.1", + "text": "abaco arbol", + "words": [ + "abaco", + "arbol" + ] + }, + { + "name": "«ábaco árbol» of §38.1", + "text": "ábaco árbol", + "words": [ + "abaco", + "arbol" + ] + }, + { + "name": "the accents written as combining marks", + "text": "ábaco árbol", + "words": [ + "abaco", + "arbol" + ] + }, + { + "name": "punctuation counts", + "text": "Perro, luna.", + "words": [ + "perro,", + "luna." + ] + }, + { + "name": "no text", + "text": "", + "words": [] + }, + { + "name": "only spaces", + "text": " \t  ", + "words": [] + }, + { + "name": "the simple lower case of a final sigma", + "text": "ΟΔΟΣ", + "words": [ + "οδοσ" + ] + }, + { + "name": "capital sharp s, lowered by its simple mapping", + "text": "STRAẞE", + "words": [ + "straße" + ] + }, + { + "name": "a dotless i stays", + "text": "ı", + "words": [ + "ı" + ] + }, + { + "name": "the angstrom sign, whose NFD is A and a ring", + "text": "Å", + "words": [ + "a" + ] + }, + { + "name": "Hangul, decomposed into jamo", + "text": "한글", + "words": [ + "한글" + ] + }, + { + "name": "a combining mark outside U+0300 to U+036F stays", + "text": "a⃗", + "words": [ + "a⃗" + ] + }, + { + "name": "fullwidth letters are not folded to ASCII", + "text": "ABC", + "words": [ + "abc" + ] + }, + { + "name": "U+A7CB, lowered by Unicode 18.0.0 to U+0264", + "text": "Ɤ", + "words": [ + "ɤ" + ] + }, + { + "name": "a zero width space is not a space", + "text": "a​b", + "words": [ + "a​b" + ] + }, + { + "name": "U+180E is not a space", + "text": "a᠎b", + "words": [ + "a᠎b" + ] + }, + { + "name": "a byte order mark is not a space", + "text": "ab", + "words": [ + "ab" + ] + }, + { + "name": "U+0009 is a space", + "text": "uno\tdos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+000A is a space", + "text": "uno\ndos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+000B is a space", + "text": "uno\u000bdos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+000C is a space", + "text": "uno\fdos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+000D is a space", + "text": "uno\rdos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+0020 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+0085 is a space", + "text": "uno…dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+00A0 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+1680 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2000 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2001 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2002 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2003 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2004 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2005 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2006 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2007 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2008 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2009 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+200A is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2028 is a space", + "text": "uno\u2028dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+2029 is a space", + "text": "uno\u2029dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+202F is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+205F is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + }, + { + "name": "U+3000 is a space", + "text": "uno dos", + "words": [ + "uno", + "dos" + ] + } + ], + "check": [ + { + "name": "six words", + "text": "perro luna casa verde tren mar", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "ok" + }, + { + "name": "six words with upper case and accents", + "text": "Perro LUNA casa verde trén mar", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "ok" + }, + { + "name": "three words", + "text": "uno dos tres", + "words": [ + "uno", + "dos", + "tres" + ], + "result": "error", + "error": "wordkey: a key of words needs at least 6 different words of 3 or more letters, not 3" + }, + { + "name": "only short words", + "text": "a b c d e f g h", + "words": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h" + ], + "result": "error", + "error": "wordkey: a key of words needs at least 6 different words of 3 or more letters, not 0" + }, + { + "name": "one word six times", + "text": "perro perro perro perro perro perro", + "words": [ + "perro", + "perro", + "perro", + "perro", + "perro", + "perro" + ], + "result": "error", + "error": "wordkey: a key of words needs at least 6 different words of 3 or more letters, not 1" + }, + { + "name": "short words do not count", + "text": "de la casa al mar en tren verde", + "words": [ + "de", + "la", + "casa", + "al", + "mar", + "en", + "tren", + "verde" + ], + "result": "error", + "error": "wordkey: a key of words needs at least 6 different words of 3 or more letters, not 4" + }, + { + "name": "five words that count and a short one", + "text": "perro luna casa verde tren y", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "y" + ], + "result": "error", + "error": "wordkey: a key of words needs at least 6 different words of 3 or more letters, not 5" + }, + { + "name": "a word of three letters counts", + "text": "sol luna casa verde tren mar", + "words": [ + "sol", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "ok" + }, + { + "name": "a word of three letters that are not ASCII counts", + "text": "ñuñ luna casa verde tren mar", + "words": [ + "nun", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "ok" + }, + { + "name": "the same words in other case count once", + "text": "perro PERRO luna casa verde tren", + "words": [ + "perro", + "perro", + "luna", + "casa", + "verde", + "tren" + ], + "result": "error", + "error": "wordkey: a key of words needs at least 6 different words of 3 or more letters, not 5" + }, + { + "name": "a zero width space, which the writer refuses", + "text": "perro luna casa verde tren mar​", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar​" + ], + "result": "error", + "error": "wordkey: the words hold the invisible character U+200B" + }, + { + "name": "a soft hyphen, ignorable", + "text": "per­ro luna casa verde tren mar", + "words": [ + "per­ro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "error", + "error": "wordkey: the words hold the invisible character U+00AD" + }, + { + "name": "a variation selector, ignorable", + "text": "perro luna casa verde tren mar️", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar️" + ], + "result": "error", + "error": "wordkey: the words hold the invisible character U+FE0F" + }, + { + "name": "a byte order mark", + "text": "perro luna casa verde tren mar", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "error", + "error": "wordkey: the words hold the invisible character U+FEFF" + }, + { + "name": "a control character", + "text": "perro luna casa verde tren mar\u0001", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar\u0001" + ], + "result": "error", + "error": "wordkey: the words hold the control character U+0001" + }, + { + "name": "a C1 control character", + "text": "perro luna casa verde tren marŸ", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "marŸ" + ], + "result": "error", + "error": "wordkey: the words hold the control character U+009F" + }, + { + "name": "DEL", + "text": "perro luna casa verde tren mar", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "error", + "error": "wordkey: the words hold the control character U+007F" + }, + { + "name": "an unassigned code point", + "text": "perro luna casa verde tren mar͸", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar͸" + ], + "result": "error", + "error": "wordkey: the words hold U+0378, unassigned in Unicode 18.0.0" + }, + { + "name": "a private use code point is assigned", + "text": "perro luna casa verde tren mar", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "result": "ok" + }, + { + "name": "an emoji", + "text": "perro luna casa verde tren mar 🌍", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar", + "🌍" + ], + "result": "ok" + } + ], + "keys": [ + { + "name": "the vector of §38.1", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", + "round": 1000, + "capsule_id": "000102030405060708090a0b0c0d0e0f", + "key": "fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41", + "recipient": "age1fqk6hflp8q5um2qrl5ckd8lu4x75rhtuncfus7f7up3v3v0zss0stdpqkr" + }, + { + "name": "the next round", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", + "round": 1001, + "capsule_id": "000102030405060708090a0b0c0d0e0f", + "key": "f4faecd9cac596b1a99c399d92cb031955201e5fc0db2c5d67163b1d244ef960", + "recipient": "age1f53tt9sjxc7l635jxc2xnerssquk7dl62j6nhvtjlefjwv5elgqsy2h9ae" + }, + { + "name": "another capsule_id", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", + "round": 1000, + "capsule_id": "000102030405060708090a0b0c0d0e0e", + "key": "08fe6614ea4bbcfc324c69c370d22ad5d32e09333606b565c3831fe0110d2fe2", + "recipient": "age1e2quqz6svvjqa5wrpwlkyr9362vqw8qfz43thq39977sts5wj4sqv6ep86" + }, + { + "name": "another chain hash", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "chain_hash": "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce", + "round": 1000, + "capsule_id": "000102030405060708090a0b0c0d0e0f", + "key": "5da2ab56a8c841ca05970470bca4c7c108543acf2e4129e6743a09c0443577ed", + "recipient": "age1zn5cqedknwdlqsts23azduere4zlugc3fvz7m0sfrhcsfpz05axswl0km6" + }, + { + "name": "round 2^53 - 1", + "words": [ + "perro", + "luna", + "casa", + "verde", + "tren", + "mar" + ], + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", + "round": 9007199254740991, + "capsule_id": "000102030405060708090a0b0c0d0e0f", + "key": "6077e1675c84264fbe03a33abb1359520fc6f9fdb9b293603779127327bce7d4", + "recipient": "age1fjv9e2a6vd7pcr20llzsxthauam80snlskxcrr7rp0ulvy62xyuqt3v3jf" + }, + { + "name": "words that are not ASCII", + "words": [ + "abaco", + "arbol", + "nino", + "σασ", + "nandu", + "straße" + ], + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", + "round": 1000, + "capsule_id": "ffeeddccbbaa99887766554433221100", + "key": "f64bce685596c1db68058125c0caf7cf4ab949d29d2829dff5627da5f758e6cd", + "recipient": "age1vc7tfsz048an895g5nxpkpj99cd54j44c3qv95kg57yu3klfxfdstpvqm6" + } + ] +}