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.
DateKeys/internal/testkit/builder.go

294 lines
9.1 KiB

package testkit
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"slices"
"filippo.io/age"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/profile"
)
// Build assembles a capsule step by step like capsule.Encrypt, but lets a test
// declare one policy and build another structure, and edit the stanzas of each
// age file while keeping every MAC valid. Every other value (header_binding,
// lengths) stays consistent, as a malicious creator would make it.
//
// Build is a generator of test vectors, the only kind of writer that may write
// format 1 (spec §62.1 rule 1, §70).
type Build struct {
Profile *profile.Profile // default Quicknet
Round uint64 // default 1000
// Format is the format written: capsule.Format2 by default,
// capsule.Format1, the one of spec v0.8.2, or capsule.Format3, whose
// Plaintext is BODY (spec §29.2; see Body3).
Format capsule.Format
Declared capsule.Policy // access_policy written in PUBLIC_HEADER
Structure capsule.Policy // construction actually used
// AccessRecipients of INNER_ACCESS_AGE when Structure is time_and_key,
// first and in this order. In format 2 fresh dummies follow them up to
// Slots stanzas.
AccessRecipients []age.Recipient
// Slots is the number of stanzas of INNER_ACCESS_AGE in format 2; 0 means
// agewrap.AccessSlots.
Slots int
// Plaintext is the content. In format 2 the plaintext of PAYLOAD_AGE is
// the content and the zeros of its padding.
Plaintext []byte
// Padding is the padding rule of format 2; 0 means capsule.Reforzado.
Padding capsule.Padding
// EditPlaintext, when not nil, edits the plaintext of PAYLOAD_AGE, with
// its padding in formats 2 and 3, before it is encrypted. The control
// still declares L = len(Plaintext).
EditPlaintext func(p []byte) []byte
DateKeyString string // overrides the canonical dk1_ string in PUBLIC_HEADER
// RawPolicy, when not zero, is the access_policy value written in
// PUBLIC_HEADER instead of Declared, which may be outside V1. A header
// written with RawPolicy or DateKeyString carries no extensions.
RawPolicy uint64
HeaderCritical, HeaderNoncritical []extension.Extension
ControlCritical, ControlNoncritical []extension.Extension
// Stanza edits, applied with the corresponding file key.
EditOuter func(fileKey []byte, s []*age.Stanza) []*age.Stanza
EditInner func(fileKey []byte, s []*age.Stanza) []*age.Stanza
EditPayload func(fileKey []byte, s []*age.Stanza) []*age.Stanza
}
// Built is a capsule produced by Build and its secrets.
type Built struct {
DKC []byte
Prelude [capsule.PreludeSize]byte
PublicHeader []byte
Sealed []byte
Payload []byte
Control []byte
PayloadIdentity []byte
CapsuleID [capsule.CapsuleIDSize]byte
}
// Make builds the capsule.
func (b Build) Make() (*Built, error) {
p := b.Profile
if p == nil {
p = profile.Quicknet()
}
round := b.Round
if round == 0 {
round = 1000
}
format := b.Format
if format == 0 {
format = capsule.Format2
}
ctrl := &capsule.Control{Critical: b.ControlCritical, Noncritical: b.ControlNoncritical}
plaintext := b.Plaintext
access := b.AccessRecipients
if format == capsule.Format2 || format == capsule.Format3 {
ctrl.PayloadLength, ctrl.Padding = uint64(len(b.Plaintext)), b.Padding
if ctrl.Padding == 0 {
ctrl.Padding = capsule.Reforzado
}
padded, err := capsule.PaddedLength(ctrl.PayloadLength, ctrl.Padding)
if err != nil {
return nil, err
}
plaintext = append(bytes.Clone(b.Plaintext), make([]byte, padded-ctrl.PayloadLength)...)
if b.Structure == capsule.TimeAndKey {
if access, err = dummies(access, b.Slots); err != nil {
return nil, err
}
}
}
if b.EditPlaintext != nil {
plaintext = b.EditPlaintext(bytes.Clone(plaintext))
}
out := &Built{}
if _, err := rand.Read(out.CapsuleID[:]); err != nil {
return nil, err
}
header, err := b.header(p, round, out.CapsuleID)
if err != nil {
return nil, err
}
out.PublicHeader = header
payloadID, err := age.GenerateX25519Identity()
if err != nil {
return nil, err
}
if out.PayloadIdentity, err = agewrap.RawX25519Identity(payloadID); err != nil {
return nil, err
}
payload, fk, err := Encrypt(plaintext, payloadID.Recipient())
if err != nil {
return nil, err
}
if b.EditPayload != nil {
if payload, err = RewriteAge(payload, fk, func(s []*age.Stanza) []*age.Stanza { return b.EditPayload(fk, s) }); err != nil {
return nil, err
}
}
out.Payload = payload
timeRecipient, err := agewrap.NewTimeRecipient(p, round)
if err != nil {
return nil, err
}
seal := func(control []byte) ([]byte, error) {
plaintext := control
if b.Structure == capsule.TimeAndKey {
if len(access) == 0 {
return nil, errors.New("testkit: time_and_key structure needs AccessRecipients")
}
inner, fk, err := Encrypt(control, access...)
if err != nil {
return nil, err
}
if b.EditInner != nil {
if inner, err = RewriteAge(inner, fk, func(s []*age.Stanza) []*age.Stanza { return b.EditInner(fk, s) }); err != nil {
return nil, err
}
}
plaintext = inner
}
outer, fk, err := Encrypt(plaintext, timeRecipient)
if err != nil {
return nil, err
}
if b.EditOuter != nil {
return RewriteAge(outer, fk, func(s []*age.Stanza) []*age.Stanza { return b.EditOuter(fk, s) })
}
return outer, nil
}
draft, err := capsule.EncodeControl(ctrl, format)
if err != nil {
return nil, err
}
draftSealed, err := seal(draft)
if err != nil {
return nil, err
}
out.Prelude = capsule.Prelude{Format: format, PublicHeaderLen: uint32(len(header)), SealedControlLen: uint32(len(draftSealed))}.Bytes()
ctrl.HeaderBinding = capsule.HeaderBinding(out.Prelude, header)
copy(ctrl.PayloadIdentity[:], out.PayloadIdentity)
if out.Control, err = capsule.EncodeControl(ctrl, format); err != nil {
return nil, err
}
if out.Sealed, err = seal(out.Control); err != nil {
return nil, err
}
if len(out.Sealed) != len(draftSealed) {
return nil, fmt.Errorf("testkit: SEALED_CONTROL length changed from %d to %d", len(draftSealed), len(out.Sealed))
}
out.DKC = Join(out.Prelude[:], out.PublicHeader, out.Sealed, out.Payload)
return out, nil
}
// dummies returns the recipients followed by fresh dummies up to slots,
// agewrap.AccessSlots when slots is 0 (spec §39). The private key of each
// dummy is dropped.
func dummies(recipients []age.Recipient, slots int) ([]age.Recipient, error) {
if slots == 0 {
slots = agewrap.AccessSlots
}
out := slices.Clone(recipients)
for len(out) < slots {
id, err := age.GenerateX25519Identity()
if err != nil {
return nil, err
}
out = append(out, id.Recipient())
}
return out, nil
}
func (b Build) header(p *profile.Profile, round uint64, id [capsule.CapsuleIDSize]byte) ([]byte, error) {
h := &capsule.Header{
CapsuleID: id,
DateKey: datekey.DateKey{ProfileID: p.ID, Round: round},
Policy: b.Declared,
Critical: b.HeaderCritical,
Noncritical: b.HeaderNoncritical,
}
if b.DateKeyString == "" && b.RawPolicy == 0 {
return capsule.EncodeHeader(h)
}
dk, policy := b.DateKeyString, uint64(b.Declared)
if dk == "" {
dk = h.DateKey.Compact()
}
if b.RawPolicy != 0 {
policy = b.RawPolicy
}
return RawHeader(id, dk, policy)
}
// RawHeader encodes a PUBLIC_HEADER with an arbitrary DateKey string, which
// must be valid UTF-8, and policy value, bypassing the validation of
// capsule.EncodeHeader.
func RawHeader(id [capsule.CapsuleIDSize]byte, dk string, policy uint64) ([]byte, error) {
var e codec.Encoder
e.Map(5)
e.Uint(0)
e.Text(capsule.HeaderTypeTag)
e.Uint(1)
e.Uint(capsule.HeaderVersion)
e.Uint(2)
e.Bstr(id[:])
e.Uint(3)
e.Text(dk)
e.Uint(4)
e.Uint(policy)
return e.Out()
}
// Parts is a .dkc split into its four sections.
type Parts struct {
Prelude, Header, Sealed, Payload []byte
}
// Split splits a .dkc using the lengths in its prelude.
func Split(dkc []byte) (Parts, error) {
if len(dkc) < capsule.PreludeSize {
return Parts{}, errors.New("testkit: short capsule")
}
hl := int(binary.BigEndian.Uint32(dkc[8:12]))
sl := int(binary.BigEndian.Uint32(dkc[12:16]))
if len(dkc) < capsule.PreludeSize+hl+sl {
return Parts{}, errors.New("testkit: capsule shorter than its prelude says")
}
h := dkc[capsule.PreludeSize : capsule.PreludeSize+hl]
s := dkc[capsule.PreludeSize+hl : capsule.PreludeSize+hl+sl]
return Parts{Prelude: dkc[:capsule.PreludeSize], Header: h, Sealed: s, Payload: dkc[capsule.PreludeSize+hl+sl:]}, nil
}
// Join concatenates sections into a new slice.
func Join(parts ...[]byte) []byte {
var out []byte
for _, p := range parts {
out = append(out, p...)
}
return out
}
// Reframe joins sections with a prelude whose lengths match them, keeping
// the version, flags and reserved bytes of prelude.
func Reframe(prelude, header, sealed, payload []byte) []byte {
pre := append([]byte(nil), prelude[:capsule.PreludeSize]...)
binary.BigEndian.PutUint32(pre[8:12], uint32(len(header)))
binary.BigEndian.PutUint32(pre[12:16], uint32(len(sealed)))
return Join(pre, header, sealed, payload)
}

Powered by TurnKey Linux.