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.
950 lines
38 KiB
950 lines
38 KiB
//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<<logN, 8, 1, 32)
|
|
check(err)
|
|
a, err := chacha20poly1305.New(k)
|
|
check(err)
|
|
return &age.Stanza{Type: "scrypt", Args: []string{b64.EncodeToString(salt), wf}, Body: a.Seal(nil, make([]byte, 12), fileKey, nil)}
|
|
}
|
|
|
|
// An identity of a case, as the tests build it again.
|
|
type idSpec struct {
|
|
X25519 string `json:"x25519,omitempty"`
|
|
Scrypt string `json:"scrypt,omitempty"`
|
|
MaxWorkFactor int `json:"max_work_factor,omitempty"`
|
|
}
|
|
|
|
func (s idSpec) identity() age.Identity {
|
|
if s.X25519 != "" {
|
|
id, err := age.ParseX25519Identity(s.X25519)
|
|
check(err)
|
|
return id
|
|
}
|
|
id, err := age.NewScryptIdentity(s.Scrypt)
|
|
check(err)
|
|
id.SetMaxWorkFactor(s.MaxWorkFactor)
|
|
return id
|
|
}
|
|
|
|
// result decrypts file with ids as capsule.decryptAll does and returns the
|
|
// outcome: the SHA-256 of the plaintext, or the text of the error and its
|
|
// phase, header for age.Decrypt and payload for the STREAM.
|
|
func result(file []byte, ids ...age.Identity) obj {
|
|
r, err := age.Decrypt(bytes.NewReader(file), ids...)
|
|
if err != nil {
|
|
return obj{"error": err.Error(), "phase": "header"}
|
|
}
|
|
plain, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return obj{"error": err.Error(), "phase": "payload"}
|
|
}
|
|
return obj{"plaintext_sha256": sum(plain), "plaintext_length": len(plain)}
|
|
}
|
|
|
|
// A part of a file: text or hex, repeated.
|
|
type part struct {
|
|
Text *string `json:"text,omitempty"`
|
|
Hex *string `json:"hex,omitempty"`
|
|
Repeat int `json:"repeat,omitempty"`
|
|
}
|
|
|
|
// txt is a text part, or a hex one when s is not UTF-8, which JSON cannot
|
|
// hold.
|
|
func txt(s string) part {
|
|
if !utf8.ValidString(s) {
|
|
return hx([]byte(s))
|
|
}
|
|
return part{Text: &s}
|
|
}
|
|
func hx(b []byte) part { s := h(b); return part{Hex: &s} }
|
|
func rep(s string, n int) part { return part{Text: &s, Repeat: n} }
|
|
func build(parts []part) []byte {
|
|
var buf bytes.Buffer
|
|
for _, p := range parts {
|
|
var b []byte
|
|
if p.Text != nil {
|
|
b = []byte(*p.Text)
|
|
} else {
|
|
b = mustHex(*p.Hex)
|
|
}
|
|
n := p.Repeat
|
|
if n == 0 {
|
|
n = 1
|
|
}
|
|
for range n {
|
|
buf.Write(b)
|
|
}
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func stanzasOf(ss []*age.Stanza) []obj {
|
|
var out []obj
|
|
for _, s := range ss {
|
|
args := s.Args
|
|
if args == nil {
|
|
args = []string{}
|
|
}
|
|
out = append(out, obj{"type": s.Type, "args": args, "body_length": len(s.Body), "body_sha256": sum(s.Body)})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// probe gives the stanzas of agewrap.Stanzas: all of them when they are
|
|
// few, else their number and the first and the last.
|
|
func probe(file []byte) obj {
|
|
ss, err := agewrap.Stanzas(bytes.NewReader(file))
|
|
if err != nil {
|
|
return obj{"error": err.Error()}
|
|
}
|
|
if len(ss) <= 16 {
|
|
return obj{"count": len(ss), "stanzas": stanzasOf(ss)}
|
|
}
|
|
return obj{"count": len(ss), "first": stanzasOf(ss[:1])[0], "last": stanzasOf(ss[len(ss)-1:])[0]}
|
|
}
|
|
|
|
func errText(err error) any {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
func main() {
|
|
outDir := flag.String("out", "", "directory of the vectors")
|
|
flag.StringVar(&testdata, "testdata", "../datekeys-dart/testdata", "the testdata/ of datekeys-dart, synced from datekeys-go")
|
|
flag.Parse()
|
|
if *outDir == "" {
|
|
log.Fatal("-out is required")
|
|
}
|
|
ageJSON(*outDir)
|
|
fixturesJSON(*outDir)
|
|
}
|
|
|
|
func ageJSON(outDir string) {
|
|
primary, err := age.GenerateX25519Identity()
|
|
check(err)
|
|
other, err := age.GenerateX25519Identity()
|
|
check(err)
|
|
mainID := idSpec{X25519: primary.String()}
|
|
otherID := idSpec{X25519: other.String()}
|
|
const pass = "correct horse battery staple"
|
|
mainRaw, err := agewrap.RawX25519Recipient(primary.Recipient())
|
|
check(err)
|
|
|
|
doc := obj{
|
|
"description": "age files written by filippo.io/age v1.3.2 and read by it, with the text of Go's error for each case; see tool/gen_age_vectors.go. A file is hex, or parts (text or hex, repeated) to concatenate. A large file is its header, its nonce and its file key: its plaintext is pattern(length), byte i = (31 i + 7) mod 256, encrypted again by the test, which checks the SHA-256 of the whole file. An identity is {x25519: AGE-SECRET-KEY-1...} or {scrypt: passphrase, max_work_factor}. A result is the SHA-256 and length of the plaintext, or the error of age.Decrypt (phase header) or of reading its plaintext (phase payload).",
|
|
"generator": "tool/gen_age_vectors.go, " + runtime.Version(),
|
|
"identities": obj{"main": primary.String(), "main_recipient": primary.Recipient().String(), "main_recipient_raw": h(mainRaw), "other": other.String(), "passphrase": pass},
|
|
}
|
|
|
|
// Files.
|
|
files := map[string][]byte{}
|
|
var fileList []obj
|
|
addFile := func(name string, file []byte, plain []byte) {
|
|
files[name] = file
|
|
fileList = append(fileList, obj{"name": name, "hex": h(file), "plaintext": h(plain)})
|
|
}
|
|
for _, n := range []int{0, 1, 100} {
|
|
p := pattern(n)
|
|
addFile(fmt.Sprintf("x25519_%d", n), encrypt(p, primary.Recipient()), p)
|
|
}
|
|
addFile("x25519_two_recipients", encrypt(pattern(50), other.Recipient(), primary.Recipient()), pattern(50))
|
|
for _, logN := range []int{1, 2, 10, 16, 17} {
|
|
r, err := age.NewScryptRecipient(pass)
|
|
check(err)
|
|
r.SetWorkFactor(logN)
|
|
p := pattern(40)
|
|
addFile(fmt.Sprintf("scrypt_%d", logN), encrypt(p, r), p)
|
|
}
|
|
// An scrypt stanza beside an X25519 one, which age.Encrypt never writes.
|
|
addFile("x25519_and_scrypt", encrypt(pattern(10), primary.Recipient(), fnRecipient(func(fk []byte) []*age.Stanza {
|
|
return []*age.Stanza{scryptStanza(fk, pass, pattern(16), 1, "1")}
|
|
})), pattern(10))
|
|
// scrypt stanzas of every form that age refuses, each with a valid MAC.
|
|
scryptForms := []struct {
|
|
name string
|
|
s func(fk []byte) *age.Stanza
|
|
}{
|
|
{"scrypt_wf_leading_zero", func(fk []byte) *age.Stanza { return scryptStanza(fk, pass, pattern(16), 1, "01") }},
|
|
{"scrypt_wf_zero", func(fk []byte) *age.Stanza { return scryptStanza(fk, pass, pattern(16), 1, "0") }},
|
|
{"scrypt_wf_sign", func(fk []byte) *age.Stanza { return scryptStanza(fk, pass, pattern(16), 1, "+1") }},
|
|
{"scrypt_wf_letters", func(fk []byte) *age.Stanza { return scryptStanza(fk, pass, pattern(16), 1, "1a") }},
|
|
{"scrypt_wf_out_of_range", func(fk []byte) *age.Stanza {
|
|
return scryptStanza(fk, pass, pattern(16), 1, "99999999999999999999")
|
|
}},
|
|
{"scrypt_wf_max_int64", func(fk []byte) *age.Stanza {
|
|
return scryptStanza(fk, pass, pattern(16), 1, "9223372036854775807")
|
|
}},
|
|
{"scrypt_wf_23", func(fk []byte) *age.Stanza { return scryptStanza(fk, pass, pattern(16), 1, "23") }},
|
|
{"scrypt_one_arg", func(fk []byte) *age.Stanza {
|
|
s := scryptStanza(fk, pass, pattern(16), 1, "1")
|
|
s.Args = s.Args[:1]
|
|
return s
|
|
}},
|
|
{"scrypt_salt_15", func(fk []byte) *age.Stanza { return scryptStanza(fk, pass, pattern(15), 1, "1") }},
|
|
{"scrypt_salt_padded", func(fk []byte) *age.Stanza {
|
|
s := scryptStanza(fk, pass, pattern(16), 1, "1")
|
|
s.Args[0] += "=="
|
|
return s
|
|
}},
|
|
{"scrypt_body_31", func(fk []byte) *age.Stanza {
|
|
s := scryptStanza(fk, pass, pattern(16), 1, "1")
|
|
s.Body = s.Body[:31]
|
|
return s
|
|
}},
|
|
}
|
|
for _, f := range scryptForms {
|
|
addFile(f.name, encrypt(pattern(5), fnRecipient(func(fk []byte) []*age.Stanza { return []*age.Stanza{f.s(fk)} })), pattern(5))
|
|
}
|
|
// X25519 stanzas of every form that age refuses, each with a valid MAC.
|
|
// u = 1, a point of low order: X25519 of any scalar and it is zero.
|
|
lowOrder := append([]byte{1}, make([]byte, 31)...)
|
|
if _, err := curve25519.X25519(pattern(32), lowOrder); err == nil {
|
|
log.Fatal("u = 1 is not refused")
|
|
}
|
|
x25519Forms := []struct {
|
|
name string
|
|
s func(fk []byte) *age.Stanza
|
|
}{
|
|
{"x25519_no_arg", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Args = nil
|
|
return s
|
|
}},
|
|
{"x25519_two_args", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Args = append(s.Args, "x")
|
|
return s
|
|
}},
|
|
{"x25519_share_31", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Args[0] = b64.EncodeToString(pattern(31))
|
|
return s
|
|
}},
|
|
{"x25519_share_bad_base64", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Args[0] = s.Args[0][:42] + "B"
|
|
return s
|
|
}},
|
|
{"x25519_share_low_order", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Args[0] = b64.EncodeToString(lowOrder)
|
|
return s
|
|
}},
|
|
{"x25519_body_33", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Body = append(s.Body, 0)
|
|
return s
|
|
}},
|
|
{"x25519_body_tampered", func(fk []byte) *age.Stanza {
|
|
s := x25519Stanza(fk, mainRaw, pattern(32))
|
|
s.Body[3] ^= 1
|
|
return s
|
|
}},
|
|
{"x25519_custom_valid", func(fk []byte) *age.Stanza { return x25519Stanza(fk, mainRaw, pattern(32)) }},
|
|
}
|
|
for _, f := range x25519Forms {
|
|
addFile(f.name, encrypt(pattern(5), fnRecipient(func(fk []byte) []*age.Stanza { return []*age.Stanza{f.s(fk)} })), pattern(5))
|
|
}
|
|
doc["files"] = fileList
|
|
|
|
// Large files: more than one STREAM chunk.
|
|
var large []obj
|
|
largeFiles := map[string][]byte{}
|
|
for _, n := range []int{65535, 65536, 65537, 131072, 140000} {
|
|
file := encrypt(pattern(n), primary.Recipient())
|
|
hdr, err := age.ExtractHeader(bytes.NewReader(file))
|
|
check(err)
|
|
fk, err := age.DecryptHeader(hdr, primary)
|
|
check(err)
|
|
name := fmt.Sprintf("x25519_large_%d", n)
|
|
largeFiles[name] = file
|
|
files[name] = file
|
|
large = append(large, obj{"name": name, "header": h(hdr), "nonce": h(file[len(hdr) : len(hdr)+16]), "file_key": h(fk), "length": n, "file_length": len(file), "file_sha256": sum(file)})
|
|
}
|
|
doc["large"] = large
|
|
|
|
// Cases: a file, an edit, identities, and Go's result.
|
|
type edit struct {
|
|
Truncate *int `json:"truncate,omitempty"`
|
|
Append string `json:"append,omitempty"`
|
|
At *int `json:"at,omitempty"`
|
|
Xor string `json:"xor,omitempty"`
|
|
}
|
|
apply := func(b []byte, e *edit) []byte {
|
|
b = slices.Clone(b)
|
|
if e == nil {
|
|
return b
|
|
}
|
|
if e.Truncate != nil {
|
|
b = b[:*e.Truncate]
|
|
}
|
|
if e.At != nil {
|
|
x := mustHex(e.Xor)
|
|
for i := range x {
|
|
b[*e.At+i] ^= x[i]
|
|
}
|
|
}
|
|
return append(b, mustHex(e.Append)...)
|
|
}
|
|
intp := func(i int) *int { return &i }
|
|
var caseList []obj
|
|
addCase := func(name, file string, e *edit, ids ...idSpec) {
|
|
b := apply(files[file], e)
|
|
var ages []age.Identity
|
|
for _, s := range ids {
|
|
ages = append(ages, s.identity())
|
|
}
|
|
c := obj{"name": name, "file": file, "identities": ids, "result": result(b, ages...)}
|
|
if ids == nil {
|
|
c["identities"] = []idSpec{}
|
|
}
|
|
if e != nil {
|
|
c["edit"] = e
|
|
}
|
|
caseList = append(caseList, c)
|
|
}
|
|
scryptID := func(p string, max int) idSpec { return idSpec{Scrypt: p, MaxWorkFactor: max} }
|
|
for _, f := range fileList {
|
|
name := f["name"].(string)
|
|
switch {
|
|
case strings.HasPrefix(name, "scrypt_"), name == "x25519_and_scrypt":
|
|
addCase(name+", passphrase, max 16", name, nil, scryptID(pass, 16))
|
|
addCase(name+", passphrase, max 22", name, nil, scryptID(pass, 22))
|
|
addCase(name+", X25519 and passphrase", name, nil, mainID, scryptID(pass, 16))
|
|
default:
|
|
addCase(name+", main identity", name, nil, mainID)
|
|
}
|
|
}
|
|
addCase("x25519_1, other identity", "x25519_1", nil, otherID)
|
|
addCase("x25519_1, other and main", "x25519_1", nil, otherID, mainID)
|
|
addCase("x25519_1, two other identities", "x25519_1", nil, otherID, otherID)
|
|
addCase("x25519_1, passphrase", "x25519_1", nil, scryptID(pass, 16))
|
|
addCase("x25519_1, passphrase then main", "x25519_1", nil, scryptID(pass, 16), mainID)
|
|
addCase("x25519_1, no identity", "x25519_1", nil)
|
|
addCase("x25519_two_recipients, other", "x25519_two_recipients", nil, otherID)
|
|
addCase("scrypt_2, wrong passphrase", "scrypt_2", nil, scryptID("wrong", 16))
|
|
addCase("scrypt_2, wrong passphrase then main", "scrypt_2", nil, scryptID("wrong", 16), mainID)
|
|
addCase("scrypt_17, max 17", "scrypt_17", nil, scryptID(pass, 17))
|
|
|
|
// Truncations and manipulations of x25519_100 and of the large files.
|
|
small := files["x25519_100"]
|
|
hdrSmall, err := age.ExtractHeader(bytes.NewReader(small))
|
|
check(err)
|
|
hl := len(hdrSmall)
|
|
for _, n := range []int{0, 10, 21, 22, 23, hl - 2, hl - 1, hl, hl + 1, hl + 15, hl + 16, hl + 17, hl + 31, hl + 32, len(small) - 1} {
|
|
addCase(fmt.Sprintf("x25519_100 truncated to %d", n), "x25519_100", &edit{Truncate: intp(n)}, mainID)
|
|
}
|
|
addCase("x25519_100 and a byte", "x25519_100", &edit{Append: "00"}, mainID)
|
|
for _, at := range []int{0, 5, 21, 25, hl - 10, hl - 2, hl, hl + 15, hl + 16, hl + 50, len(small) - 1} {
|
|
addCase(fmt.Sprintf("x25519_100, byte %d changed", at), "x25519_100", &edit{At: intp(at), Xor: "01"}, mainID)
|
|
}
|
|
for _, name := range []string{"x25519_large_65535", "x25519_large_65536", "x25519_large_65537", "x25519_large_131072", "x25519_large_140000"} {
|
|
f := files[name]
|
|
hdr, err := age.ExtractHeader(bytes.NewReader(f))
|
|
check(err)
|
|
p := len(hdr) + 16
|
|
const enc = 65536 + 16
|
|
cuts := []int{p, p + 1, p + 16, p + enc - 1, p + enc, p + enc + 1, p + enc + 16, p + enc + 17, len(f) - 1, len(f) - 16, len(f) - 17}
|
|
for _, n := range cuts {
|
|
if n < 0 || n > 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())
|
|
}
|