//go:build ignore // Opens with Go the age files that the writer of datekeys-dart wrote, stage // 6a of docs/PLAN_dart.md, and writes test/vectors/age_interop.json and // age_interop.g.dart: each recipe of tool/age_interop_dart_samples.dart // with the length and the SHA-256 of its file, the file itself when it is // small, the length of its header (testkit.HeaderLen), its stanzas, the // stanza rules of agewrap on them, and the verdict of Go with each of its // openers: age.Decrypt and the reading of the whole plaintext with // // - x25519: age.ParseX25519Identity of the string; // - payload: agewrap.NewPayloadIdentity of the raw identity; // - access: agewrap.NewAccessIdentity of the slots and the raw // identities; // - scrypt: age.NewScryptIdentity with SetMaxWorkFactor; // - tlock: agewrap.NewTimeIdentity for Quicknet, the round and the // published release of another round, 1000 or 1001, those of the // fixtures; // // and, with then, the same for the plaintext it opens, an age file too. A // verdict is the length and the SHA-256 of the plaintext, or the text of // the error. // // 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, after the Dart tool wrote the samples: // // commit=$(git -C ../datekeys-go rev-parse v0.12) // root=$PWD // tmp=$(mktemp -d) // dart run tool/age_interop_dart_samples.dart "$tmp/samples" // git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp" // cp tool/age_interop_go_verdicts.go "$tmp" // (cd "$tmp" && go run ./age_interop_go_verdicts.go -source "$commit" \ // -samples "$tmp/samples" -out "$root/test/vectors") // rm -rf "$tmp" package main import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "flag" "fmt" "io" "log" "os" "path/filepath" "runtime" "filippo.io/age" "g.activething.com/go/DateKeys/agewrap" "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 } func num(v any) int64 { n, err := v.(json.Number).Int64() check(err) return n } // The published Quicknet signatures of rounds 1000 and 1001, as in the // fixtures. var releases = map[int64]string{ 1000: "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", 1001: "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", } func identityOf(o obj) age.Identity { switch { case o["x25519"] != nil: id, err := age.ParseX25519Identity(o["x25519"].(string)) check(err) return id case o["payload"] != nil: id, err := agewrap.NewPayloadIdentity(mustHex(o["payload"].(string))) check(err) return id case o["access"] != nil: var ids []age.Identity for _, raw := range o["access"].([]any) { id, err := agewrap.X25519IdentityFromRaw(mustHex(raw.(string))) check(err) ids = append(ids, id) } a, err := agewrap.NewAccessIdentity(int(num(o["slots"])), ids...) check(err) return a case o["scrypt"] != nil: s, err := age.NewScryptIdentity(o["scrypt"].(string)) check(err) s.SetMaxWorkFactor(int(num(o["max_work_factor"]))) return s default: round, release := num(o["tlock"]), num(o["release"]) id, err := agewrap.NewTimeIdentity(profile.Quicknet(), uint64(round), provider.Release{Round: uint64(release), Signature: mustHex(releases[release])}) check(err) return id } } func open(file []byte, o obj) ([]byte, error) { r, err := age.Decrypt(bytes.NewReader(file), identityOf(o)) if err != nil { return nil, err } plain, err := io.ReadAll(r) if err != nil { return nil, err } if then, ok := o["then"].(obj); ok { return open(plain, then) } return plain, nil } func ruleText(err error) string { if err != nil { return err.Error() } return "ok" } func main() { samplesDir := flag.String("samples", "", "the directory of the samples") outDir := flag.String("out", "", "where the vectors go") src := flag.String("source", "", "the commit of datekeys-go") flag.Parse() if *samplesDir == "" || *outDir == "" || *src == "" { log.Fatal("usage: -source -samples -out ") } raw, err := os.ReadFile(filepath.Join(*samplesDir, "samples.json")) check(err) dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() var in struct { Samples []obj `json:"samples"` } check(dec.Decode(&in)) q := profile.Quicknet() var out []obj for _, s := range in.Samples { file, err := os.ReadFile(filepath.Join(*samplesDir, s["file"].(string))) check(err) delete(s, "file") n, err := testkit.HeaderLen(file) check(err) stanzas, err := agewrap.Stanzas(bytes.NewReader(file)) check(err) var types []string for _, st := range stanzas { types = append(types, st.Type) } s["file_length"] = len(file) s["file_sha256"] = sum(file) s["header_length"] = n s["stanzas"] = types s["rules"] = obj{ "payload": ruleText(agewrap.CheckPayloadStanzas(stanzas)), "access_0": ruleText(agewrap.CheckAccessStanzas(stanzas, 0)), "access_16": ruleText(agewrap.CheckAccessStanzas(stanzas, agewrap.AccessSlots)), "time_1000": ruleText(agewrap.CheckTimeStanzas(stanzas, q, 1000)), } if len(file) <= 4096 { s["file"] = h(file) } var verdicts []obj for _, o := range s["openers"].([]any) { plain, err := open(file, o.(obj)) if err != nil { verdicts = append(verdicts, obj{"error": err.Error()}) continue } verdicts = append(verdicts, obj{"plaintext_length": len(plain), "plaintext_sha256": sum(plain)}) } s["verdicts"] = verdicts out = append(out, s) fmt.Printf("%s: %d bytes\n", s["name"], len(file)) } doc := obj{ "source": *src, "go": runtime.Version(), "description": "Age files that datekeys-dart writes with SeededRandomSource from the recipes of test/age_interop_support.dart, opened by filippo.io/age v1.3.2 and agewrap (tool/age_interop_go_verdicts.go): the length and the SHA-256 of each file, the file when small, the length of its header, its stanzas, the stanza rules of agewrap on them, and the verdict of Go with each opener, the length and the SHA-256 of the plaintext or the text of the error. The plaintext of n bytes has (31·i + 7) mod 256 as byte i. A sample marked node false is left out compiled to JavaScript.", "samples": out, } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) enc.SetIndent("", " ") check(enc.Encode(doc)) path := filepath.Join(*outDir, "age_interop.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_interop_go_verdicts.go from age_interop.json, for\n" + "// the tests that also run compiled to JavaScript, where no file can be\n" + "// read. Do not edit.\n\n" + "/// The text of test/vectors/age_interop.json.\n" + "const ageInteropJson = r'''\n" + buf.String() + "''';\n" dpath := filepath.Join(*outDir, "age_interop.g.dart") check(os.WriteFile(dpath, []byte(dart), 0o644)) fmt.Printf("wrote %s\n", dpath) }