//go:build ignore // gen_age_vectors writes the age vectors of stage 2 of datekeys-dart, every // file and every result produced by filippo.io/age v1.3.2 and the agewrap // package of datekeys-go: // // - test/vectors/age.json: age files with X25519 and scrypt recipients, // their truncations and manipulations, a corpus of headers against the // grammar and the limits of the parser, the stanza rules and identities // of agewrap, and the text of Go's error for each; // - test/vectors/age_fixtures.json: the PAYLOAD_AGE of every official // capsule of testdata/ with its payload_identity, and the // INNER_ACCESS_AGE of the time_and_key ones, which OUTER_TIME_AGE seals // and this program opens with the release of the fixture. // // Large files are not stored: a file of more than one STREAM chunk is its // header, its nonce and its file key, and the tests encrypt the documented // plaintext again and check the SHA-256 of the whole file before reading it. // Headers of megabytes are written as parts to repeat. // // It needs the module context of datekeys-go and changes nothing there: // // cd ../datekeys-go && go run ../datekeys-dart/tool/gen_age_vectors.go -out ../datekeys-dart/test/vectors // // The fixtures are those of the testdata/ of datekeys-dart (-testdata), the // copy synced from datekeys-go at the version that this package implements. // // age draws its file keys, nonces and ephemeral shares from crypto/rand, so // each run writes other files; the tests read whatever is committed. package main import ( "bytes" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "flag" "fmt" "io" "log" "os" "path/filepath" "runtime" "slices" "strings" "unicode/utf8" "filippo.io/age" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" "golang.org/x/crypto/scrypt" "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/agewrap" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" ) type obj = map[string]any // testdata is the synced copy of datekeys-dart, read only. var testdata string func h(b []byte) string { return hex.EncodeToString(b) } func sum(b []byte) string { s := sha256.Sum256(b) return h(s[:]) } func check(err error) { if err != nil { log.Fatal(err) } } func mustHex(s string) []byte { b, err := hex.DecodeString(s) check(err) return b } var b64 = base64.RawStdEncoding // pattern is the plaintext of n bytes that the large files encrypt: byte i // is (31·i + 7) mod 256. func pattern(n int) []byte { b := make([]byte, n) for i := range b { b[i] = byte(31*i + 7) } return b } // eph is the i-th ephemeral scalar of the crafted stanzas. func eph(i int) []byte { s := sha256.Sum256([]byte{byte(i)}) return s[:] } func encrypt(plain []byte, rs ...age.Recipient) []byte { var buf bytes.Buffer w, err := age.Encrypt(&buf, rs...) check(err) _, err = w.Write(plain) check(err) check(w.Close()) return buf.Bytes() } // fnRecipient wraps the file key with a function: stanzas that age would not // write, with the file key that the header MAC uses. type fnRecipient func(fileKey []byte) []*age.Stanza func (f fnRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { return f(fileKey), nil } // x25519Stanza wraps fileKey to the public key pub with the ephemeral // scalar eph, as age's X25519Recipient, with the share and body given. func x25519Stanza(fileKey, pub, eph []byte) *age.Stanza { share, err := curve25519.X25519(eph, curve25519.Basepoint) check(err) secret, err := curve25519.X25519(eph, pub) check(err) salt := append(slices.Clone(share), pub...) key := make([]byte, 32) _, err = io.ReadFull(hkdf.New(sha256.New, secret, salt, []byte("age-encryption.org/v1/X25519")), key) check(err) a, err := chacha20poly1305.New(key) check(err) return &age.Stanza{Type: "X25519", Args: []string{b64.EncodeToString(share)}, Body: a.Seal(nil, make([]byte, 12), fileKey, nil)} } func scryptStanza(fileKey []byte, pass string, salt []byte, logN int, wf string) *age.Stanza { k, err := scrypt.Key([]byte(pass), append([]byte("age-encryption.org/v1/scrypt"), salt...), 1< len(f) { continue } addCase(fmt.Sprintf("%s truncated to %d", name, n), name, &edit{Truncate: intp(n)}, mainID) } addCase(name+" and a byte", name, &edit{Append: "00"}, mainID) addCase(name+" and 17 bytes", name, &edit{Append: h(make([]byte, 17))}, mainID) for _, at := range []int{p, p + 100, p + enc - 1, p + enc, len(f) - 1} { if at < len(f) { addCase(fmt.Sprintf("%s, byte %d changed", name, at), name, &edit{At: intp(at), Xor: "80"}, mainID) } } addCase(name+", nonce changed", name, &edit{At: intp(len(hdr)), Xor: "01"}, mainID) } doc["cases"] = caseList // The grammar and the limits of the header. Each file is the header of // x25519_1 or a variant of it, with its payload. base := files["x25519_1"] hdr1, err := age.ExtractHeader(bytes.NewReader(base)) check(err) payload := base[len(hdr1):] lines := strings.SplitAfter(string(hdr1), "\n") intro, open, body, footer := lines[0], lines[1], lines[2], lines[3] mac := strings.TrimSuffix(strings.TrimPrefix(footer, "--- "), "\n") tail := []part{txt(footer), hx(payload)} withBody := func(o, b string) []part { return append([]part{txt(intro), txt(o), txt(b)}, tail...) } var headers []obj addHeader := func(name string, parts []part) { file := build(parts) headers = append(headers, obj{"name": name, "parts": parts, "length": len(file), "sha256": sum(file), "result": result(file, primary), "probe": probe(file)}) } full := strings.Repeat("A", 64) + "\n" big := 2 << 20 addHeader("x25519_1 as written", []part{hx(base)}) addHeader("empty", nil) addHeader("intro without its newline", []part{txt(strings.TrimSuffix(intro, "\n"))}) addHeader("a short intro", []part{txt("age")}) addHeader("an identity, no newline", []part{txt(primary.String())}) addHeader("an identity line", []part{txt(primary.String() + "\n")}) addHeader("a plugin identity line", []part{txt("AGE-PLUGIN-X-1QQQ\n")}) addHeader("intro v2", append([]part{txt("age-encryption.org/v2\n"), txt(open), txt(body)}, tail...)) addHeader("a long first line cut inside a rune", []part{txt("age-encryption.org/v1ñ\n")}) addHeader("a binary first line", []part{hx([]byte{0xff, 0xfe, 0, 1, '\n'})}) addHeader("intro only", []part{txt(intro)}) addHeader("intro and two bytes", []part{txt(intro), txt("--")}) addHeader("intro and ---", []part{txt(intro), txt("---")}) addHeader("no stanzas", []part{txt(intro), txt(footer), hx(payload)}) addHeader("the footer without newline", []part{txt(intro), txt(open), txt(body), txt(strings.TrimSuffix(footer, "\n"))}) addHeader("the footer without MAC", append([]part{txt(intro), txt(open), txt(body), txt("---\n")}, hx(payload))) addHeader("the footer with an empty MAC", append([]part{txt(intro), txt(open), txt(body), txt("--- \n")}, hx(payload))) addHeader("the footer with two spaces", append([]part{txt(intro), txt(open), txt(body), txt("--- " + mac + "\n")}, hx(payload))) addHeader("the footer with two arguments", append([]part{txt(intro), txt(open), txt(body), txt("--- " + mac + " x\n")}, hx(payload))) addHeader("a MAC of 31 bytes", append([]part{txt(intro), txt(open), txt(body), txt("--- " + b64.EncodeToString(pattern(31)) + "\n")}, hx(payload))) addHeader("a MAC with padding", append([]part{txt(intro), txt(open), txt(body), txt("--- " + mac + "=\n")}, hx(payload))) addHeader("a MAC with a CR", append([]part{txt(intro), txt(open), txt(body), txt("--- " + mac + "\r\n")}, hx(payload))) addHeader("a changed MAC", append([]part{txt(intro), txt(open), txt(body), txt("--- " + b64.EncodeToString(pattern(32)) + "\n")}, hx(payload))) addHeader("----", append([]part{txt(intro), txt(open), txt(body), txt("---- " + mac + "\n")}, hx(payload))) addHeader("opening line ->", withBody("->\n", body)) addHeader("opening line -> and a space", withBody("-> \n", body)) addHeader("opening line with two spaces", withBody(strings.Replace(open, " ", " ", 1), body)) addHeader("opening line with a trailing space", withBody(strings.TrimSuffix(open, "\n")+" \n", body)) addHeader("opening line ->X25519", withBody(strings.Replace(open, "-> ", "->", 1), body)) addHeader("opening line - >", withBody(strings.Replace(open, "->", "- >", 1), body)) addHeader("opening line with a tab", withBody(strings.Replace(open, " ", "\t", 1), body)) addHeader("opening line with a CR", withBody(strings.TrimSuffix(open, "\n")+"\r\n", body)) addHeader("an argument with a byte above 0x7e", withBody("-> X25519 \x80\n", body)) addHeader("an argument with DEL", withBody("-> X25519 a\x7f\n", body)) addHeader("a type of non-ASCII letters", withBody("-> tlöck 1\n", body)) addHeader("129 tokens", withBody("->"+strings.Repeat(" a", 129)+"\n", "\n")) addHeader("130 tokens", withBody("->"+strings.Repeat(" a", 130)+"\n", "\n")) addHeader("a body line with a CR", withBody(open, strings.TrimSuffix(body, "\n")+"\r\n")) addHeader("a body line with padding", withBody(open, strings.TrimSuffix(body, "\n")+"=\n")) // The last character of 32 bytes in Base64 carries two zero bits. const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" last := strings.IndexByte(alphabet, body[len(body)-2]) addHeader("a body line with non-zero trailing bits", withBody(open, body[:len(body)-2]+string(alphabet[last|1])+"\n")) addHeader("a body line of 41 bytes", withBody(open, strings.TrimSuffix(body, "\n")[:42]+"\n")) addHeader("a body line with a space", withBody(open, " "+body)) addHeader("a body line of 68 columns", withBody(open, strings.Repeat("A", 68)+"\n")) addHeader("a body line of 65 columns", withBody(open, strings.Repeat("A", 65)+"\n")) addHeader("a full line, then the footer", withBody(open, full)) addHeader("a full line, then a stanza", append([]part{txt(intro), txt(open), txt(full), txt(open), txt(body)}, tail...)) addHeader("a full line and an empty one", withBody(open, full+"\n")) addHeader("an empty body", withBody(open, "\n")) addHeader("a stanza cut after its opening line", []part{txt(intro), txt(open)}) addHeader("a stanza cut in its body", []part{txt(intro), txt(open), txt(strings.TrimSuffix(body, "\n"))}) addHeader("two copies of the stanza", append([]part{txt(intro), txt(open), txt(body), txt(open), txt(body)}, tail...)) addHeader("1024 stanzas", append([]part{txt(intro), rep(open+body, 1024)}, tail...)) addHeader("1025 stanzas", append([]part{txt(intro), rep(open+body, 1025)}, tail...)) addHeader("1023 other stanzas and the stanza", append([]part{txt(intro), rep("-> other\n\n", 1023), txt(open), txt(body)}, tail...)) // The 2 MiB limit: a first line, a body, the Peek of the footer, the // footer line, and a header that ends exactly at the limit. addHeader("a first line of 2 MiB and a byte", []part{rep("a", big+1)}) addHeader("a first line of 2 MiB, no newline", []part{rep("a", big)}) addHeader("a first line of 2 MiB with its newline", []part{rep("a", big-1), txt("\n")}) addHeader("a body past 2 MiB", append([]part{txt(intro), txt("-> pad x\n"), rep(full, 33000)}, tail...)) // intro (22) + "-> pad " (7) + token + "\n" + k full lines + "\n" ends // at 31 + t + 65k. padStanza := func(end int) []part { k := (end - 32) / 65 t := end - 31 - 65*k if t < 1 { k-- t += 65 } return []part{txt(intro), txt("-> pad "), rep("t", t), txt("\n"), rep(full, k), txt("\n")} } footerLine := "--- " + mac + "\n" addHeader("the footer peeked at 2 MiB - 2", append(padStanza(big-2), txt(footerLine), hx(payload))) addHeader("the footer peeked at 2 MiB - 3", append(padStanza(big-3), txt(footerLine), hx(payload))) addHeader("a header that ends at 2 MiB", append(padStanza(big-len(footerLine)), txt(footerLine), hx(payload))) addHeader("a header that ends at 2 MiB + 1", append(padStanza(big-len(footerLine)+1), txt(footerLine), hx(payload))) addHeader("a header that ends at 2 MiB, nothing after", append(padStanza(big-len(footerLine)), txt(footerLine))) doc["headers"] = headers // The stanza rules of agewrap, on stanzas given directly. qn := profile.Quicknet() st := func(typ string, args ...string) *age.Stanza { if args == nil { args = []string{} } return &age.Stanza{Type: typ, Args: args, Body: []byte{}} } chain := qn.ChainHashHex() type rule struct { name string stanzas []*age.Stanza } stanzaJSON := func(ss []*age.Stanza) []obj { var out []obj for _, s := range ss { out = append(out, obj{"type": s.Type, "args": s.Args}) } if out == nil { out = []obj{} } return out } var timeRules []obj for _, r := range []rule{ {"one tlock stanza", []*age.Stanza{st("tlock", "1000", chain)}}, {"no stanza", nil}, {"two stanzas", []*age.Stanza{st("tlock", "1000", chain), st("tlock", "1000", chain)}}, {"an X25519 stanza", []*age.Stanza{st("X25519", "x")}}, {"tlock with one argument", []*age.Stanza{st("tlock", "1000")}}, {"tlock with three arguments", []*age.Stanza{st("tlock", "1000", chain, "x")}}, {"round 0999", []*age.Stanza{st("tlock", "0999", chain)}}, {"round 1001", []*age.Stanza{st("tlock", "1001", chain)}}, {"round with a sign", []*age.Stanza{st("tlock", "+1000", chain)}}, {"chain hash in uppercase", []*age.Stanza{st("tlock", "1000", strings.ToUpper(chain))}}, {"another chain hash", []*age.Stanza{st("tlock", "1000", strings.Repeat("ab", 32))}}, {"type TLOCK", []*age.Stanza{st("TLOCK", "1000", chain)}}, {"type with a quote", []*age.Stanza{st("t\"x", "1000", chain)}}, } { timeRules = append(timeRules, obj{"name": r.name, "stanzas": stanzaJSON(r.stanzas), "round": 1000, "error": errText(agewrap.CheckTimeStanzas(r.stanzas, qn, 1000))}) } var payloadRules, accessRules []obj for _, r := range []rule{ {"one X25519 stanza", []*age.Stanza{st("X25519", "a")}}, {"no stanza", nil}, {"two X25519 stanzas", []*age.Stanza{st("X25519", "a"), st("X25519", "b")}}, {"a scrypt stanza", []*age.Stanza{st("scrypt", "a", "1")}}, {"type x25519", []*age.Stanza{st("x25519", "a")}}, } { payloadRules = append(payloadRules, obj{"name": r.name, "stanzas": stanzaJSON(r.stanzas), "error": errText(agewrap.CheckPayloadStanzas(r.stanzas))}) } sixteen := func(mod func([]*age.Stanza) []*age.Stanza) []*age.Stanza { var ss []*age.Stanza for i := range 16 { ss = append(ss, st("X25519", fmt.Sprintf("share%d", i))) } if mod != nil { ss = mod(ss) } return ss } for _, slots := range []int{0, 16} { for _, r := range []rule{ {"one X25519 stanza", []*age.Stanza{st("X25519", "a")}}, {"no stanza", nil}, {"sixteen X25519 stanzas", sixteen(nil)}, {"fifteen", sixteen(func(s []*age.Stanza) []*age.Stanza { return s[:15] })}, {"seventeen", sixteen(func(s []*age.Stanza) []*age.Stanza { return append(s, st("X25519", "x")) })}, {"stanza 3 of type tlock", sixteen(func(s []*age.Stanza) []*age.Stanza { s[3] = st("tlock", "1", "2"); return s })}, {"stanza 9 repeats the share of stanza 2", sixteen(func(s []*age.Stanza) []*age.Stanza { s[9] = st("X25519", "share2"); return s })}, {"stanzas of two arguments with the same first one", sixteen(func(s []*age.Stanza) []*age.Stanza { s[1] = st("X25519", "same", "x") s[2] = st("X25519", "same", "y") return s })}, {"stanzas without arguments", sixteen(func(s []*age.Stanza) []*age.Stanza { s[1] = st("X25519") s[2] = st("X25519") return s })}, } { accessRules = append(accessRules, obj{"name": r.name, "slots": slots, "stanzas": stanzaJSON(r.stanzas), "error": errText(agewrap.CheckAccessStanzas(r.stanzas, slots))}) } } doc["time_stanzas"] = obj{"profile_id": qn.ID, "chain_hash": chain, "cases": timeRules} doc["payload_stanzas"] = payloadRules doc["access_stanzas"] = accessRules // The identities of agewrap on whole files. var wrapCases []obj addWrap := func(name string, file []byte, kind string, slots int, raws [][]byte, want error) { var id age.Identity var err error switch kind { case "payload": id, err = agewrap.NewPayloadIdentity(raws[0]) case "access": var ids []age.Identity for _, r := range raws { x, e := agewrap.X25519IdentityFromRaw(r) check(e) ids = append(ids, x) } id, err = agewrap.NewAccessIdentity(slots, ids...) } var rawHex []string for _, r := range raws { rawHex = append(rawHex, h(r)) } if rawHex == nil { rawHex = []string{} } c := obj{"name": name, "kind": kind, "slots": slots, "raw_identities": rawHex, "file": h(file)} if err != nil { c["result"] = obj{"error": err.Error(), "phase": "identity"} } else { c["result"] = result(file, id) } wrapCases = append(wrapCases, c) } newRaw := func() (*age.X25519Identity, []byte) { id, err := age.GenerateX25519Identity() check(err) raw, err := agewrap.RawX25519Identity(id) check(err) return id, raw } pid, praw := newRaw() _, wrongRaw := newRaw() addWrap("PAYLOAD_AGE for R_PAYLOAD", encrypt(pattern(7), pid.Recipient()), "payload", 0, [][]byte{praw}, nil) addWrap("PAYLOAD_AGE for another recipient", encrypt(pattern(7), pid.Recipient()), "payload", 0, [][]byte{wrongRaw}, nil) addWrap("PAYLOAD_AGE with two stanzas", encrypt(pattern(7), pid.Recipient(), other.Recipient()), "payload", 0, [][]byte{praw}, nil) sr, err := age.NewScryptRecipient(pass) check(err) sr.SetWorkFactor(1) addWrap("PAYLOAD_AGE with a scrypt stanza", encrypt(pattern(7), sr), "payload", 0, [][]byte{praw}, nil) ppub, err := agewrap.RawX25519Recipient(pid.Recipient()) check(err) for _, f := range []struct { name string mod func(s *age.Stanza) }{ {"a share of low order", func(s *age.Stanza) { s.Args[0] = b64.EncodeToString(lowOrder) }}, {"a share of 31 bytes", func(s *age.Stanza) { s.Args[0] = b64.EncodeToString(pattern(31)) }}, {"two arguments", func(s *age.Stanza) { s.Args = append(s.Args, "x") }}, {"a body of 33 bytes", func(s *age.Stanza) { s.Body = append(s.Body, 1) }}, } { file := encrypt(pattern(7), fnRecipient(func(fk []byte) []*age.Stanza { s := x25519Stanza(fk, ppub, pattern(32)) f.mod(s) return []*age.Stanza{s} })) addWrap("PAYLOAD_AGE with "+f.name, file, "payload", 0, [][]byte{praw}, nil) } addWrap("a payload identity of 31 bytes", encrypt(pattern(7), pid.Recipient()), "payload", 0, [][]byte{praw[:31]}, nil) // INNER_ACCESS_AGE: sixteen slots, three credentials. var creds []*age.X25519Identity var credRaws [][]byte var recips []age.Recipient for range 3 { id, raw := newRaw() creds = append(creds, id) credRaws = append(credRaws, raw) recips = append(recips, id.Recipient()) } for range 13 { d, _ := newRaw() recips = append(recips, d.Recipient()) } inner := encrypt(pattern(9), recips...) addWrap("sixteen slots, the first credential", inner, "access", 16, [][]byte{credRaws[0]}, nil) addWrap("sixteen slots, three credentials", inner, "access", 16, credRaws, nil) addWrap("sixteen slots, a stranger and the third", inner, "access", 16, [][]byte{wrongRaw, credRaws[2]}, nil) addWrap("sixteen slots, a stranger", inner, "access", 16, [][]byte{wrongRaw}, nil) addWrap("sixteen slots read as format 1", inner, "access", 0, [][]byte{credRaws[1]}, nil) addWrap("no identity", inner, "access", 16, nil, nil) addWrap("fifteen slots", encrypt(pattern(9), recips[:15]...), "access", 16, [][]byte{credRaws[0]}, nil) addWrap("one recipient, format 1", encrypt(pattern(9), creds[0].Recipient()), "access", 0, [][]byte{credRaws[0]}, nil) dup := append(slices.Clone(recips[:15]), creds[1].Recipient()) addWrap("a credential in two slots", encrypt(pattern(9), dup...), "access", 16, [][]byte{credRaws[0], credRaws[1]}, nil) addWrap("a credential in two slots, read by another", encrypt(pattern(9), dup...), "access", 16, [][]byte{credRaws[0]}, nil) repeated := encrypt(pattern(9), fnRecipient(func(fk []byte) []*age.Stanza { var ss []*age.Stanza for i := range 15 { ss = append(ss, x25519Stanza(fk, ppub, eph(i))) } return append(ss, x25519Stanza(fk, ppub, eph(0))) })) addWrap("a repeated ephemeral share", repeated, "access", 16, [][]byte{praw}, nil) malformed := encrypt(pattern(9), fnRecipient(func(fk []byte) []*age.Stanza { var ss []*age.Stanza for i := range 16 { ss = append(ss, x25519Stanza(fk, ppub, eph(100+i))) } ss[7].Args[0] = b64.EncodeToString(lowOrder) return ss })) addWrap("a stanza with a share of low order", malformed, "access", 16, [][]byte{wrongRaw}, nil) addWrap("INNER_ACCESS_AGE with a scrypt stanza", encrypt(pattern(9), sr), "access", 0, [][]byte{credRaws[0]}, nil) doc["agewrap"] = wrapCases // age.ParseX25519Identity. var parseCases []obj addParse := func(s string) { id, err := age.ParseX25519Identity(s) c := obj{"input": s} if err != nil { c["error"] = err.Error() } else { raw, err := agewrap.RawX25519Identity(id) check(err) c["raw"] = h(raw) c["recipient"] = id.Recipient().String() c["string"] = id.String() } parseCases = append(parseCases, c) } addParse(primary.String()) addParse(strings.ToLower(primary.String())) addParse(primary.Recipient().String()) addParse(primary.String()[:len(primary.String())-1] + "Q") addParse("AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQSGNLJW") addParse("AGE-SECRET-KEY-") addParse("") addParse("Age-secret-key-1qqq") addParse(strings.Replace(primary.String(), "AGE-SECRET-KEY-1", "AGE-SECRET-KEY-2", 1)) doc["parse_identity"] = parseCases write(filepath.Join(outDir, "age.json"), doc) } // fixturesJSON opens, in each official capsule of testdata/, its // PAYLOAD_AGE with its payload_identity and, under time_and_key, its // OUTER_TIME_AGE with the release of the fixture, to give the // INNER_ACCESS_AGE that it seals. func fixturesJSON(outDir string) { matches, err := filepath.Glob(filepath.Join(testdata, "fixtures", "*.dkc")) check(err) qn := profile.Quicknet() var out []obj for _, path := range matches { name := strings.TrimSuffix(filepath.Base(path), ".dkc") dkc, err := os.ReadFile(path) check(err) var rec struct { Format int AccessPolicy string `json:"access_policy"` PayloadIdentity string `json:"payload_identity"` ControlCBOR string `json:"control_cbor"` Identities []string AccessKeyFile string `json:"access_key_file"` Release struct { Round uint64 Signature string } } raw, err := os.ReadFile(strings.TrimSuffix(path, ".dkc") + ".json") check(err) check(json.Unmarshal(raw, &rec)) pre, err := capsule.ParsePrelude(dkc[:capsule.PreludeSize]) check(err) off := int(pre.PayloadOffset()) payload := dkc[off:] pid, err := agewrap.NewPayloadIdentity(mustHex(rec.PayloadIdentity)) check(err) res := result(payload, pid) if _, ok := res["error"]; ok { log.Fatalf("%s: PAYLOAD_AGE: %v", name, res["error"]) } f := obj{"name": name, "format": rec.Format, "access_policy": rec.AccessPolicy, "payload_offset": off, "payload_length": len(payload), "payload_sha256": sum(payload), "payload_identity": rec.PayloadIdentity, "payload_result": res} // A stranger gets the error of spec §30.1. wrong := strings.Repeat("0", 63) + "1" wid, err := agewrap.NewPayloadIdentity(mustHex(wrong)) check(err) f["payload_wrong_identity"] = obj{"raw": wrong, "result": result(payload, wid)} sealedStart := capsule.PreludeSize + int(pre.PublicHeaderLen) sealed := dkc[sealedStart:off] if rec.AccessPolicy == "time_and_key" { tid, err := agewrap.NewTimeIdentity(qn, rec.Release.Round, provider.Release{Round: rec.Release.Round, Signature: mustHex(rec.Release.Signature)}) check(err) r, err := age.Decrypt(bytes.NewReader(sealed), tid) check(err) innerAge, err := io.ReadAll(r) check(err) slots := 0 if rec.Format >= 2 { slots = agewrap.AccessSlots } var ids []obj var all []age.Identity addID := func(source string, id age.Identity) { x := id.(*age.X25519Identity) raw, err := agewrap.RawX25519Identity(x) check(err) aid, err := agewrap.NewAccessIdentity(slots, id) check(err) ids = append(ids, obj{"source": source, "raw": h(raw), "result": result(innerAge, aid)}) all = append(all, id) } if rec.AccessKeyFile != "" { kf, err := os.Open(filepath.Join(testdata, "fixtures", rec.AccessKeyFile)) check(err) k, err := accesskey.Decode(kf) check(err) kf.Close() id, err := k.Identity() check(err) addID(rec.AccessKeyFile, id) } for i, s := range rec.Identities { id, err := age.ParseX25519Identity(s) check(err) addID(fmt.Sprintf("identities[%d]", i), id) } aid, err := agewrap.NewAccessIdentity(slots, all...) check(err) allRes := result(innerAge, aid) if allRes["plaintext_sha256"] != sum(mustHex(rec.ControlCBOR)) { log.Fatalf("%s: INNER_ACCESS_AGE does not seal control_cbor", name) } stranger, err := age.GenerateX25519Identity() check(err) sid, err := agewrap.NewAccessIdentity(slots, stranger) check(err) f["inner_access_age"] = obj{"hex": h(innerAge), "slots": slots, "identities": ids, "all_identities_result": allRes, "stranger_result": result(innerAge, sid), "control_cbor_sha256": sum(mustHex(rec.ControlCBOR))} } out = append(out, f) } if len(out) == 0 { log.Fatal("no fixtures: run it from datekeys-go") } write(filepath.Join(outDir, "age_fixtures.json"), obj{ "description": "The PAYLOAD_AGE of every official capsule of testdata/fixtures of datekeys-dart, opened by Go with its payload_identity (agewrap.PayloadIdentity) and with a stranger, and the INNER_ACCESS_AGE of the time_and_key ones, which this program took from OUTER_TIME_AGE with the release of the fixture, opened with each credential of the fixture and with a stranger (agewrap.AccessIdentity). Results as in age.json. See tool/gen_age_vectors.go.", "generator": "tool/gen_age_vectors.go, " + runtime.Version(), "fixtures": out, }) } func write(path string, doc obj) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) enc.SetIndent("", " ") check(enc.Encode(doc)) check(os.WriteFile(path, buf.Bytes(), 0o644)) fmt.Printf("wrote %s, %d bytes\n", path, buf.Len()) }