tool/age_writer_go_vectors.go runs in an export of datekeys-go at c531e93 and makes crypto/rand read the keystream of the seeded source of the writer: ChaCha20 under SHA-256(seed), with a zero nonce. age.Encrypt, with the real X25519, scrypt and tlock recipients, then draws known values, and age_writer.json records every draw, its size and its order, with the files: one X25519 recipient over plaintexts of 0 bytes to 3 MiB across the chunk boundaries; two, three and sixteen, and one with bit 255 set, which age accepts; scrypt with work factors 1 to 16; and the tlock stanza of rounds of 1 to 11 digits. The generator checks each file against testkit.SealAge, with the first draw as the file key and the last as the nonce, checks each X25519 stanza against its ephemeral secret, and opens the file with Go. It also records the errors of age.Encrypt with the draws before them, those of the constructors, ParseX25519Recipient, CheckX25519Recipient, GenerateX25519Identity, crypto/rand.Int and the permute of capsule over the keystream, and the lengths of testkit and capsule. The output is the same on every run. age_writer.g.dart holds the same JSON for the tests compiled to JavaScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.11
parent
a14a3b8d01
commit
6837b9c463
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,746 @@
|
||||
//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 <commit> -out <dir>")
|
||||
}
|
||||
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)
|
||||
}
|
||||
Loading…
Reference in new issue