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.
671 lines
21 KiB
671 lines
21 KiB
package testkit
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
"g.activething.com/go/DateKeys/internal/cbortest"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
// DifferentialSeed seeds the generator of the differential corpus.
|
|
const DifferentialSeed = 20260925
|
|
|
|
// DifferentialFile is testdata/vectors/inspect_differential.json.
|
|
type DifferentialFile struct {
|
|
Spec string `json:"spec"`
|
|
Description string `json:"description"`
|
|
Format string `json:"format"`
|
|
Seed uint64 `json:"seed"`
|
|
Bases []DifferentialBase `json:"bases"`
|
|
Mutations []DifferentialCase `json:"mutations"`
|
|
}
|
|
|
|
// DifferentialBase is an official .dkc fixture the mutations edit.
|
|
type DifferentialBase struct {
|
|
File string `json:"file"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
|
|
// DifferentialCase is one mutation and the verdict of capsule.Inspect.
|
|
type DifferentialCase struct {
|
|
Base int `json:"base"` // index into Bases
|
|
Kind string `json:"kind"`
|
|
Edits []Edit `json:"edits"`
|
|
// Result is ResultOK or the normative code of the failure.
|
|
Result string `json:"result"`
|
|
// Step is the step of spec §63 that failed; absent when Result is ok.
|
|
Step int `json:"step,omitempty"`
|
|
}
|
|
|
|
const differentialFormat = "Each mutation is bases[base].file (in testdata/fixtures) with its edits applied. " +
|
|
"An edit is [at, delete, insert]: the delete bytes at offset at of the base are replaced by the bytes of the hex string insert. " +
|
|
"The edits of one mutation refer to offsets of the unmodified base, are sorted by offset and do not overlap. " +
|
|
"result is the verdict of steps 1 to 8 of spec §63 (capsule.Inspect, the Quicknet profile pinned, no extension known, no network, no secret): " +
|
|
"ok, or the normative error code, with step the step that failed. kind names the generator of the mutation and is informative."
|
|
|
|
// Kinds of differential mutations and how many each base gets.
|
|
var differentialKinds = []struct {
|
|
kind string
|
|
count int
|
|
}{
|
|
{"flip", 60}, // one bit flipped
|
|
{"byte", 30}, // one byte replaced
|
|
{"truncate", 25}, // the file cut short
|
|
{"insert", 30}, // one to four bytes inserted
|
|
{"delete", 30}, // one to four bytes deleted
|
|
{"length", 25}, // PUBLIC_HEADER_LEN or SEALED_CONTROL_LEN edited
|
|
{"header", 80}, // PUBLIC_HEADER re-encoded with a CBOR-aware change
|
|
{"datekey", 25}, // PUBLIC_HEADER re-encoded with another DateKey string
|
|
{"age", 60}, // an age header of SEALED_CONTROL or PAYLOAD_AGE edited
|
|
}
|
|
|
|
// InspectVerdict runs capsule.Inspect on dkc with the default registry and
|
|
// no extension known, and returns ResultOK or the error code, and the step
|
|
// that failed.
|
|
func InspectVerdict(dkc []byte) (string, int) {
|
|
in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: Registry()})
|
|
if err == nil {
|
|
return ResultOK, 0
|
|
}
|
|
step := 0
|
|
if n := len(in.Checks); n > 0 && !in.Checks[n-1].OK {
|
|
step = in.Checks[n-1].Step
|
|
}
|
|
return Result(err), step
|
|
}
|
|
|
|
// splitmix64 is the generator of the corpus: fixed, simple and independent
|
|
// of the Go release.
|
|
type splitmix64 struct{ s uint64 }
|
|
|
|
func (r *splitmix64) next() uint64 {
|
|
r.s += 0x9e3779b97f4a7c15
|
|
z := r.s
|
|
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
|
|
z = (z ^ (z >> 27)) * 0x94d049bb133111eb
|
|
return z ^ (z >> 31)
|
|
}
|
|
|
|
// intn returns a number in [0, n).
|
|
func (r *splitmix64) intn(n int) int { return int(r.next() % uint64(n)) }
|
|
|
|
func (r *splitmix64) byte() byte { return byte(r.next()) }
|
|
|
|
func (r *splitmix64) bytes(n int) []byte {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = r.byte()
|
|
}
|
|
return b
|
|
}
|
|
|
|
func pick[T any](r *splitmix64, s []T) T { return s[r.intn(len(s))] }
|
|
|
|
// diffBase is a base fixture and the offsets of its sections.
|
|
type diffBase struct {
|
|
file string
|
|
dkc []byte
|
|
parts Parts
|
|
header map[uint64]any
|
|
sealedAt, payloadAt int // offsets of SEALED_CONTROL and PAYLOAD_AGE
|
|
sealedHdr, payHdr int // lengths of their age headers
|
|
interesting int // the end of the bytes Inspect reads, and a little more
|
|
regions [][2]int
|
|
}
|
|
|
|
func newDiffBase(dir, name string) (*diffBase, error) {
|
|
f, err := LoadFixture(dir, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := &diffBase{file: f.File, dkc: f.DKC, parts: f.Parts}
|
|
if b.header, err = cbortest.UnmarshalMap(f.Parts.Header); err != nil {
|
|
return nil, err
|
|
}
|
|
b.sealedAt = capsule.PreludeSize + len(f.Parts.Header)
|
|
b.payloadAt = b.sealedAt + len(f.Parts.Sealed)
|
|
if b.sealedHdr, err = HeaderLen(f.Parts.Sealed); err != nil {
|
|
return nil, err
|
|
}
|
|
if b.payHdr, err = HeaderLen(f.Parts.Payload); err != nil {
|
|
return nil, err
|
|
}
|
|
b.interesting = min(len(b.dkc), b.payloadAt+b.payHdr+64)
|
|
b.regions = [][2]int{
|
|
{0, capsule.PreludeSize},
|
|
{capsule.PreludeSize, b.sealedAt},
|
|
{b.sealedAt, b.sealedAt + b.sealedHdr},
|
|
{b.sealedAt + b.sealedHdr, b.payloadAt},
|
|
{b.payloadAt, b.payloadAt + b.payHdr},
|
|
{b.payloadAt + b.payHdr, len(b.dkc)},
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// position picks an offset, weighted towards the bytes steps 1 to 8 read:
|
|
// the prelude, the header, and the age headers of the two age files.
|
|
func (b *diffBase) position(r *splitmix64) int {
|
|
weights := []int{15, 30, 25, 5, 20, 5}
|
|
n := r.intn(100)
|
|
for i, w := range weights {
|
|
if n < w {
|
|
reg := b.regions[i]
|
|
if reg[1] > reg[0] {
|
|
return reg[0] + r.intn(reg[1]-reg[0])
|
|
}
|
|
break
|
|
}
|
|
n -= w
|
|
}
|
|
return r.intn(len(b.dkc))
|
|
}
|
|
|
|
// lengths returns an edit of the prelude that sets the section lengths.
|
|
func (b *diffBase) lengths(headerLen, sealedLen int) Edit {
|
|
var v [8]byte
|
|
binary.BigEndian.PutUint32(v[0:4], uint32(headerLen))
|
|
binary.BigEndian.PutUint32(v[4:8], uint32(sealedLen))
|
|
return Edit{At: 8, Delete: 8, Insert: v[:]}
|
|
}
|
|
|
|
// InspectDifferential computes testdata/vectors/inspect_differential.json:
|
|
// deterministic mutations of the official .dkc fixtures of dir, each with
|
|
// the verdict of capsule.Inspect.
|
|
func InspectDifferential(dir string, names []string) (DifferentialFile, error) {
|
|
f := DifferentialFile{
|
|
Spec: SpecVersion,
|
|
Description: "Differential corpus of the pre-unlock checks (spec §63 steps 1 to 8): deterministic mutations of the official .dkc fixtures " +
|
|
"with the verdict of the reference implementation. See testdata/README.md.",
|
|
Format: differentialFormat,
|
|
Seed: DifferentialSeed,
|
|
}
|
|
r := &splitmix64{s: DifferentialSeed}
|
|
for bi, name := range names {
|
|
b, err := newDiffBase(dir, name)
|
|
if err != nil {
|
|
return f, err
|
|
}
|
|
sum := sha256.Sum256(b.dkc)
|
|
f.Bases = append(f.Bases, DifferentialBase{File: b.file, SHA256: hex.EncodeToString(sum[:])})
|
|
seen := map[[32]byte]bool{sum: true}
|
|
for _, k := range differentialKinds {
|
|
made := 0
|
|
for attempt := 0; made < k.count; attempt++ {
|
|
if attempt > 50*k.count {
|
|
return f, fmt.Errorf("differential %s %s: only %d distinct mutations", name, k.kind, made)
|
|
}
|
|
edits, err := b.mutate(r, k.kind)
|
|
if err != nil {
|
|
return f, fmt.Errorf("differential %s %s: %w", name, k.kind, err)
|
|
}
|
|
if edits, err = normalizeEdits(b.dkc, edits); err != nil {
|
|
return f, err
|
|
}
|
|
out, err := ApplyEdits(b.dkc, edits)
|
|
if err != nil {
|
|
return f, err
|
|
}
|
|
h := sha256.Sum256(out)
|
|
if len(edits) == 0 || seen[h] {
|
|
continue
|
|
}
|
|
seen[h] = true
|
|
result, step := InspectVerdict(out)
|
|
f.Mutations = append(f.Mutations, DifferentialCase{Base: bi, Kind: k.kind, Edits: edits, Result: result, Step: step})
|
|
made++
|
|
}
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (b *diffBase) mutate(r *splitmix64, kind string) ([]Edit, error) {
|
|
switch kind {
|
|
case "flip":
|
|
p := b.position(r)
|
|
return []Edit{{At: p, Delete: 1, Insert: []byte{b.dkc[p] ^ 1<<r.intn(8)}}}, nil
|
|
case "byte":
|
|
p := b.position(r)
|
|
v := pick(r, []byte{0x00, 0xff, 0x7f, 0x80, 0x18, 0x1b, 0x40, 0x60, 0x80, 0xa0, 0xf6, '\n', ' ', '-', r.byte()})
|
|
return []Edit{{At: p, Delete: 1, Insert: []byte{v}}}, nil
|
|
case "truncate":
|
|
p := r.intn(b.interesting)
|
|
return []Edit{{At: p, Delete: len(b.dkc) - p}}, nil
|
|
case "insert":
|
|
p := b.position(r)
|
|
ins := r.bytes(1 + r.intn(4))
|
|
if r.intn(3) == 0 {
|
|
ins = []byte{pick(r, []byte{0x00, 0xff, 0xf6, 0x20, '\n', ' ', '\r'})}
|
|
}
|
|
return []Edit{{At: p, Insert: ins}}, nil
|
|
case "delete":
|
|
p := b.position(r)
|
|
return []Edit{{At: p, Delete: min(1+r.intn(4), len(b.dkc)-p)}}, nil
|
|
case "length":
|
|
return b.lengthEdit(r), nil
|
|
case "header":
|
|
return b.headerEdit(r)
|
|
case "datekey":
|
|
m := with(b.header)
|
|
m[3] = r.dateKey(b.dateKey())
|
|
h, err := cbortest.Marshal(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.replaceHeader(h, false), nil
|
|
case "age":
|
|
return b.ageEdit(r)
|
|
}
|
|
return nil, fmt.Errorf("unknown kind %q", kind)
|
|
}
|
|
|
|
func (b *diffBase) lengthEdit(r *splitmix64) []Edit {
|
|
hl, sl := len(b.parts.Header), len(b.parts.Sealed)
|
|
vary := func(v int) int {
|
|
switch r.intn(8) {
|
|
case 0:
|
|
return v + 1
|
|
case 1:
|
|
return v - 1
|
|
case 2:
|
|
return v + 1 + r.intn(64)
|
|
case 3:
|
|
return max(0, v-1-r.intn(64))
|
|
case 4:
|
|
return pick(r, []int{0, 1, 16, 255, 256, 65535, 65536})
|
|
case 5:
|
|
return pick(r, []int{capsule.MaxPublicHeaderLen, capsule.MaxPublicHeaderLen + 1, capsule.MaxSealedControlLen, capsule.MaxSealedControlLen + 1, 1<<32 - 1})
|
|
case 6:
|
|
return int(r.next() >> 32)
|
|
}
|
|
return v ^ 1<<(8+r.intn(24))
|
|
}
|
|
switch r.intn(4) {
|
|
case 0:
|
|
hl = vary(hl)
|
|
case 1:
|
|
sl = vary(sl)
|
|
case 2:
|
|
hl, sl = sl, hl
|
|
default:
|
|
hl, sl = vary(hl), vary(sl)
|
|
}
|
|
return []Edit{b.lengths(hl, sl)}
|
|
}
|
|
|
|
// replaceHeader returns the edits that replace PUBLIC_HEADER with h and,
|
|
// unless keepPrelude, set PUBLIC_HEADER_LEN to its length.
|
|
func (b *diffBase) replaceHeader(h []byte, keepPrelude bool) []Edit {
|
|
edits := []Edit{{At: capsule.PreludeSize, Delete: len(b.parts.Header), Insert: h}}
|
|
if !keepPrelude {
|
|
edits = append([]Edit{b.lengths(len(h), len(b.parts.Sealed))}, edits...)
|
|
}
|
|
return edits
|
|
}
|
|
|
|
// dateKey returns the DateKey of the base header.
|
|
func (b *diffBase) dateKey() datekey.DateKey {
|
|
if s, ok := b.header[3].(string); ok {
|
|
if d, err := datekey.Parse(s); err == nil {
|
|
return d
|
|
}
|
|
}
|
|
return datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}
|
|
}
|
|
|
|
func (b *diffBase) headerEdit(r *splitmix64) ([]Edit, error) {
|
|
m := with(b.header)
|
|
dk := b.dateKey()
|
|
var v any = m
|
|
switch r.intn(12) {
|
|
case 0: // remove a key
|
|
keys := sortedKeys(m)
|
|
delete(m, pick(r, keys))
|
|
case 1: // add a key
|
|
m[pick(r, []uint64{5, 6, 7, 8, 23, 24, 255, 65535, 1 << 32})] = r.value()
|
|
case 2: // change the type of a value
|
|
m[pick(r, sortedKeys(m))] = r.value()
|
|
case 3:
|
|
m[0] = pick(r, []string{"", "datekeyca", "datekeycapx", "DATEKEYCAP", capsule.ControlTypeTag, "datekeys-access-key", strings.Repeat("a", 65)})
|
|
case 4:
|
|
m[1] = pick(r, []any{uint64(0), uint64(2), uint64(255), uint64(256), uint64(1 << 32), uint64(1<<53 - 1), uint64(1 << 53), uint64(1<<64 - 1), "1", -1})
|
|
case 5:
|
|
m[2] = r.bytes(pick(r, []int{0, 1, 15, 17, 32}))
|
|
case 6:
|
|
m[3] = r.dateKey(dk)
|
|
case 7:
|
|
m[4] = pick(r, []any{uint64(2), uint64(3), uint64(23), uint64(24), uint64(255), uint64(256), uint64(257), uint64(65536), uint64(1 << 32), uint64(1<<53 - 1), uint64(1 << 53)})
|
|
case 8: // an extension array
|
|
key := uint64(5 + r.intn(2))
|
|
arr := r.extensions()
|
|
if arr == nil {
|
|
// The same extension in both arrays.
|
|
m[5] = []any{ext("org.example.a", 1)}
|
|
m[6] = []any{ext("org.example.a", 1)}
|
|
} else {
|
|
m[key] = arr
|
|
}
|
|
case 9: // a head not in its shortest form
|
|
enc, err := cbortest.Marshal(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch r.intn(4) {
|
|
case 0:
|
|
v = cbortest.Raw(append([]byte{0xb8, enc[0] & 0x1f}, enc[1:]...))
|
|
case 1:
|
|
m[4] = cbortest.Raw(pick(r, [][]byte{{0x18, 0x00}, {0x18, 0x01}, {0x19, 0x00, 0x00}, {0x1b, 0, 0, 0, 0, 0, 0, 0, 0}}))
|
|
case 2:
|
|
m[1] = cbortest.Raw{0x18, 0x01}
|
|
default:
|
|
if id, ok := m[2].([]byte); ok {
|
|
m[2] = cbortest.Raw(append([]byte{0x58, byte(len(id))}, id...))
|
|
}
|
|
}
|
|
case 10: // keys out of order or repeated
|
|
keys := sortedKeys(m)
|
|
var p cbortest.Pairs
|
|
for _, k := range keys {
|
|
p = append(p, k, m[k])
|
|
}
|
|
i := 2 * r.intn(len(keys))
|
|
j := 2 * r.intn(len(keys))
|
|
if r.intn(2) == 0 {
|
|
p[i], p[j] = p[j], p[i]
|
|
p[i+1], p[j+1] = p[j+1], p[i+1]
|
|
} else {
|
|
p = append(p, p[i], p[i+1])
|
|
}
|
|
v = p
|
|
default: // the whole item wrapped or framed differently
|
|
enc, err := cbortest.Marshal(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch r.intn(6) {
|
|
case 0:
|
|
v = cbortest.Raw(append([]byte{0xc1}, enc...))
|
|
case 1:
|
|
v = []any{cbortest.Raw(enc)}
|
|
case 2:
|
|
v = cbortest.Raw(append(append([]byte{0xbf}, enc[1:]...), 0xff))
|
|
case 3:
|
|
v = cbortest.Raw(append(enc, 0x00))
|
|
case 4:
|
|
v = cbortest.Raw(append(enc, enc...))
|
|
default:
|
|
v = cbortest.Raw(enc[:len(enc)-1])
|
|
}
|
|
}
|
|
h, err := cbortest.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.replaceHeader(h, r.intn(10) == 0), nil
|
|
}
|
|
|
|
func sortedKeys(m map[uint64]any) []uint64 {
|
|
var keys []uint64
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
for i := 1; i < len(keys); i++ {
|
|
for j := i; j > 0 && keys[j] < keys[j-1]; j-- {
|
|
keys[j], keys[j-1] = keys[j-1], keys[j]
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// value returns a value of a random type, most of them outside the profile.
|
|
func (r *splitmix64) value() any {
|
|
return pick(r, []any{
|
|
uint64(0), uint64(1), uint64(1 << 53), "x", []byte{0}, []byte{}, []any{}, map[uint64]any{},
|
|
cbortest.Raw{0xf6}, cbortest.Raw{0xf5}, cbortest.Raw{0x20}, cbortest.Raw{0xc1, 0x00},
|
|
cbortest.Raw{0xf9, 0x3c, 0x00}, cbortest.Raw{0x5f, 0x40, 0xff},
|
|
})
|
|
}
|
|
|
|
// dateKey returns a DateKey string derived from dk, most of them invalid.
|
|
func (r *splitmix64) dateKey(dk datekey.DateKey) any {
|
|
s := dk.Compact()
|
|
enc := func(j string) string { return datekey.Prefix + b64(j) }
|
|
switch r.intn(11) {
|
|
case 0: // one character changed
|
|
i := len(datekey.Prefix) + r.intn(len(s)-len(datekey.Prefix))
|
|
c := pick(r, []byte("AZaz09-_=+/ ."))
|
|
return s[:i] + string(c) + s[i+1:]
|
|
case 1:
|
|
return s[:len(datekey.Prefix)+r.intn(len(s)-len(datekey.Prefix))]
|
|
case 2:
|
|
return s + "="
|
|
case 3:
|
|
return "DK1_" + s[len(datekey.Prefix):]
|
|
case 4: // canonical, but of a profile that is not pinned
|
|
return datekey.DateKey{ProfileID: pick(r, []string{"datekeys:evmnet:v1", "datekeys:quicknet:v2", "drand:quicknet"}), Round: dk.Round}.Compact()
|
|
case 5: // canonical, but of a round the tlock stanza does not name or the profile never reaches
|
|
return datekey.DateKey{ProfileID: dk.ProfileID, Round: pick(r, []uint64{1, dk.Round - 1, dk.Round + 1, 66884212,
|
|
profile.Quicknet().MaxRound(), profile.Quicknet().MaxRound() + 1, datekey.MaxRound})}.Compact()
|
|
case 6:
|
|
return enc(fmt.Sprintf(`{"version":1,"network":"%s","round":%s}`, dk.ProfileID,
|
|
pick(r, []string{"0", "-1", "1.0", "1e3", "9007199254740992", strconv.FormatUint(dk.Round+1, 10), `"1000"`})))
|
|
case 7:
|
|
return enc(fmt.Sprintf(`{"version":1, "network":"%s", "round":%d}`, dk.ProfileID, dk.Round))
|
|
case 8:
|
|
return datekey.Prefix
|
|
case 9:
|
|
return cbortest.Raw(append([]byte{0x62}, 0xc0, 0x80))
|
|
}
|
|
return s + strings.Repeat("A", 200+r.intn(100))
|
|
}
|
|
|
|
func (r *splitmix64) extensions() []any {
|
|
many := func(n int) []any {
|
|
out := make([]any, n)
|
|
for i := range out {
|
|
out[i] = ext(fmt.Sprintf("org.example.%03d", i), 1)
|
|
}
|
|
return out
|
|
}
|
|
switch r.intn(16) {
|
|
case 0:
|
|
return []any{}
|
|
case 1:
|
|
return []any{ext("org.example.a", 1)}
|
|
case 2:
|
|
return []any{ext("org.example.a", 1, r.bytes(1+r.intn(8)))}
|
|
case 3:
|
|
return []any{ext("org.example.a", 1, []byte{})}
|
|
case 4:
|
|
return []any{ext("org.example.a", 1, pick(r, []any{"text", uint64(7), cbortest.Raw{0xf6}, map[uint64]any{}, []any{}, cbortest.Raw{0x58, 0x01, 0x2a}, cbortest.Raw{0xc1, 0x41, 0x00}}))}
|
|
case 5:
|
|
return []any{ext("org.example.a", 1<<32)}
|
|
case 6:
|
|
return many(64)
|
|
case 7:
|
|
return many(65)
|
|
case 8:
|
|
return []any{ext("org.example.b", 1), ext("org.example.a", 1)}
|
|
case 9:
|
|
return []any{ext("org.example.a", 1), ext("org.example.a", 1)}
|
|
case 10:
|
|
return []any{map[uint64]any{0: "org.example.a", 1: uint64(1), 3: uint64(0)}}
|
|
case 11:
|
|
return []any{map[uint64]any{0: "org.example.a"}}
|
|
case 12:
|
|
return []any{ext("\ufefforg.example.a", 1)}
|
|
case 13:
|
|
return []any{ext("\U00010000", 1), ext("\uff61", 1)}
|
|
case 14:
|
|
return []any{ext("\uff61", 1), ext("\U00010000", 1)}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ageEdit edits the age header of SEALED_CONTROL (and then, most of the
|
|
// time, SEALED_CONTROL_LEN) or of PAYLOAD_AGE.
|
|
func (b *diffBase) ageEdit(r *splitmix64) ([]Edit, error) {
|
|
sealed := r.intn(5) < 3
|
|
at, n := b.payloadAt, b.payHdr
|
|
if sealed {
|
|
at, n = b.sealedAt, b.sealedHdr
|
|
}
|
|
hdr := string(b.dkc[at : at+n])
|
|
lines := strings.SplitAfter(hdr, "\n")
|
|
lines = lines[:len(lines)-1] // SplitAfter leaves an empty string after the final newline
|
|
// lines: intro, stanza lines (argument line, body lines), MAC line.
|
|
stanzaAt := 1
|
|
mac := len(lines) - 1
|
|
switch r.intn(11) {
|
|
case 0: // intro line
|
|
lines[0] = pick(r, []string{"age-encryption.org/v2\n", "age-encryption.org/V1\n", "age-encryption.org/v1 \n", "age-encryption.org/\n", "\n"})
|
|
case 1: // stanza type
|
|
f := strings.Fields(lines[stanzaAt])
|
|
f[1] = pick(r, []string{"tlock", "TLOCK", "X25519", "x25519", "scrypt", "tlock2", "t"})
|
|
lines[stanzaAt] = strings.Join(f, " ") + "\n"
|
|
case 2: // one argument
|
|
f := strings.Fields(lines[stanzaAt])
|
|
if len(f) > 2 {
|
|
i := 2 + r.intn(len(f)-2)
|
|
f[i] = r.arg(f[i])
|
|
lines[stanzaAt] = strings.Join(f, " ") + "\n"
|
|
}
|
|
case 3: // one more argument, or one less
|
|
f := strings.Fields(lines[stanzaAt])
|
|
if r.intn(2) == 0 {
|
|
f = append(f, pick(r, []string{"extra", "1000", "AAAA"}))
|
|
} else if len(f) > 2 {
|
|
f = f[:len(f)-1]
|
|
}
|
|
lines[stanzaAt] = strings.Join(f, " ") + "\n"
|
|
case 4: // the stanza twice
|
|
stanza := lines[stanzaAt:mac]
|
|
lines = append(append(append([]string{}, lines[:mac]...), stanza...), lines[mac])
|
|
case 5: // an extra stanza
|
|
extra := pick(r, []string{
|
|
"-> X25519 " + strings.Repeat("A", 43) + "\n" + strings.Repeat("B", 43) + "\n",
|
|
"-> scrypt c2FsdHNhbHRzYWx0c2FsdA 18\n" + strings.Repeat("C", 43) + "\n",
|
|
"-> tlock 1000 " + profile.Quicknet().ChainHashHex() + "\n" + strings.Repeat("D", 64) + "\n\n",
|
|
"-> grease-x !@#\n\n",
|
|
})
|
|
lines = append(append(append([]string{}, lines[:mac]...), extra), lines[mac])
|
|
case 6: // no stanza
|
|
lines = []string{lines[0], lines[mac]}
|
|
case 7: // a body line
|
|
if mac-stanzaAt > 1 {
|
|
i := stanzaAt + 1 + r.intn(mac-stanzaAt-1)
|
|
l := strings.TrimSuffix(lines[i], "\n")
|
|
switch r.intn(4) {
|
|
case 0:
|
|
if len(l) > 0 {
|
|
j := r.intn(len(l))
|
|
l = l[:j] + string(pick(r, []byte("A/+=_-"))) + l[j+1:]
|
|
}
|
|
case 1:
|
|
l += "A"
|
|
case 2:
|
|
l += " "
|
|
default:
|
|
if len(l) > 0 {
|
|
l = l[:len(l)-1]
|
|
}
|
|
}
|
|
lines[i] = l + "\n"
|
|
}
|
|
case 8: // the MAC line
|
|
l := lines[mac]
|
|
switch r.intn(4) {
|
|
case 0:
|
|
l = "--" + l[3:]
|
|
case 1:
|
|
j := 4 + r.intn(len(l)-5)
|
|
l = l[:j] + string(pick(r, []byte("AB/+"))) + l[j+1:]
|
|
case 2:
|
|
l = "---\n"
|
|
default:
|
|
l = strings.TrimSuffix(l, "\n") + " \n"
|
|
}
|
|
lines[mac] = l
|
|
case 9: // a line ending
|
|
i := r.intn(len(lines))
|
|
lines[i] = strings.TrimSuffix(lines[i], "\n") + "\r\n"
|
|
default: // an empty line
|
|
i := 1 + r.intn(len(lines)-1)
|
|
lines = append(append(append([]string{}, lines[:i]...), "\n"), lines[i:]...)
|
|
}
|
|
edited := []byte(strings.Join(lines, ""))
|
|
edits := []Edit{{At: at, Delete: n, Insert: edited}}
|
|
if sealed && r.intn(10) != 0 {
|
|
edits = append([]Edit{b.lengths(len(b.parts.Header), len(b.parts.Sealed)-n+len(edited))}, edits...)
|
|
}
|
|
return edits, nil
|
|
}
|
|
|
|
// arg returns a variant of a stanza argument.
|
|
func (r *splitmix64) arg(a string) string {
|
|
if _, err := strconv.ParseUint(a, 10, 64); err == nil {
|
|
return pick(r, []string{"999", "1001", "0" + a, a + "0", "0", "-1", "18446744073709551616", a + "a", "2000"})
|
|
}
|
|
if len(a) == 0 {
|
|
return "A"
|
|
}
|
|
i := r.intn(len(a))
|
|
switch r.intn(4) {
|
|
case 0:
|
|
return a[:i] + string(pick(r, []byte("0aAf/+"))) + a[i+1:]
|
|
case 1:
|
|
return strings.ToUpper(a)
|
|
case 2:
|
|
return a[:len(a)-1]
|
|
}
|
|
return a + "A"
|
|
}
|
|
|
|
// MarshalDifferential writes f with one mutation per line.
|
|
func MarshalDifferential(f DifferentialFile) ([]byte, error) {
|
|
head := f
|
|
head.Mutations = nil
|
|
b, err := json.MarshalIndent(head, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Replace the closing "\n}" and the null mutations.
|
|
b = bytes.TrimSuffix(b, []byte("\n}"))
|
|
b = bytes.TrimSuffix(b, []byte(",\n \"mutations\": null"))
|
|
var out bytes.Buffer
|
|
out.Write(b)
|
|
out.WriteString(",\n \"mutations\": [\n")
|
|
for i, m := range f.Mutations {
|
|
line, err := json.Marshal(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.WriteString(" ")
|
|
out.Write(line)
|
|
if i < len(f.Mutations)-1 {
|
|
out.WriteByte(',')
|
|
}
|
|
out.WriteByte('\n')
|
|
}
|
|
out.WriteString(" ]\n}\n")
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// WriteDifferential writes f to path with MarshalDifferential.
|
|
func WriteDifferential(path string, f DifferentialFile) error {
|
|
b, err := MarshalDifferential(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeFile(path, b)
|
|
}
|