v0.16: the key of words in the recovery annex, and a full last chunk

scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.

Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 8 hours ago
parent b57033813d
commit 7ef9e61c60

@ -11,6 +11,7 @@ import (
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
@ -24,6 +25,7 @@ import (
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
"g.activething.com/go/DateKeys/wordkey"
)
const fixtureDir = "../testdata/fixtures"
@ -600,6 +602,33 @@ func TestFixtureRecipients(t *testing.T) {
}
}
// format3_time_and_key_words opens with the identity that its words give
// (spec §38.1): the text of the annex vector of v0.16, 79.7, with capitals,
// accents, two spaces and a tab.
func TestFixtureWords(t *testing.T) {
f := loadFixture(t, "format3_time_and_key_words")
if f.WordsText == "" || len(f.ids) != 1 || f.AccessKeyFile != "" {
t.Fatalf("the record: words %q, %d identities, .dkk %q", f.WordsText, len(f.ids), f.AccessKeyFile)
}
words := wordkey.Normalize(f.WordsText)
if strings.Join(words, " ") != "nandu pinguino camion arbol eter ola" {
t.Fatalf("words %q", words)
}
capsuleID, err := hex.DecodeString(f.CapsuleID)
if err != nil {
t.Fatal(err)
}
id, err := wordkey.Identity(words, profile.Quicknet().ChainHash[:], f.Release.Round, capsuleID)
if err != nil || id.String() != f.Identities[0] {
t.Fatalf("the identity of the words: %v", err)
}
o := f.openOptions(t)
o.Identities = []age.Identity{id}
if _, err := capsule.Open(context.Background(), io.Discard, bytes.NewReader(f.dkc), o); err != nil {
t.Fatalf("open with the words: %v", err)
}
}
// A non-seekable reader works too: only the capsule_digest shortcut is skipped.
func TestOpenFromPlainReader(t *testing.T) {
for _, name := range []string{"time_only", "time_and_key_portable", "format2_time_only", "format2_time_and_key_portable"} {

@ -520,8 +520,8 @@ func TestInspectJSONGoldens(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(names) != len(dkcs) || len(dkcs) != 26 {
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 26)", len(names), len(dkcs))
if len(names) != len(dkcs) || len(dkcs) != 28 {
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 28)", len(names), len(dkcs))
}
t.Chdir(fixtures)
for _, path := range names {

@ -60,8 +60,12 @@ type DKCFixture struct {
IdentityStanzas []int `json:"identity_stanzas,omitempty"`
AccessKeyFile string `json:"access_key_file,omitempty"`
Identities []string `json:"identities,omitempty"`
ControlCBOR string `json:"control_cbor"`
PayloadIdentity string `json:"payload_identity"`
// WordsText is the text of the words of a key of words, as the person
// types it (spec §38.1, annex 79.7 of v0.16); the identity it gives is
// in Identities.
WordsText string `json:"words_text,omitempty"`
ControlCBOR string `json:"control_cbor"`
PayloadIdentity string `json:"payload_identity"`
// PayloadLength is L, the length of the content: the plaintext the
// reader delivers in formats 1 and 2, and BODY in format 3, whose files
// Files describes. In formats 2 and 3 the plaintext of PAYLOAD_AGE is

@ -58,6 +58,7 @@ import (
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
"g.activething.com/go/DateKeys/wordkey"
)
func main() {
@ -422,7 +423,11 @@ type spec struct {
// configure, when not nil, sets what only this fixture needs in the
// options of EncryptFiles: a signer with certificates, or a sealer.
configure func(o *capsule.EncryptOptions) error
body func() ([]byte, error)
// words, when not empty, is the text of a key of words, a credential of
// a time_and_key fixture (spec §38.1): its identity goes in the record
// with the identities.
words string
body func() ([]byte, error)
// area, when not 0, is the security area of a fixture of an earlier
// version, which EncryptFiles writes only for test vectors: 512 bytes in
// the fixtures of v0.10, which are compatibility fixtures (spec §67).
@ -476,6 +481,10 @@ func specs() []spec {
{path: "música/canción.txt", content: []byte("La, la, la.\n"), mtime: when},
}
report := []file3{{path: "informe.txt", content: []byte(strings.Repeat("Informe trimestral, sin cifras.\n", 625)), mtime: when}}
// fullChunk makes BODY 65536 bytes with the area of 32 KiB, so that the
// plaintext of PAYLOAD_AGE, P = 65536 with bloque256, is one full STREAM
// chunk, the last one (spec v0.16, 79.5).
fullChunk := []file3{{path: "bloque.bin", content: patterned("bloque", fullChunkFile), mtime: when}}
secret := []file3{{path: "secreto.txt", content: []byte("DateKeys fixture opened with a portable .dkk.\n"), mtime: when}}
return []spec{
{name: "time_only", format: f1, description: "time_only capsule, two STREAM chunks, no extensions", round: 1000, policy: capsule.TimeOnly, plaintext: large},
@ -537,6 +546,8 @@ func specs() []spec {
}},
{name: "format3_unsigned", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, as format3_signed, without a signature: the area of 32 KiB of spec v0.11 holds the empty security, and P is the one of format3_signed", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note},
{name: "format3_signed", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by a test key whose seed the record gives: verdict F4, and the commitments and the message of the signature", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note, signer: signerSeed},
{name: "format3_time_and_key_words", format: f3, description: "format 3 time_and_key capsule with one credential, a key of words, and 15 dummies: the text of the vector of the annex of spec v0.16 (79.7), «Ñandú», two spaces, «PINGÜINO», a tab and «camión árbol Éter ola», whose words are «nandu pinguino camion arbol eter ola»", round: 1000, policy: capsule.TimeAndKey, padding: capsule.Reforzado, files: secret, words: annexWords},
{name: "format3_full_chunk", format: f3, description: "format 3 time_only capsule with padding code 1 (bloque256) and one file, whose BODY and P are 65536 bytes: PAYLOAD_AGE ends in a full STREAM chunk, which the annex of spec v0.16 (79.5) allows", round: 1000, policy: capsule.TimeOnly, padding: capsule.Bloque256, files: fullChunk},
{name: "format3_signed_cms", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, signed with alg 2 by two test certificates, an ECDSA P-256 one and an RSA 2048 one, each sealed by a test time-stamping authority before the round time: verdict F6, with the certificates, the commitments, SIGNERS and the result of each signer in the record", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note, configure: configureCMS},
{name: "format3_note", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, and the public note «Cartas del viaje a Lisboa» in the noncritical array of PUBLIC_HEADER (spec v0.11, §24.1)", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note,
configure: func(o *capsule.EncryptOptions) error { o.PublicNote = "Cartas del viaje a Lisboa"; return nil }},
@ -696,6 +707,9 @@ func write(s spec) (*written, error) {
return nil, err
}
}
if s.words != "" {
opts.Words = wordkey.Normalize(s.words)
}
if s.signer != nil {
k, err := authorkey.NewFromSeed(s.signer)
if err != nil {
@ -713,9 +727,30 @@ func write(s spec) (*written, error) {
return nil, err
}
w.dkc, w.portable = dkc.Bytes(), res.PortableKey
if s.words != "" {
id, err := wordkey.Identity(opts.Words, p.ChainHash[:], s.round, res.CapsuleID[:])
if err != nil {
return nil, err
}
w.ids = append(w.ids, id)
}
if s.name == "format3_full_chunk" && (res.Length != fullChunkBody || res.PaddedLength != fullChunkBody) {
return nil, fmt.Errorf("format3_full_chunk: L = %d and P = %d, not %d: change fullChunkFile", res.Length, res.PaddedLength, fullChunkBody)
}
return w, nil
}
// annexWords is the text of the second vector of the annex of spec v0.16,
// 79.7: «Ñandú», two spaces, «PINGÜINO», a tab and «camión árbol Éter ola».
const annexWords = "\u00d1and\u00fa PING\u00dcINO\tcami\u00f3n \u00e1rbol \u00c9ter ola"
// fullChunkBody is the length of BODY and of P in format3_full_chunk, one
// STREAM chunk, and fullChunkFile the length of its file that gives it.
const (
fullChunkBody = 65536
fullChunkFile = 32637
)
// generate writes the fixture s and records every intermediate value,
// recovered by opening it layer by layer.
func generate(dir string, s spec) error {
@ -750,7 +785,7 @@ func generate(dir string, s spec) error {
return err
}
control := inner
f := testkit.DKCFixture{Description: s.description, File: s.name + ".dkc", PlaintextFile: s.name + ".plaintext"}
f := testkit.DKCFixture{Description: s.description, File: s.name + ".dkc", PlaintextFile: s.name + ".plaintext", WordsText: s.words}
var dkkFile string
var dkkBytes []byte
if s.policy == capsule.TimeAndKey {

@ -67,6 +67,15 @@ var wordSpaces = []rune{
// The six words of the vector of spec §38.1.
const sixWords = "perro luna casa verde tren mar"
// The second vector of the annex of spec v0.16, 79.7: «Ñandú», two spaces,
// «PINGÜINO», a tab and «camión árbol Éter ola», also with the acute accent,
// the diaeresis and the tilde as marks after their letter, and its words.
const (
annexWordsText = "\u00d1and\u00fa PING\u00dcINO\tcami\u00f3n \u00e1rbol \u00c9ter ola"
annexWordsMarks = "N\u0303andu\u0301 PINGU\u0308INO\tcamio\u0301n a\u0301rbol E\u0301ter ola"
annexWords = "nandu pinguino camion arbol eter ola"
)
// 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) {
@ -103,6 +112,10 @@ func WordKeyVectors() (WordKeyVectorFile, error) {
{"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"}},
// The second vector of the annex of spec v0.16, 79.7, which its
// normalization without tables gives too.
{"the text of the annex of v0.16", annexWordsText, strings.Fields(annexWords)},
{"the text of the annex of v0.16, with its marks apart", annexWordsMarks, strings.Fields(annexWords)},
}
for _, s := range wordSpaces {
norm = append(norm, struct {
@ -171,6 +184,7 @@ func WordKeyVectors() (WordKeyVectorFile, error) {
capsuleID, want string
}{
{"the vector of §38.1", sixWords, quicknet, 1000, id0, "fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41"},
{"the second vector of the annex of v0.16, 79.7", annexWordsText, quicknet, 1000, id0, "273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7"},
{"the next round", sixWords, quicknet, 1001, id0, ""},
{"another capsule_id", sixWords, quicknet, 1000, "000102030405060708090a0b0c0d0e0e", ""},
{"another chain hash", sixWords, mainnet, 1000, id0, ""},

@ -4,11 +4,17 @@
// software DateKeys" of the specification: everything it needs is a generic
// BLS12-381 library (drand/kyber-bls12381), the age library (filippo.io/age)
// for the X25519 layers, the Go standard library and golang.org/x/crypto for
// ChaCha20-Poly1305. It does not import the DateKeys module, nor drand or
// ChaCha20-Poly1305, and PBKDF2 of the standard library for a key of words.
// It does not import the DateKeys module, nor drand or
// tlock. The tlock layer and the age file that it protects, whose file key no
// age tool accepts, are written out here step by step.
//
// go run ./scripts/recovery -dkc FILE.dkc -release FILE [-dkk FILE.dkk] -out PATH [-body FILE]
// go run ./scripts/recovery -dkc FILE.dkc -release FILE [-dkk FILE.dkk | -words FILE [-unicodedata FILE]] -out PATH [-body FILE]
//
// A time_and_key capsule opens with its .dkk (-dkk) or with the words of a
// key of words, read from the text file -words (annex 79.7). Words of the
// DateKeys lists normalize without tables; any other text needs
// UnicodeData.txt of Unicode 18.0.0 (-unicodedata), checked by its SHA-256.
//
// FILE is the release object of the round of the capsule (spec §47.1), from
// any source: an archive, a cache service or any copy. Its signature is
@ -987,7 +993,16 @@ type result struct {
body *body // format 3 only
}
func recoverCapsule(dkc, relObj, dkk []byte, log io.Writer) (*result, error) {
// credential is what opens the access layer of a time_and_key capsule: a
// .dkk, or the text of the words of a key of words, with UnicodeData.txt
// when the text needs it.
type credential struct {
dkk []byte
words *string
ucd *unicodeData
}
func recoverCapsule(dkc, relObj []byte, key credential, log io.Writer) (*result, error) {
c, err := parseCapsule(dkc)
if err != nil {
return nil, err
@ -1016,17 +1031,29 @@ func recoverCapsule(dkc, relObj, dkk []byte, log io.Writer) (*result, error) {
ctl := inner
if c.policy == 1 {
if dkk == nil {
return nil, errors.New("time_and_key capsule: it needs its .dkk (-dkk)")
}
mat, err := readAccessKey(dkk, c.capsuleID)
if err != nil {
return nil, err
var id []byte
from := "the .dkk"
switch {
case key.words != nil:
words, err := normalizeWords(*key.words, key.ucd)
if err != nil {
return nil, err
}
if id, err = wordKey(words, c.round, c.capsuleID); err != nil {
return nil, err
}
from = fmt.Sprintf("the %d words", len(words))
case key.dkk != nil:
if id, err = readAccessKey(key.dkk, c.capsuleID); err != nil {
return nil, err
}
default:
return nil, errors.New("time_and_key capsule: it needs its .dkk (-dkk) or the words of its key (-words)")
}
if ctl, err = ageDecryptX25519(inner, mat); err != nil {
if ctl, err = ageDecryptX25519(inner, id); err != nil {
return nil, fmt.Errorf("access layer: %w", err)
}
fmt.Fprintln(log, "access layer: opened with the .dkk")
fmt.Fprintln(log, "access layer: opened with "+from)
}
cc, err := parseControl(ctl, c.format)
@ -1059,13 +1086,15 @@ func run(args []string, log io.Writer) error {
dkcPath := fs.String("dkc", "", "the capsule (.dkc)")
relPath := fs.String("release", "", "the release object of its round")
dkkPath := fs.String("dkk", "", "the access key (.dkk), for time_and_key")
wordsPath := fs.String("words", "", "a text file with the words of a key of words, for time_and_key")
ucdPath := fs.String("unicodedata", "", "UnicodeData.txt of Unicode 18.0.0, for words outside the normalization without tables")
out := fs.String("out", "", "output: a file in formats 1 and 2, a directory in format 3")
bodyPath := fs.String("body", "", "optional: write the L bytes (content, or BODY in format 3)")
if err := fs.Parse(args); err != nil {
return err
}
if *dkcPath == "" || *relPath == "" || *out == "" {
return errors.New("usage: recovery -dkc FILE.dkc -release FILE [-dkk FILE.dkk] -out PATH [-body FILE]")
return errors.New("usage: recovery -dkc FILE.dkc -release FILE [-dkk FILE.dkk | -words FILE [-unicodedata FILE]] -out PATH [-body FILE]")
}
dkc, err := os.ReadFile(*dkcPath)
if err != nil {
@ -1075,13 +1104,30 @@ func run(args []string, log io.Writer) error {
if err != nil {
return err
}
var dkk []byte
var key credential
if *dkkPath != "" {
if dkk, err = os.ReadFile(*dkkPath); err != nil {
if key.dkk, err = os.ReadFile(*dkkPath); err != nil {
return err
}
}
if *wordsPath != "" {
b, err := os.ReadFile(*wordsPath)
if err != nil {
return err
}
text := string(b)
key.words = &text
}
if *ucdPath != "" {
b, err := os.ReadFile(*ucdPath)
if err != nil {
return err
}
if key.ucd, err = parseUnicodeData(b); err != nil {
return err
}
}
res, err := recoverCapsule(dkc, relObj, dkk, log)
res, err := recoverCapsule(dkc, relObj, key, log)
if err != nil {
return err
}

@ -35,6 +35,7 @@ type fixtureRecord struct {
PlaintextFile string `json:"plaintext_file"`
PlaintextSHA256 string `json:"plaintext_sha256"`
AccessKeyFile string `json:"access_key_file"`
WordsText string `json:"words_text"`
Files []struct {
Path string `json:"path"`
Size uint64 `json:"size"`
@ -136,7 +137,9 @@ func TestRecoverFixtures(t *testing.T) {
"format3_time_and_key_portable",
"format3_tree",
"format3_signed",
"format2_time_only", // two STREAM chunks
"format3_time_and_key_words", // opened with its words (annex 79.7)
"format3_full_chunk", // PAYLOAD_AGE ends in a full chunk (79.5)
"format2_time_only", // two STREAM chunks
"format2_time_and_key_portable",
"format2_time_and_key_sixteen", // round 2000; no .dkk, so only its release is checked below
"time_only",
@ -145,13 +148,17 @@ func TestRecoverFixtures(t *testing.T) {
t.Run(name, func(t *testing.T) {
rec, dkc, relObj, dkk := loadFixture(t, name)
if name == "format2_time_and_key_sixteen" {
_, err := recoverCapsule(dkc, relObj, nil, io.Discard)
_, err := recoverCapsule(dkc, relObj, credential{}, io.Discard)
if err == nil || !strings.Contains(err.Error(), "needs its .dkk") {
t.Fatalf("got %v, want the .dkk to be required", err)
}
return
}
res, err := recoverCapsule(dkc, relObj, dkk, io.Discard)
key := credential{dkk: dkk}
if rec.WordsText != "" {
key.words = &rec.WordsText
}
res, err := recoverCapsule(dkc, relObj, key, io.Discard)
if err != nil {
t.Fatal(err)
}
@ -238,7 +245,7 @@ func TestBadReleases(t *testing.T) {
{"wrong type tag", "type tag", bytes.Replace(encodeRelease(chain, 1000, sig), []byte("release"), []byte("relaxed"), 1)},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := recoverCapsule(dkc, tc.relObj, nil, io.Discard)
_, err := recoverCapsule(dkc, tc.relObj, credential{}, io.Discard)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("got %v, want an error with %q", err, tc.want)
}
@ -388,7 +395,7 @@ func TestTampered(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
bad := bytes.Clone(dkc)
bad[tc.at] ^= 0x01
if _, err := recoverCapsule(bad, relObj, nil, io.Discard); err == nil {
if _, err := recoverCapsule(bad, relObj, credential{}, io.Discard); err == nil {
t.Fatal("a tampered capsule opened")
}
})

@ -0,0 +1,220 @@
package main
import (
"bufio"
"bytes"
"crypto/pbkdf2"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
)
// ---------------------------------------------------------------------------
// The key of words (annex 79.7, spec §38.1): words give the X25519 identity
// of a credential of a time_and_key capsule.
//
// P = the words, normalized, in UTF-8, separated by a space
// S = "DateKeys llave de palabras v2|" || chain_hash || "|" || round || "|" || capsule_id
// id = PBKDF2-HMAC-SHA256(P, S, 600000 iterations, 32 bytes)
//
// with the chain hash and capsule_id in lower-case hexadecimal and the round
// in decimal.
const wordKeyIterations = 600000
// wordKey derives the identity of the words of the capsule.
func wordKey(words []string, round uint64, capsuleID []byte) ([]byte, error) {
p := strings.Join(words, " ")
s := "DateKeys llave de palabras v2|" + quicknetChainHash + "|" + strconv.FormatUint(round, 10) + "|" + hex.EncodeToString(capsuleID)
return pbkdf2.Key(sha256.New, p, []byte(s), wordKeyIterations, 32)
}
// ---------------------------------------------------------------------------
// The normalization without tables (79.7): for a text of printable ASCII,
// the ASCII spaces, á, é, í, ó, ú, ü, ñ and their capitals, and the marks
// U+0300 to U+036F, which is what any word of the DateKeys lists gives. It
// is exactly the normalization of §38.1 for that text.
// latinBase maps the letters of the recipe to their base letter, in lower
// case.
var latinBase = map[rune]rune{
0x00e1: 'a', 0x00c1: 'a', // á Á
0x00e9: 'e', 0x00c9: 'e', // é É
0x00ed: 'i', 0x00cd: 'i', // í Í
0x00f3: 'o', 0x00d3: 'o', // ó Ó
0x00fa: 'u', 0x00fc: 'u', 0x00da: 'u', 0x00dc: 'u', // ú ü Ú Ü
0x00f1: 'n', 0x00d1: 'n', // ñ Ñ
}
func asciiSpace(r rune) bool { return r == ' ' || r >= 0x09 && r <= 0x0d }
// normalizeSimple applies the recipe without tables, and returns false when
// the text holds a character outside it.
func normalizeSimple(text string) ([]string, bool) {
var sb strings.Builder
for _, r := range text {
switch {
case r >= 0x300 && r <= 0x36f: // 1. the marks go
case latinBase[r] != 0: // 2. the letters with marks, to their base
sb.WriteRune(latinBase[r])
case r >= 'A' && r <= 'Z': // 3. A to Z, to a to z
sb.WriteRune(r + 'a' - 'A')
case r >= 0x21 && r <= 0x7e || asciiSpace(r):
sb.WriteRune(r)
default:
return nil, false
}
}
return strings.FieldsFunc(sb.String(), asciiSpace), true // 4.
}
// ---------------------------------------------------------------------------
// The full normalization (79.7), for any other text: NFD of UAX #15, without
// U+0300 to U+036F, the simple lower case of each code point, and the split
// at the spaces of §38.1. It reads UnicodeData.txt of Unicode 18.0.0, which
// the annex names by its SHA-256.
const unicodeDataSHA256 = "0736451de439ae7baf1425136617da495e09ee5afbe6e394374db7009ea08950"
// unicodeData holds what the normalization takes from UnicodeData.txt: the
// canonical decomposition (field 5, without the tagged ones), the canonical
// combining class (field 3) and the simple lower case (field 13).
type unicodeData struct {
decomposition map[rune][]rune
class map[rune]int
lower map[rune]rune
}
// parseUnicodeData reads UnicodeData.txt, which must be that of Unicode
// 18.0.0: another version can give other words.
func parseUnicodeData(b []byte) (*unicodeData, error) {
sum := sha256.Sum256(b)
if hex.EncodeToString(sum[:]) != unicodeDataSHA256 {
return nil, fmt.Errorf("UnicodeData.txt has the SHA-256 %x, not that of Unicode 18.0.0, %s", sum, unicodeDataSHA256)
}
u := &unicodeData{decomposition: map[rune][]rune{}, class: map[rune]int{}, lower: map[rune]rune{}}
sc := bufio.NewScanner(bytes.NewReader(b))
for sc.Scan() {
f := strings.Split(sc.Text(), ";")
if len(f) != 15 {
return nil, fmt.Errorf("UnicodeData.txt: a line of %d fields", len(f))
}
cp, err := codePoint(f[0])
if err != nil {
return nil, err
}
// A range, <…, First> to <…, Last>, has no decomposition, class or
// lower case: its two lines say nothing more.
if c, _ := strconv.Atoi(f[3]); c != 0 {
u.class[cp] = c
}
if f[5] != "" && !strings.HasPrefix(f[5], "<") {
for _, h := range strings.Fields(f[5]) {
d, err := codePoint(h)
if err != nil {
return nil, err
}
u.decomposition[cp] = append(u.decomposition[cp], d)
}
}
if f[13] != "" {
if u.lower[cp], err = codePoint(f[13]); err != nil {
return nil, err
}
}
}
return u, sc.Err()
}
func codePoint(h string) (rune, error) {
n, err := strconv.ParseUint(h, 16, 32)
if err != nil || n > 0x10ffff {
return 0, fmt.Errorf("UnicodeData.txt: %q is not a code point", h)
}
return rune(n), nil
}
// The Hangul syllables decompose by computation (Unicode, §3.12).
const (
hangulS, hangulL, hangulV, hangulT = 0xac00, 0x1100, 0x1161, 0x11a7
hangulVCount, hangulTCount = 21, 28
hangulCount = 19 * hangulVCount * hangulTCount
)
// decompose appends the full canonical decomposition of r.
func (u *unicodeData) decompose(out []rune, r rune) []rune {
if s := r - hangulS; s >= 0 && s < hangulCount {
out = append(out, hangulL+s/(hangulVCount*hangulTCount), hangulV+s%(hangulVCount*hangulTCount)/hangulTCount)
if t := s % hangulTCount; t != 0 {
out = append(out, hangulT+t)
}
return out
}
d, ok := u.decomposition[r]
if !ok {
return append(out, r)
}
for _, x := range d {
out = u.decompose(out, x)
}
return out
}
// nfd is the NFD of text: the full canonical decomposition, then the
// canonical ordering of each run of marks by their combining class.
func (u *unicodeData) nfd(text string) []rune {
var out []rune
for _, r := range text {
out = u.decompose(out, r)
}
for i := 1; i < len(out); i++ {
for j := i; j > 0; j-- {
a, b := u.class[out[j-1]], u.class[out[j]]
if b == 0 || a <= b {
break
}
out[j-1], out[j] = out[j], out[j-1]
}
}
return out
}
// wordSpace is a space of §38.1, step 4: those of Go's unicode.IsSpace.
func wordSpace(r rune) bool {
switch {
case asciiSpace(r), r == 0x85, r == 0xa0, r == 0x1680, r >= 0x2000 && r <= 0x200a,
r == 0x2028, r == 0x2029, r == 0x202f, r == 0x205f, r == 0x3000:
return true
}
return false
}
// normalize applies the full normalization.
func (u *unicodeData) normalize(text string) []string {
var sb strings.Builder
for _, r := range u.nfd(text) {
if r >= 0x300 && r <= 0x36f {
continue
}
if l, ok := u.lower[r]; ok {
r = l
}
sb.WriteRune(r)
}
return strings.FieldsFunc(sb.String(), wordSpace)
}
// normalizeWords normalizes text without tables when it can, and with u
// otherwise.
func normalizeWords(text string, u *unicodeData) ([]string, error) {
if words, ok := normalizeSimple(text); ok {
return words, nil
}
if u == nil {
return nil, errors.New("the words hold a character outside the normalization without tables of the annex (79.7): give UnicodeData.txt of Unicode 18.0.0 with -unicodedata")
}
return u.normalize(text), nil
}

@ -0,0 +1,154 @@
package main
import (
"encoding/hex"
"io"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
// unicodeDataPath is the copy of UnicodeData.txt of Unicode 18.0.0 that
// internal/pathrule/gen reads; it is not committed. The tests of the full
// normalization skip without it.
const unicodeDataPath = "../../.cache/unicode/18.0.0/UnicodeData.txt"
func loadUnicodeData(t *testing.T) *unicodeData {
t.Helper()
b, err := os.ReadFile(unicodeDataPath)
if err != nil {
t.Skipf("no UnicodeData.txt of Unicode 18.0.0 in %s: %v", unicodeDataPath, err)
}
u, err := parseUnicodeData(b)
if err != nil {
t.Fatal(err)
}
return u
}
// The vectors of the annex, 79.7: the words without tables and their key,
// for the chain hash of Quicknet, round 1000 and capsule_id 00 to 0f.
func TestAnnexWordVectors(t *testing.T) {
id := unhex(t, "000102030405060708090a0b0c0d0e0f")
for _, c := range []struct{ text, words, key string }{
{"perro luna casa verde tren mar", "perro luna casa verde tren mar", "fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41"},
{"Ñandú PINGÜINO\tcamión árbol Éter ola", "nandu pinguino camion arbol eter ola", "273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7"},
{"Ñandú PINGÜINO\tcamión árbol Éter ola", "nandu pinguino camion arbol eter ola", "273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7"},
} {
words, ok := normalizeSimple(c.text)
if !ok || strings.Join(words, " ") != c.words {
t.Fatalf("%q: %q, %v", c.text, words, ok)
}
key, err := wordKey(words, 1000, id)
if err != nil || hex.EncodeToString(key) != c.key {
t.Fatalf("%q: key %x, %v", c.text, key, err)
}
}
}
// The recipe without tables refuses what it does not cover, and then the
// words need UnicodeData.txt.
func TestNormalizeSimpleRefuses(t *testing.T) {
for _, text := range []string{"ça va", "straße", "à la", "σασ", "a b", "Å", "a​b"} {
if _, ok := normalizeSimple(text); ok {
t.Errorf("%q: the recipe without tables does not cover it", text)
}
if _, err := normalizeWords(text, nil); err == nil || !strings.Contains(err.Error(), "-unicodedata") {
t.Errorf("%q: %v", text, err)
}
}
// The whole of printable ASCII stays, and only A to Z change.
var ascii []rune
for r := rune(0x21); r <= 0x7e; r++ {
ascii = append(ascii, r)
}
words, ok := normalizeSimple(string(ascii))
if !ok || len(words) != 1 || words[0] != strings.ToLower(string(ascii)) {
t.Errorf("printable ASCII: %q, %v", words, ok)
}
}
// The full normalization gives the words of every case of wordkey.json, and
// the recipe without tables, where it applies, the same.
func TestNormalizeVectors(t *testing.T) {
u := loadUnicodeData(t)
var f struct {
Normalize []struct {
Name string `json:"name"`
Text string `json:"text"`
Words []string `json:"words"`
} `json:"normalize"`
}
readJSON(t, filepath.Join(vectorsDir, "wordkey.json"), &f)
if len(f.Normalize) < 40 {
t.Fatalf("%d cases of normalize", len(f.Normalize))
}
simple := 0
for _, c := range f.Normalize {
got := u.normalize(c.Text)
if got == nil {
got = []string{}
}
if !slices.Equal(got, c.Words) {
t.Errorf("%s: %q, want %q", c.Name, got, c.Words)
}
if words, ok := normalizeSimple(c.Text); ok {
simple++
if words == nil {
words = []string{}
}
if !slices.Equal(words, c.Words) {
t.Errorf("%s, without tables: %q, want %q", c.Name, words, c.Words)
}
}
}
if simple < 8 {
t.Errorf("only %d cases without tables", simple)
}
}
// Only UnicodeData.txt of Unicode 18.0.0 is accepted: another version can
// give other words.
func TestUnicodeDataVersion(t *testing.T) {
b, err := os.ReadFile(unicodeDataPath)
if err != nil {
t.Skip(err)
}
b[len(b)-2] ^= 1
if _, err := parseUnicodeData(b); err == nil || !strings.Contains(err.Error(), "not that of Unicode 18.0.0") {
t.Fatalf("a changed UnicodeData.txt: %v", err)
}
}
// format3_time_and_key_words opens with the text of its words, which needs
// no tables, and with UnicodeData.txt too.
func TestRecoverWithWords(t *testing.T) {
rec, dkc, relObj, _ := loadFixture(t, "format3_time_and_key_words")
if rec.WordsText == "" {
t.Fatal("the record has no words_text")
}
text := rec.WordsText
res, err := recoverCapsule(dkc, relObj, credential{words: &text}, io.Discard)
if err != nil || res.format != 3 || len(res.body.files) != 1 {
t.Fatalf("%v", err)
}
other := "nandu pinguino camion arbol eter mar"
if _, err := recoverCapsule(dkc, relObj, credential{words: &other}, io.Discard); err == nil || !strings.Contains(err.Error(), "access layer") {
t.Fatalf("other words: %v", err)
}
if b, err := os.ReadFile(unicodeDataPath); err == nil {
u, err := parseUnicodeData(b)
if err != nil {
t.Fatal(err)
}
if _, err := recoverCapsule(dkc, relObj, credential{words: &text, ucd: u}, io.Discard); err != nil {
t.Fatal(err)
}
}
}
type ioDiscard struct{}
func (ioDiscard) Write(p []byte) (int, error) { return len(p), nil }

@ -28,4 +28,14 @@ recover_one() {
recover_one format3_single 1000
recover_one format3_time_and_key_portable 1000 -dkk "$fx/format3_time_and_key_portable.dkk"
# The key of words of the annex (79.7): the text of its second vector,
# "Ñandú", two spaces, "PINGÜINO", a tab and "camión árbol Éter ola", which
# normalizes without tables.
printf '\xc3\x91and\xc3\xba PING\xc3\x9cINO\tcami\xc3\xb3n \xc3\xa1rbol \xc3\x89ter ola' >"$tmp/words.txt"
recover_one format3_time_and_key_words 1000 -words "$tmp/words.txt"
# PAYLOAD_AGE of 65536 bytes of plaintext: one full STREAM chunk, the last
# one (79.5).
recover_one format3_full_chunk 1000
echo "recovery check passed"

19
testdata/README.md vendored

@ -94,7 +94,7 @@ extension and a noncritical CONTROL_CBOR extension. The release that opens each
capsule, a published Quicknet signature, is in its `<name>.json`, so they all
decrypt offline.
Fourteen are in format 3. Their plaintext file is BODY, L bytes: the frame, the
Sixteen are in format 3. Their plaintext file is BODY, L bytes: the frame, the
security area, the head and the files (spec §29.2).
| Fixture | Policy | Files | Comment | L | Padding code | P | Area | Verdicts |
@ -113,6 +113,8 @@ security area, the head and the files (spec §29.2).
| `format3_signed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S0 |
| `format3_signed_cms` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F6, S0 |
| `format3_sealed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S4 |
| `format3_time_and_key_words` | `time_and_key`, a key of words and 15 dummies | 1, `secreto.txt`, with mtime | — | 32944 | 2 | 34816 | 32768 | F0, S0 |
| `format3_full_chunk` | `time_only` | 1 of 32637 bytes, with mtime | — | 65536 | 1 | 65536 | 32768 | F0, S0 |
The first five were written by a writer of v0.10, with the area of 512 bytes.
The next four only a generator of test vectors may write (spec §62.1 rule 13):
@ -153,7 +155,20 @@ their record has a `signature` object, and `seal` in the third:
`SEAL_SUBJECT`. The record has `seal`: `seal_subject`, the `token` in
hexadecimal, the `holder` of the authority as §29.7 shows it, and the time.
In the three, the record gives the commitments `control_commit`, `head_digest`
The last two are of v0.16, for the annex of recovery (spec §79):
- `format3_time_and_key_words` opens with a key of words (spec §38.1): the
text of the second vector of the annex, 79.7, «Ñandú», two spaces,
«PINGÜINO», a tab and «camión árbol Éter ola», whose words are «nandu
pinguino camion arbol eter ola». The record gives the text in `words_text`
and the identity it derives with this capsule_id in `identities`, with its
stanza in `identity_stanzas`, so that a reader without words opens it too.
- `format3_full_chunk`: BODY and P measure 65536 bytes, so PAYLOAD_AGE ends
in a full STREAM chunk of age, the last one, which the annex of v0.16
allows (79.5). `scripts/recovery_check.sh` opens both following only the
annex.
In the three signed ones, the record gives the commitments `control_commit`, `head_digest`
and `signers_digest`, the text `author_message` and its `author_code`, and the
exact content of key 2 of `SECURITY_CBOR`. An implementation checks them from
the control, the head and the security area of the fixture, and the verdicts

Binary file not shown.

@ -0,0 +1,61 @@
{
"file": "format3_full_chunk.dkc",
"format": 3,
"capsule_id": "9c672412223e65667407568b2ffab62d",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_only",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=458"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=9c672412223e65667407568b2ffab62d datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_only profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,152 @@
{
"description": "format 3 time_only capsule with padding code 1 (bloque256) and one file, whose BODY and P are 65536 bytes: PAYLOAD_AGE ends in a full STREAM chunk, which the annex of spec v0.16 (79.5) allows",
"spec": "0.15",
"format": 3,
"file": "format3_full_chunk.dkc",
"sha256": "af658967b0b2c79379e25edfe3a785095e9aa2686203e93e9f30dacf27a38684",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b43310300000000000079000001ca",
"public_header": "a5006a646174656b6579636170010102509c672412223e65667407568b2ffab62d037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300400",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "9c672412223e65667407568b2ffab62d",
"access_policy": "time_only",
"structure": "time_only",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "ea2cd41f6216135cd349fceb1891772252e3f90ed945fc71fd73852a982fbf1f",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"QsnJmO1LaffXIjpp0ms0Rh2IXta9VzX38GP6TMYlAHA"
]
}
],
"control_cbor": "a60070646174656b6579732d636f6e74726f6c0103025820ea2cd41f6216135cd349fceb1891772252e3f90ed945fc71fd73852a982fbf1f0358205be72d0295b21d37b6c8380a91d7c875a2dcab7257fa7ea44dcee5ae6280c95a064800000000000100000701",
"payload_identity": "5be72d0295b21d37b6c8380a91d7c875a2dcab7257fa7ea44dcee5ae6280c95a",
"payload_length": 65536,
"padding": 1,
"padded_length": 65536,
"plaintext_file": "format3_full_chunk.plaintext",
"plaintext_sha256": "ec5bfd307b2e36c1b8e232031167401a1f06d205ccade7a054d39914ebf5c9f8",
"area_len": 32768,
"security_cbor": "a20071646174656b6579732d73656375726974790101",
"head_cbor": "a4006d646174656b6579732d68656164010102582029068855227bf0d314be5b3b5e16392b7157581cf62b0c8d156f74017e2f19bf0581a6006a626c6f7175652e62696e01197f7d020003197f7d045820b84764fc9aa813643c9b76145bfdc87673a4b83ccb3cdfaa3c07bbbc5dba560d051a6abcf9c0",
"salt": "29068855227bf0d314be5b3b5e16392b7157581cf62b0c8d156f74017e2f19bf",
"content_offset": 32899,
"files": [
{
"path": "bloque.bin",
"size": 32637,
"start": 0,
"end": 32637,
"sha256": "b84764fc9aa813643c9b76145bfdc87673a4b83ccb3cdfaa3c07bbbc5dba560d",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F0",
"seal": "S0",
"lines": [
"Sin firma de autor."
]
},
"stages": [
{
"step": 1,
"name": "parse DKC1",
"ok": true
},
{
"step": 2,
"name": "prelude",
"ok": true
},
{
"step": 3,
"name": "public header",
"ok": true
},
{
"step": 4,
"name": "header validation",
"ok": true
},
{
"step": 5,
"name": "sealed control structure",
"ok": true
},
{
"step": 6,
"name": "payload structure",
"ok": true
},
{
"step": 7,
"name": "condition",
"ok": true
},
{
"step": 8,
"name": "tlock stanza",
"ok": true
},
{
"step": 9,
"name": "release",
"ok": true
},
{
"step": 10,
"name": "release verification",
"ok": true
},
{
"step": 11,
"name": "open sealed control",
"ok": true
},
{
"step": 12,
"name": "policy structure",
"ok": true
},
{
"step": 14,
"name": "control",
"ok": true
},
{
"step": 15,
"name": "header binding",
"ok": true
},
{
"step": 16,
"name": "payload identity",
"ok": true
},
{
"step": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

Binary file not shown.

@ -0,0 +1,61 @@
{
"file": "format3_time_and_key_words.dkc",
"format": 3,
"capsule_id": "30e865a5c1e148c14410817a65eecfd1",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_and_key",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=2128"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=30e865a5c1e148c14410817a65eecfd1 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_and_key profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,267 @@
{
"description": "format 3 time_and_key capsule with one credential, a key of words, and 15 dummies: the text of the vector of the annex of spec v0.16 (79.7), «Ñandú», two spaces, «PINGÜINO», a tab and «camión árbol Éter ola», whose words are «nandu pinguino camion arbol eter ola»",
"spec": "0.15",
"format": 3,
"file": "format3_time_and_key_words.dkc",
"sha256": "64a11824630b6134892087a4d4ad3ee6e27941513507ad17fc87a7a2b4421e33",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b4331030000000000007900000850",
"public_header": "a5006a646174656b65796361700101025030e865a5c1e148c14410817a65eecfd1037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300401",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "30e865a5c1e148c14410817a65eecfd1",
"access_policy": "time_and_key",
"structure": "time_and_key",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "4bc6ecdcd80e37d0ed1f51ef781db6800564c309e222ba6151665ddd1fe4c99d",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"y+DAEDO0p07P4YDE2pHXFhIpzVKiv+YUku15lbotTjM"
]
}
],
"inner_stanzas": [
{
"type": "X25519",
"args": [
"MSeAnfrz4I+Pn3yhL+/+hkNASJPwQzNQLxeiYu4YeBc"
]
},
{
"type": "X25519",
"args": [
"5VwR2r6e5MknAz//TJrRNRYOtQOeqCaTDdJ+A8QV+S8"
]
},
{
"type": "X25519",
"args": [
"X09hnKn4HOIFwf6H4GfJe3vSG5f5/EqDtV6Bx+OPgFY"
]
},
{
"type": "X25519",
"args": [
"GhhTyyFwZItGXCDKsG3sIcDYJrD9QU45ybqQxjUQUQ4"
]
},
{
"type": "X25519",
"args": [
"SS7ZyiY/U4O6PokUavgTMRMghx4lHDiJ+k+K/rsC8FU"
]
},
{
"type": "X25519",
"args": [
"zchoCmsfxz/dR7YbYGOEoMkSjrDVzOLt5al0CprXG2E"
]
},
{
"type": "X25519",
"args": [
"xg6iz12nCO/jm/rpa4k6aUM1mu2MUm7CwLJsF8qDwUA"
]
},
{
"type": "X25519",
"args": [
"NfHtp8ATUtya6UcudC1FTfiL2SMfrKCFj8V3/slMfEw"
]
},
{
"type": "X25519",
"args": [
"89gYRmkwISvkX1BNb/opcezkVOmNkj7mh89WkniGkWU"
]
},
{
"type": "X25519",
"args": [
"yBQHKKHENrGZfD9qysIoZ/2sMcSXvKClt6VUL4Sx6Sc"
]
},
{
"type": "X25519",
"args": [
"POtK0+b9IiRSRxlNBYm7DPzApiULuJaVWWOSHap4C00"
]
},
{
"type": "X25519",
"args": [
"2dGTxtsPQRUHGAros0o30jqTpEa4+6d5KRt4U86Qx2o"
]
},
{
"type": "X25519",
"args": [
"EKRbRpTDe5KZJgGgsfvnwb0iaHXkzVsqplsmCj3HzHY"
]
},
{
"type": "X25519",
"args": [
"+wHShSAq5PhGXD9ZHs+AwjXxq0TRbcpXwjf46fwW1wc"
]
},
{
"type": "X25519",
"args": [
"HRclh6xyQdsMOSV2MBP0ewe7JB+6EvgTod/4BOYXNAo"
]
},
{
"type": "X25519",
"args": [
"ZIfAt94wDxyQ4Y3WWw54v7H55b26Ye19HFn7USqSwnE"
]
}
],
"identity_stanzas": [
1
],
"identities": [
"AGE-SECRET-KEY-1CWYUF8E9RJ4SYWL8D7WN43M30XHFFFXD6LSZDRYS2WZ8CMUWWENSEXCGDP"
],
"words_text": "Ñandú PINGÜINO\tcamión árbol Éter ola",
"control_cbor": "a60070646174656b6579732d636f6e74726f6c01030258204bc6ecdcd80e37d0ed1f51ef781db6800564c309e222ba6151665ddd1fe4c99d035820a5f41e788e692ea55a3931bc5e25c7e6180ecc1d14235994198f8e00edb7420a064800000000000080b00702",
"payload_identity": "a5f41e788e692ea55a3931bc5e25c7e6180ecc1d14235994198f8e00edb7420a",
"payload_length": 32944,
"padding": 2,
"padded_length": 34816,
"plaintext_file": "format3_time_and_key_words.plaintext",
"plaintext_sha256": "2ff49df00ad9a37446c626be6d0353e94bd3bf41d73a6141e10f77b586abcb67",
"area_len": 32768,
"security_cbor": "a20071646174656b6579732d73656375726974790101",
"head_cbor": "a4006d646174656b6579732d686561640101025820702740f9850339d6907640e1de069200437bf8688288ed4cc735b37bd875eb260581a6006b7365637265746f2e74787401182e020003182e045820937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b051a6abcf9c0",
"salt": "702740f9850339d6907640e1de069200437bf8688288ed4cc735b37bd875eb26",
"content_offset": 32898,
"files": [
{
"path": "secreto.txt",
"size": 46,
"start": 0,
"end": 46,
"sha256": "937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F0",
"seal": "S0",
"lines": [
"Sin firma de autor."
]
},
"stages": [
{
"step": 1,
"name": "parse DKC1",
"ok": true
},
{
"step": 2,
"name": "prelude",
"ok": true
},
{
"step": 3,
"name": "public header",
"ok": true
},
{
"step": 4,
"name": "header validation",
"ok": true
},
{
"step": 5,
"name": "sealed control structure",
"ok": true
},
{
"step": 6,
"name": "payload structure",
"ok": true
},
{
"step": 7,
"name": "condition",
"ok": true
},
{
"step": 8,
"name": "tlock stanza",
"ok": true
},
{
"step": 9,
"name": "access credential",
"ok": true
},
{
"step": 9,
"name": "release",
"ok": true
},
{
"step": 10,
"name": "release verification",
"ok": true
},
{
"step": 11,
"name": "open sealed control",
"ok": true
},
{
"step": 12,
"name": "policy structure",
"ok": true
},
{
"step": 13,
"name": "open access layer",
"ok": true
},
{
"step": 14,
"name": "control",
"ok": true
},
{
"step": 15,
"name": "header binding",
"ok": true
},
{
"step": 16,
"name": "payload identity",
"ok": true
},
{
"step": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

@ -152,6 +152,30 @@
"ab"
]
},
{
"name": "the text of the annex of v0.16",
"text": "Ñandú PINGÜINO\tcamión árbol Éter ola",
"words": [
"nandu",
"pinguino",
"camion",
"arbol",
"eter",
"ola"
]
},
{
"name": "the text of the annex of v0.16, with its marks apart",
"text": "Ñandú PINGÜINO\tcamión árbol Éter ola",
"words": [
"nandu",
"pinguino",
"camion",
"arbol",
"eter",
"ola"
]
},
{
"name": "U+0009 is a space",
"text": "uno\tdos",
@ -648,6 +672,22 @@
"key": "fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41",
"recipient": "age1fqk6hflp8q5um2qrl5ckd8lu4x75rhtuncfus7f7up3v3v0zss0stdpqkr"
},
{
"name": "the second vector of the annex of v0.16, 79.7",
"words": [
"nandu",
"pinguino",
"camion",
"arbol",
"eter",
"ola"
],
"chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
"round": 1000,
"capsule_id": "000102030405060708090a0b0c0d0e0f",
"key": "273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7",
"recipient": "age1zje929pk9ej2kqp4pjra5rs4cly7k7deaeclz0jrp5843u6qcy5qwsvjdh"
},
{
"name": "the next round",
"words": [

Loading…
Cancel
Save

Powered by TurnKey Linux.