//go:build ignore // Writes test/vectors/age_writer.json, the vectors of the age writer of // datekeys-dart, stage 6a of docs/PLAN_dart.md: age files that // filippo.io/age v1.3.2 and the agewrap package of datekeys-go write, // deterministically, and the texts of the errors of the writer. // // age.Encrypt draws every random value from crypto/rand: the file key, the // ephemeral secret of each X25519 stanza, the salt and then the label of a // scrypt stanza, sigma and then the label of the tlock stanza, and the nonce // of the payload. Here crypto/rand.Reader reads the keystream of // SeededRandomSource of lib/src/random.dart instead: ChaCha20 under // SHA-256(seed), with a zero nonce. With the same seed, the writer of // datekeys-dart draws the same values, in the same order, and must write the // same bytes. Every case records the size and the bytes of each draw, and // the generator checks the files against internal/testkit: testkit.SealAge // seals the same file with the first draw as the file key and the last as // the nonce, each X25519 stanza is the one of its ephemeral secret, the // length is the one of testkit.HeaderLen and testkit.StreamLen, and Go opens // the file with its identities. // // - seeded: the keystream of a few seeds, read in fills of several sizes; // - rand_int and permute: crypto/rand.Int and the permute of // capsule.Encrypt, restated because it is not exported, over the // keystream of a seed; // - encrypt: age.Encrypt with X25519 recipients (one, two, three and // sixteen, and one with bit 255 set, which age accepts), scrypt // recipients of work factor 1, 2, 10 and 16, and the tlock recipient of // agewrap for rounds of 1 to 11 digits, over plaintexts of 0, 1, 64 KiB // - 1, 64 KiB, 64 KiB + 1, 128 KiB and more bytes, up to a few MiB. Byte // i of a plaintext is (31·i + 7) mod 256. A small file is stored whole; // a large one, as its header, its length and its SHA-256; // - errors: what age.Encrypt refuses, with its text: no recipient, // recipients whose labels cannot be mixed and X25519 recipients of low // order; and NewScryptRecipient, SetWorkFactor and NewTimeRecipient; // - stream: the writer of age.Encrypt after Close; // - parse, check and generate: age.ParseX25519Recipient, // agewrap.CheckX25519Recipient and age.GenerateX25519Identity; // - lengths: testkit.StreamLen and capsule.PayloadAgeLength. // // It also writes test/vectors/age_writer.g.dart, the same JSON as a Dart // constant, for the tests that also run compiled to JavaScript. // // It imports internal/testkit, so it runs in an export of datekeys-go made // with git archive, without changing the repository, on the branch v0.12 at // c531e93: // // commit=$(git -C ../datekeys-go rev-parse v0.12) // out=$PWD/test/vectors // tmp=$(mktemp -d) // git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp" // cp tool/age_writer_go_vectors.go "$tmp" // (cd "$tmp" && go run ./age_writer_go_vectors.go -source "$commit" -out "$out") // rm -rf "$tmp" // // The output is the same on every run. package main import ( "bytes" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "flag" "fmt" "io" "log" "math/big" "os" "path/filepath" "runtime" "strings" "filippo.io/age" "golang.org/x/crypto/chacha20" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" "g.activething.com/go/DateKeys/agewrap" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/codec/bech32" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" ) type obj = map[string]any 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 { _, file, line, _ := runtime.Caller(1) log.Fatalf("%s:%d: %v", filepath.Base(file), line, err) } } func mustHex(s string) []byte { b, err := hex.DecodeString(s) check(err) return b } // pattern is a plaintext of n bytes: 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 } // The published Quicknet signatures of rounds 1000 and 1001, as in the // fixtures; provider.Verify checks them when a file is opened. var releases = map[uint64]string{ 1000: "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", 1001: "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", } // --------------------------------------------------------------------------- // crypto/rand from a seed // seeded is the crypto/rand.Reader of a case: the ChaCha20 keystream under // SHA-256(seed) and a zero nonce, the stream of SeededRandomSource. It keeps // every read: age reads each value whole, with io.ReadFull. type seeded struct { c *chacha20.Cipher draws [][]byte } func newSeeded(seed string) *seeded { key := sha256.Sum256([]byte(seed)) c, err := chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize)) check(err) return &seeded{c: c} } func (s *seeded) Read(p []byte) (int, error) { clear(p) s.c.XORKeyStream(p, p) s.draws = append(s.draws, bytes.Clone(p)) return len(p), nil } // with runs f while crypto/rand reads the keystream of seed, and returns // the draws. func with(seed string, f func()) [][]byte { s := newSeeded(seed) old := rand.Reader rand.Reader = s defer func() { rand.Reader = old }() f() return s.draws } func drawsOf(d [][]byte) []obj { out := []obj{} for _, b := range d { out = append(out, obj{"n": len(b), "hex": h(b)}) } return out } // --------------------------------------------------------------------------- // Recipients // identity is the X25519 identity of a label: its raw secret is // SHA-256("identity " + label). func identity(label string) *age.X25519Identity { s := sha256.Sum256([]byte("identity " + label)) id, err := agewrap.X25519IdentityFromRaw(s[:]) check(err) return id } func recipientOfRaw(raw []byte) *age.X25519Recipient { s, err := bech32.Encode("age", raw) check(err) r, err := age.ParseX25519Recipient(s) check(err) return r } func rawOf(r *age.X25519Recipient) []byte { raw, err := agewrap.RawX25519Recipient(r) check(err) return raw } // spec is a recipient of a case: X25519, scrypt or tlock. type spec struct { x25519 *age.X25519Recipient id *age.X25519Identity // the identity of x25519, when known pass string wf int round uint64 } func x(label string) spec { id := identity(label) return spec{x25519: id.Recipient(), id: id} } func xraw(raw []byte) spec { return spec{x25519: recipientOfRaw(raw)} } func sc(pass string, wf int) spec { return spec{pass: pass, wf: wf} } func tl(round uint64) spec { return spec{round: round} } func (s spec) recipient() age.Recipient { switch { case s.x25519 != nil: return s.x25519 case s.pass != "": r, err := age.NewScryptRecipient(s.pass) check(err) r.SetWorkFactor(s.wf) return r default: r, err := agewrap.NewTimeRecipient(profile.Quicknet(), s.round) check(err) return r } } func (s spec) json() obj { switch { case s.x25519 != nil: o := obj{"x25519": s.x25519.String()} if s.id != nil { o["identity"] = s.id.String() } return o case s.pass != "": return obj{"scrypt": s.pass, "work_factor": s.wf} default: return obj{"tlock": s.round} } } func recipients(specs []spec) []age.Recipient { var out []age.Recipient for _, s := range specs { out = append(out, s.recipient()) } return out } func specsJSON(specs []spec) []obj { out := []obj{} for _, s := range specs { out = append(out, s.json()) } return out } func encrypt(plain []byte, rs []age.Recipient) ([]byte, error) { var buf bytes.Buffer w, err := age.Encrypt(&buf, rs...) if err != nil { return nil, err } if _, err := w.Write(plain); err != nil { return nil, err } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil } // x25519Stanza is the X25519 stanza that wraps fileKey for pub with the // ephemeral secret eph, restated from age's X25519Recipient.Wrap. 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) key := make([]byte, 32) _, err = io.ReadFull(hkdf.New(sha256.New, secret, append(bytes.Clone(share), pub...), []byte("age-encryption.org/v1/X25519")), key) check(err) a, err := chacha20poly1305.New(key) check(err) return &age.Stanza{Type: "X25519", Args: []string{base64.RawStdEncoding.EncodeToString(share)}, Body: a.Seal(nil, make([]byte, 12), fileKey, nil)} } // crossCheck checks a file that age.Encrypt wrote with draws against // internal/testkit and Go's identities. func crossCheck(name string, file []byte, draws [][]byte, plain []byte, specs []spec) { stanzas, err := agewrap.Stanzas(bytes.NewReader(file)) check(err) if len(stanzas) != len(specs) { log.Fatalf("%s: %d stanzas for %d recipients", name, len(stanzas), len(specs)) } fileKey, nonce := draws[0], draws[len(draws)-1] if len(fileKey) != 16 || len(nonce) != 16 { log.Fatalf("%s: a file key of %d bytes and a nonce of %d", name, len(fileKey), len(nonce)) } again, err := testkit.SealAge(stanzas, fileKey, nonce, plain) check(err) if !bytes.Equal(again, file) { log.Fatalf("%s: testkit.SealAge writes another file", name) } n, err := testkit.HeaderLen(file) check(err) if len(file) != n+16+testkit.StreamLen(len(plain)) { log.Fatalf("%s: %d bytes, not the header, the nonce and the STREAM", name, len(file)) } i := 1 for k, s := range specs { switch { case s.x25519 != nil: want := x25519Stanza(fileKey, rawOf(s.x25519), draws[i]) got := stanzas[k] if got.Type != want.Type || strings.Join(got.Args, " ") != strings.Join(want.Args, " ") || !bytes.Equal(got.Body, want.Body) { log.Fatalf("%s: stanza %d is not the one of its ephemeral secret", name, k) } i++ case s.pass != "": if stanzas[k].Args[0] != base64.RawStdEncoding.EncodeToString(draws[i]) { log.Fatalf("%s: stanza %d has another salt", name, k) } i += 2 // the salt and the label default: i += 2 // sigma and the label } } if i != len(draws)-1 { log.Fatalf("%s: %d draws, %d expected", name, len(draws), i+1) } // Go opens it with every identity it knows. for k, s := range specs { var id age.Identity switch { case s.id != nil: id = s.id case s.pass != "": sid, err := age.NewScryptIdentity(s.pass) check(err) sid.SetMaxWorkFactor(s.wf) id = sid case s.round != 0 && releases[s.round] != "": tid, err := agewrap.NewTimeIdentity(profile.Quicknet(), s.round, provider.Release{Round: s.round, Signature: mustHex(releases[s.round])}) check(err) id = tid default: continue } got, err := testkitOpen(file, id) if err != nil { log.Fatalf("%s: recipient %d does not open: %v", name, k, err) } if !bytes.Equal(got, plain) { log.Fatalf("%s: recipient %d opens another plaintext", name, k) } } } func testkitOpen(file []byte, id age.Identity) ([]byte, error) { r, err := age.Decrypt(bytes.NewReader(file), id) if err != nil { return nil, err } return io.ReadAll(r) } // --------------------------------------------------------------------------- // The sections func seededSection() []obj { fills := []int{0, 1, 15, 16, 32, 63, 64, 65, 200} var out []obj for _, seed := range []string{"", "a", "age writer", "seed with spaces and ñ"} { s := newSeeded(seed) var all []byte for _, n := range fills { b := make([]byte, n) _, _ = s.Read(b) all = append(all, b...) } out = append(out, obj{"seed": seed, "fills": fills, "hex": h(all)}) } return out } func randIntSection() []obj { var out []obj for _, n := range []int64{1, 2, 3, 5, 7, 8, 10, 16, 17, 100, 255, 256, 257, 1000, 65535, 65536, 65537, 1<<31 - 1, 1 << 31, 1<<32 - 1, 1 << 32} { seed := fmt.Sprintf("rand.Int %d", n) var results []int64 next := make([]byte, 4) draws := with(seed, func() { for range 24 { v, err := rand.Int(rand.Reader, big.NewInt(n)) check(err) results = append(results, v.Int64()) } _, _ = rand.Read(next) }) out = append(out, obj{"n": n, "seed": seed, "results": results, "reads": len(draws) - 1, "next": h(next)}) } return out } // permute is the permute of capsule.Encrypt in Go, which capsule does not // export, restated: Fisher-Yates with crypto/rand.Int. func permute[T any](s []T) error { for i := len(s) - 1; i > 0; i-- { j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) if err != nil { return err } k := int(j.Int64()) s[i], s[k] = s[k], s[i] } return nil } func permuteSection() []obj { var out []obj for _, n := range []int{0, 1, 2, 3, 5, 16, 16, 16, 16, 40} { seed := fmt.Sprintf("permute %d %d", n, len(out)) items := make([]int, n) for i := range items { items[i] = i } draws := with(seed, func() { check(permute(items)) }) out = append(out, obj{"n": n, "seed": seed, "order": items, "reads": len(draws)}) } return out } func encryptSection() []obj { var out []obj add := func(name string, specs []spec, n int, node bool) { seed := "age writer " + name plain := pattern(n) var file []byte draws := with(seed, func() { var err error file, err = encrypt(plain, recipients(specs)) check(err) }) crossCheck(name, file, draws, plain, specs) hdr, err := testkit.HeaderLen(file) check(err) o := obj{ "name": name, "seed": seed, "recipients": specsJSON(specs), "length": n, "draws": drawsOf(draws), "header": h(file[:hdr]), "file_length": len(file), "file_sha256": sum(file), "node": node, } if len(file) <= 2048 { o["file"] = h(file) } out = append(out, o) } for _, n := range []int{0, 1, 2, 15, 16, 17, 100, 103, 1000, 65535, 65536, 65537, 131071, 131072, 131073, 196608, 200000, 2 << 20, 3<<20 + 3} { add(fmt.Sprintf("x25519, %d bytes", n), []spec{x("one")}, n, n <= 262144) } add("x25519, two recipients", []spec{x("two a"), x("two b")}, 50, true) add("x25519, three recipients", []spec{x("three a"), x("three b"), x("three c")}, 1000, true) var sixteen []spec for i := range 16 { sixteen = append(sixteen, x(fmt.Sprintf("slot %d", i))) } add("x25519, sixteen recipients", sixteen, 103, true) add("x25519, sixteen recipients, 70000 bytes", sixteen, 70000, true) high := rawOf(identity("high").Recipient()) high[31] |= 0x80 add("x25519, a recipient with bit 255 set", []spec{xraw(high)}, 10, true) for _, c := range []struct { wf int n int node bool }{{1, 0, true}, {2, 150, true}, {2, 70000, true}, {10, 150, true}, {16, 150, false}} { add(fmt.Sprintf("scrypt, work factor %d, %d bytes", c.wf, c.n), []spec{sc("correct horse battery staple", c.wf)}, c.n, c.node) } add("scrypt, a passphrase of one byte", []spec{sc("p", 1)}, 3, true) add("scrypt, a passphrase in UTF-8", []spec{sc("contraseña de prueba ñ €", 2)}, 3, true) for _, c := range []struct { round uint64 n int node bool }{{1000, 16, false}, {1000, 103, true}, {1000, 1773, false}, {1000, 70000, false}, {1001, 0, false}, {1, 103, true}, {profile.Quicknet().MaxRound(), 5, false}} { add(fmt.Sprintf("tlock, round %d, %d bytes", c.round, c.n), []spec{tl(c.round)}, c.n, c.node) } return out } func errorsSection() []obj { var out []obj add := func(name string, specs []spec, node bool) { seed := "age writer error " + name var err error draws := with(seed, func() { _, err = encrypt(pattern(10), recipients(specs)) }) if err == nil { log.Fatalf("%s: no error", name) } out = append(out, obj{"name": name, "seed": seed, "recipients": specsJSON(specs), "draws": drawsOf(draws), "error": err.Error(), "node": node}) } add("no recipients", nil, true) add("x25519, then scrypt", []spec{x("mix a"), sc("pass", 1)}, true) add("scrypt, then x25519", []spec{sc("pass", 1), x("mix a")}, true) add("scrypt twice", []spec{sc("pass", 1), sc("pass", 1)}, true) add("two x25519, then scrypt", []spec{x("mix a"), x("mix b"), sc("pass", 1)}, true) add("tlock, then x25519", []spec{tl(1000), x("mix a")}, true) add("x25519, then tlock", []spec{x("mix a"), tl(1000)}, false) add("tlock twice", []spec{tl(1000), tl(1000)}, false) add("scrypt, then tlock", []spec{sc("pass", 1), tl(1000)}, false) p := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19)) le := func(v *big.Int) []byte { b := make([]byte, 32) v.FillBytes(b) for i, j := 0, 31; i < j; i, j = i+1, j-1 { b[i], b[j] = b[j], b[i] } return b } order8a, _ := new(big.Int).SetString("325606250916557431795983626356110631294008115727848805560023387167927233504", 10) order8b, _ := new(big.Int).SetString("39382357235489614581723060781553021112529911719440698176882885853963445705823", 10) low := map[string]*big.Int{ "0": big.NewInt(0), "1": big.NewInt(1), "order 8, a": order8a, "order 8, b": order8b, "p - 1": new(big.Int).Sub(p, big.NewInt(1)), "p": p, "p + 1": new(big.Int).Add(p, big.NewInt(1)), } for _, k := range []string{"0", "1", "order 8, a", "order 8, b", "p - 1", "p", "p + 1"} { add("x25519 of low order, u = "+k, []spec{xraw(le(low[k]))}, true) } zeroHigh := make([]byte, 32) zeroHigh[31] = 0x80 add("x25519 of low order, u = 0 with bit 255 set", []spec{xraw(zeroHigh)}, true) add("x25519 of low order after another one", []spec{x("mix a"), xraw(le(big.NewInt(1)))}, true) return out } func constructorSection() []obj { var out []obj _, err := age.NewScryptRecipient("") out = append(out, obj{"name": "NewScryptRecipient of an empty passphrase", "error": err.Error()}) for _, wf := range []int{0, 31, -1} { var text string func() { defer func() { text = fmt.Sprint(recover()) }() r, err := age.NewScryptRecipient("p") check(err) r.SetWorkFactor(wf) }() out = append(out, obj{"name": fmt.Sprintf("SetWorkFactor(%d)", wf), "work_factor": wf, "panic": text}) } q := profile.Quicknet() for _, round := range []uint64{0, q.MaxRound() + 1} { _, err := agewrap.NewTimeRecipient(q, round) out = append(out, obj{"name": fmt.Sprintf("NewTimeRecipient(%d)", round), "round": round, "error": err.Error()}) } for _, round := range []uint64{1, q.MaxRound()} { _, err := agewrap.NewTimeRecipient(q, round) check(err) } out = append(out, obj{"name": "the last round of Quicknet", "max_round": q.MaxRound()}) return out } func streamSection() obj { var write, empty, closeAgain error with("age writer stream", func() { var buf bytes.Buffer w, err := age.Encrypt(&buf, identity("stream").Recipient()) check(err) check(w.Close()) _, write = w.Write([]byte{1}) _, empty = w.Write(nil) closeAgain = w.Close() }) return obj{"write_after_close": write.Error(), "empty_write_after_close": empty.Error(), "close_after_close": closeAgain.Error()} } func parseSection() []obj { good := identity("parse").Recipient().String() raw := rawOf(identity("parse").Recipient()) enc := func(hrp string, b []byte) string { s, err := bech32.Encode(hrp, b) check(err) return s } flipped := good[:len(good)-1] + map[bool]string{true: "p", false: "q"}[strings.HasSuffix(good, "q")] inputs := []string{ good, strings.ToUpper(good), good[:10] + strings.ToUpper(good[10:]), enc("age1pq", raw), enc("age1tag", raw), enc("AGE", raw), enc("age", raw[:31]), enc("age", append(bytes.Clone(raw), 0)), enc("age", nil), flipped, "", "age1", "age1qqqqqq", identity("parse").String(), " " + good, good + "\n", "age1é" + good[5:], enc("age", make([]byte, 32)), } var out []obj for _, s := range inputs { r, err := age.ParseX25519Recipient(s) if err != nil { out = append(out, obj{"input": s, "error": err.Error()}) continue } out = append(out, obj{"input": s, "raw": h(rawOf(r)), "string": r.String()}) } return out } func checkSection() []obj { p := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19)) le := func(v *big.Int) []byte { b := make([]byte, 32) v.FillBytes(b) for i, j := 0, 31; i < j; i, j = i+1, j-1 { b[i], b[j] = b[j], b[i] } return b } order8a, _ := new(big.Int).SetString("325606250916557431795983626356110631294008115727848805560023387167927233504", 10) order8b, _ := new(big.Int).SetString("39382357235489614581723060781553021112529911719440698176882885853963445705823", 10) var raws [][]byte for i := range 5 { raws = append(raws, rawOf(identity(fmt.Sprintf("check %d", i)).Recipient())) } for _, v := range []*big.Int{ big.NewInt(9), big.NewInt(2), new(big.Int).Sub(p, big.NewInt(2)), p, new(big.Int).Add(p, big.NewInt(1)), new(big.Int).Add(p, big.NewInt(18)), big.NewInt(0), big.NewInt(1), order8a, order8b, new(big.Int).Sub(p, big.NewInt(1)), } { raws = append(raws, le(v)) } for _, b := range [][]byte{le(big.NewInt(9)), le(big.NewInt(0)), raws[0]} { b = bytes.Clone(b) b[31] |= 0x80 raws = append(raws, b) } var out []obj for _, raw := range raws { r := recipientOfRaw(raw) o := obj{"raw": h(raw), "recipient": r.String()} if err := agewrap.CheckX25519Recipient(r); err != nil { o["error"] = err.Error() } out = append(out, o) } return out } func generateSection() []obj { var out []obj for i := range 4 { seed := fmt.Sprintf("generate %d", i) var id *age.X25519Identity draws := with(seed, func() { var err error id, err = age.GenerateX25519Identity() check(err) }) raw, err := agewrap.RawX25519Identity(id) check(err) out = append(out, obj{"seed": seed, "draws": drawsOf(draws), "raw": h(raw), "identity": id.String(), "recipient": id.Recipient().String()}) } return out } func lengthsSection() obj { var stream, payload []obj for _, n := range []int{0, 1, 65535, 65536, 65537, 131072, 131073, 1 << 20, 1<<30 + 1, 5_000_000_000, 1<<40 + 7} { stream = append(stream, obj{"n": n, "length": testkit.StreamLen(n)}) } for _, n := range []uint64{0, 1, 65536, 65537, 1 << 32, 1<<40 + 3} { payload = append(payload, obj{"p": n, "length": capsule.PayloadAgeLength(n)}) } return obj{"stream": stream, "payload_age": payload} } func main() { out := flag.String("out", "", "where the vectors go") src := flag.String("source", "", "the commit of datekeys-go") flag.Parse() if *out == "" || *src == "" { log.Fatal("usage: -source -out ") } doc := obj{ "source": *src, "go": runtime.Version(), "description": "age files that filippo.io/age v1.3.2 and agewrap write while crypto/rand reads the keystream of SeededRandomSource (ChaCha20 under SHA-256(seed), zero nonce), with each draw; the texts of the errors of the writer; and the lengths of tool/age_writer_go_vectors.go. The plaintext of n bytes has (31·i + 7) mod 256 as byte i. A case marked node false is left out compiled to JavaScript.", "seeded": seededSection(), "rand_int": randIntSection(), "permute": permuteSection(), "encrypt": encryptSection(), "errors": errorsSection(), "constructors": constructorSection(), "stream": streamSection(), "parse": parseSection(), "check": checkSection(), "generate": generateSection(), "lengths": lengthsSection(), } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) enc.SetIndent("", " ") check(enc.Encode(doc)) path := filepath.Join(*out, "age_writer.json") check(os.WriteFile(path, buf.Bytes(), 0o644)) fmt.Printf("wrote %s, %d bytes\n", path, buf.Len()) if bytes.Contains(buf.Bytes(), []byte("'''")) { log.Fatal("the JSON holds three quotes") } dart := "// Generated by tool/age_writer_go_vectors.go from age_writer.json, for the\n" + "// tests that also run compiled to JavaScript, where no file can be read. Do\n" + "// not edit.\n\n" + "/// The text of test/vectors/age_writer.json.\n" + "const ageWriterJson = r'''\n" + buf.String() + "''';\n" dpath := filepath.Join(*out, "age_writer.g.dart") check(os.WriteFile(dpath, []byte(dart), 0o644)) fmt.Printf("wrote %s\n", dpath) }