You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
747 lines
24 KiB
747 lines
24 KiB
|
2 days ago
|
//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)
|
||
|
|
}
|