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.
512 lines
21 KiB
512 lines
21 KiB
package capsule
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"filippo.io/age"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/extension"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
)
|
|
|
|
// OpenOptions configures Open.
|
|
type OpenOptions struct {
|
|
// Registry holds the locally pinned profiles. Required.
|
|
Registry profile.Registry
|
|
// Extensions lists the extensions the application implements; see
|
|
// InspectOptions.Extensions.
|
|
Extensions extension.Registry
|
|
// Source fetches the release. Required. Its answer is always verified
|
|
// locally, at step 10. A source that fetches releases over a network,
|
|
// like provider/drand.Client, verifies each response itself and discards
|
|
// the invalid ones, so that Open reports ErrReleaseUnavailable at step 9
|
|
// when none is valid; a release that a source hands over as the caller
|
|
// supplied it gets the codes of step 10 (spec §63). Any error of Source
|
|
// is reported at step 9 with ErrReleaseUnavailable as its only normative
|
|
// code: the text of an error that carries another code, or none, is
|
|
// kept, not its code.
|
|
Source provider.ReleaseSource
|
|
// Identities are the caller's own X25519 identities, for time_and_key
|
|
// capsules encrypted to known recipients. Nil entries are ignored.
|
|
Identities []age.Identity
|
|
// AccessKey is a portable .dkk already decoded, for time_and_key
|
|
// capsules.
|
|
AccessKey *accesskey.AccessKey
|
|
// AccessKeyFile is a portable .dkk still encoded, the alternative to
|
|
// AccessKey: Open decodes it at step 9.a, and reads it only for a
|
|
// time_and_key capsule, so that its errors, framing included, are
|
|
// reported in the order of spec §63 (§69.1). A caller that decodes a .dkk
|
|
// itself reports its decoding errors first, whatever the capsule. Open
|
|
// wipes the key it decodes. At most one of AccessKey and AccessKeyFile
|
|
// may be set.
|
|
AccessKeyFile io.Reader
|
|
// Now is the clock. Required: no package of this module reads the wall
|
|
// clock on its own. Open does not ask Source for a round whose time has
|
|
// not been reached.
|
|
Now func() time.Time
|
|
// Sink receives the files of a format 3 capsule; dst receives the
|
|
// content of formats 1 and 2. Open fails right after step 2 with
|
|
// ErrSinkRequired when a format 3 capsule has no Sink.
|
|
Sink Sink
|
|
}
|
|
|
|
// Opened describes a capsule that Open decrypted completely.
|
|
type Opened struct {
|
|
Inspection *Inspection
|
|
Release provider.Release
|
|
// Format is the format of the capsule (spec §22). A caller should show
|
|
// it: format 1 does not hide the number of credentials or the exact
|
|
// length of the content (spec §55.2, §70).
|
|
Format Format
|
|
// PayloadLength is L, the number of bytes of content written to dst. In
|
|
// format 2, Padding is the padding rule sealed in the control and
|
|
// PaddedLength is P = rule(L), the length of the plaintext of
|
|
// PAYLOAD_AGE, whose padding is checked and never written (spec §29.1);
|
|
// in format 1 both are zero.
|
|
PayloadLength uint64
|
|
Padding Padding
|
|
PaddedLength uint64
|
|
// In format 3, Head is the head, as step 17.4 validated it, Verdicts
|
|
// are the verdicts of the security area, which a caller shows before the
|
|
// declared author and the comment (spec §29.7), and AreaLen is the size
|
|
// of that area. PayloadLength is then the length of BODY.
|
|
Head *Head
|
|
Verdicts Verdicts
|
|
AreaLen uint32
|
|
// UnusableHeadExtensions are the known noncritical extensions of the
|
|
// head that OpenOptions.Extensions rejects, as UnusableControlExtensions
|
|
// are for the control (spec §29.4, §54).
|
|
UnusableHeadExtensions []extension.Unusable
|
|
// ControlCritical and ControlNoncritical are the extensions of the sealed
|
|
// CONTROL_CBOR, only visible after opening.
|
|
ControlCritical []extension.Extension
|
|
ControlNoncritical []extension.Extension
|
|
// UnusableControlExtensions and UnusableAccessKeyExtensions are the known
|
|
// noncritical extensions of CONTROL_CBOR and of the .dkk whose data
|
|
// OpenOptions.Extensions rejects. They do not fail Open; the application
|
|
// must not use them (spec §54). The PUBLIC_HEADER ones are in Inspection.
|
|
UnusableControlExtensions []extension.Unusable
|
|
UnusableAccessKeyExtensions []extension.Unusable
|
|
}
|
|
|
|
// Open runs the complete decryption flow of spec §63 and streams the
|
|
// plaintext to dst.
|
|
//
|
|
// Everything verifiable locally is checked before a release is requested or a
|
|
// secret is used (steps 1 to 8, the presence and capsule binding of access
|
|
// credentials, and the .dkk capsule_digest when r is seekable). The stanza
|
|
// rules are enforced again, as a MUST, by the identities that open each age
|
|
// file (steps 11, 13 and 17).
|
|
//
|
|
// Open returns nil only after age has authenticated the whole payload and,
|
|
// in format 2, after the length and the padding of its plaintext have been
|
|
// checked against L and the padding rule of the control (steps 17 and 18). In
|
|
// format 2 only the first L bytes, the content, are written to dst; the
|
|
// padding never is. On error, dst may hold a partial plaintext that MUST be
|
|
// discarded, and that must not be presented as valid: write to a temporary
|
|
// file and publish it only on success (spec §56), as the datekeys CLI does.
|
|
func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*Opened, error) {
|
|
if opts.Source == nil {
|
|
return nil, errors.New("capsule: OpenOptions.Source is required")
|
|
}
|
|
if opts.Now == nil {
|
|
return nil, errors.New("capsule: OpenOptions.Now is required")
|
|
}
|
|
if opts.AccessKey != nil && opts.AccessKeyFile != nil {
|
|
return nil, errors.New("capsule: set OpenOptions.AccessKey or AccessKeyFile, not both")
|
|
}
|
|
start, seekable := int64(0), false
|
|
if s, ok := r.(io.Seeker); ok {
|
|
if pos, err := s.Seek(0, io.SeekCurrent); err == nil {
|
|
start, seekable = pos, true
|
|
}
|
|
}
|
|
|
|
// Steps 1 to 8. Right after step 2, the format decides where the content
|
|
// goes: a format 3 capsule needs a Sink, and the others dst.
|
|
in, st, err := inspect(r, InspectOptions{Registry: opts.Registry, Extensions: opts.Extensions}, func(p Prelude) error {
|
|
switch {
|
|
case p.Format == Format3 && opts.Sink == nil:
|
|
return ErrSinkRequired
|
|
case p.Format != Format3 && dst == nil:
|
|
return errWriterRequired
|
|
}
|
|
return nil
|
|
})
|
|
out := &Opened{Inspection: in, Format: in.Prelude.Format}
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
h, p, format := in.Header, in.Profile, in.Prelude.Format
|
|
|
|
// Access credentials are checked before any network request (spec §63
|
|
// step 9), and only for time_and_key: under time_only they play no part.
|
|
// The .dkk as an object first, decoded here when it is still encoded,
|
|
// then its bindings to this capsule, the capsule_id and, when the reader
|
|
// is seekable, the capsule_digest (spec §69.1). Then at least one
|
|
// credential (9.b); nil identities are not credentials.
|
|
var ids []age.Identity
|
|
if h.Policy == TimeAndKey {
|
|
for _, id := range opts.Identities {
|
|
if id != nil {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
k := opts.AccessKey
|
|
if opts.AccessKeyFile != nil {
|
|
if k, err = accesskey.Decode(opts.AccessKeyFile); err != nil {
|
|
return out, in.fail(9, "access credential", err)
|
|
}
|
|
defer k.Wipe()
|
|
}
|
|
if k != nil {
|
|
id, err := checkAccessKey(k, h, opts.Extensions)
|
|
if err != nil {
|
|
return out, in.fail(9, "access credential", err)
|
|
}
|
|
out.UnusableAccessKeyExtensions = extension.CheckNoncriticalIn(extension.AccessKey, k.Noncritical, opts.Extensions)
|
|
if k.Verification != nil && seekable {
|
|
payload, err := checkCapsuleDigest(r.(io.ReadSeeker), start, in.PayloadOffset, k.Verification.CapsuleDigest)
|
|
if err != nil {
|
|
return out, in.fail(9, "access credential", err)
|
|
}
|
|
st.payload = payload
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
if len(ids) == 0 {
|
|
return out, in.fail(9, "access credential", fmt.Errorf("capsule: time_and_key capsule and no identity or .dkk supplied: %w", datekeys.ErrAccessRequired))
|
|
}
|
|
in.pass(9, "access credential", fmt.Sprintf("%d identities to try%s", len(ids), unusable(out.UnusableAccessKeyExtensions)))
|
|
}
|
|
|
|
// Step 9: obtain the release, never before its round time. A network
|
|
// source has verified each response with the rules of step 10 and
|
|
// discarded the invalid ones. Whatever the failure of the source, none
|
|
// valid included, it is ErrReleaseUnavailable here.
|
|
cond := provider.Condition{Round: h.DateKey.Round}
|
|
if now := opts.Now(); now.Before(in.UnlockAt) {
|
|
err := fmt.Errorf("capsule: round %d is published at %s, it is %s: %w", cond.Round,
|
|
in.UnlockAt.Format(time.RFC3339), now.UTC().Format(time.RFC3339), datekeys.ErrReleaseUnavailable)
|
|
return out, in.fail(9, "release", err)
|
|
}
|
|
release, err := opts.Source.Fetch(ctx, p, cond)
|
|
if err != nil {
|
|
return out, in.fail(9, "release", sourceFailure(err))
|
|
}
|
|
in.pass(9, "release", fmt.Sprintf("round %d obtained", release.Round))
|
|
|
|
// Step 10: verify the release locally.
|
|
if err := provider.Verify(p, cond, release); err != nil {
|
|
return out, in.fail(10, "release verification", err)
|
|
}
|
|
out.Release = release
|
|
in.pass(10, "release verification", "BLS signature valid under the pinned key")
|
|
|
|
// Step 11: open OUTER_TIME_AGE with the strict tlock identity.
|
|
timeID, err := agewrap.NewTimeIdentity(p, cond.Round, release)
|
|
if err != nil {
|
|
return out, in.fail(11, "open sealed control", err)
|
|
}
|
|
inner, err := decryptAll(st.sealed, timeID)
|
|
if err != nil {
|
|
return out, in.fail(11, "open sealed control", err)
|
|
}
|
|
defer clear(inner)
|
|
in.pass(11, "open sealed control", "one tlock stanza, header MAC valid")
|
|
|
|
// Step 12: the structure must match access_policy (spec §36), and in
|
|
// format 2 INNER_ACCESS_AGE holds exactly 16 stanzas (spec §39).
|
|
var controlBytes []byte
|
|
switch h.Policy {
|
|
case TimeOnly:
|
|
if looksLikeAge(inner) {
|
|
return out, in.fail(12, "policy structure", fmt.Errorf("capsule: time_only capsule seals an age file: %w", datekeys.ErrPolicyStructureMismatch))
|
|
}
|
|
controlBytes = inner
|
|
in.pass(12, "policy structure", "time_only: CONTROL_CBOR sealed directly")
|
|
case TimeAndKey:
|
|
stanzas, err := agewrap.Stanzas(bytes.NewReader(inner))
|
|
if err != nil {
|
|
return out, in.fail(12, "policy structure", fmt.Errorf("capsule: time_and_key capsule does not seal an age file: %w", datekeys.ErrPolicyStructureMismatch))
|
|
}
|
|
slots := accessSlots(format)
|
|
if err := agewrap.CheckAccessStanzas(stanzas, slots); err != nil {
|
|
return out, in.fail(12, "policy structure", err)
|
|
}
|
|
in.pass(12, "policy structure", fmt.Sprintf("time_and_key: INNER_ACCESS_AGE with %d X25519 stanzas", len(stanzas)))
|
|
|
|
// Step 13: open INNER_ACCESS_AGE with the caller's identities.
|
|
accessID, err := agewrap.NewAccessIdentity(slots, ids...)
|
|
if err != nil {
|
|
return out, in.fail(13, "open access layer", err)
|
|
}
|
|
if controlBytes, err = decryptAll(inner, accessID); err != nil {
|
|
return out, in.fail(13, "open access layer", err)
|
|
}
|
|
defer clear(controlBytes)
|
|
in.pass(13, "open access layer", "identity matched exactly one stanza")
|
|
}
|
|
|
|
// Step 14: parse the canonical CONTROL_CBOR, whose schema version is the
|
|
// format of the capsule.
|
|
control, err := DecodeControl(controlBytes, format)
|
|
if err != nil {
|
|
return out, in.fail(14, "control", err)
|
|
}
|
|
defer clear(control.PayloadIdentity[:])
|
|
if err := extension.CheckCriticalIn(extension.Control, control.Critical, opts.Extensions); err != nil {
|
|
return out, in.fail(14, "control", fmt.Errorf("capsule: CONTROL_CBOR: %w", err))
|
|
}
|
|
out.ControlCritical, out.ControlNoncritical = control.Critical, control.Noncritical
|
|
out.UnusableControlExtensions = extension.CheckNoncriticalIn(extension.Control, control.Noncritical, opts.Extensions)
|
|
in.pass(14, "control", "canonical CONTROL_CBOR"+unusable(out.UnusableControlExtensions))
|
|
|
|
// Step 15: verify header_binding over the exact stored bytes.
|
|
binding := HeaderBinding(st.prelude, in.PublicHeader)
|
|
if !hmac.Equal(binding[:], control.HeaderBinding[:]) {
|
|
return out, in.fail(15, "header binding", fmt.Errorf("capsule: header_binding does not match PRELUDE || PUBLIC_HEADER: %w", datekeys.ErrHeaderBinding))
|
|
}
|
|
in.pass(15, "header binding", "matches")
|
|
|
|
// Step 16: recover I_PAYLOAD and, in format 2, L and the padding rule,
|
|
// which step 14 validated, so that P = rule(L) cannot fail. L and P are
|
|
// not frame lengths: nothing is allocated according to them (spec §57).
|
|
payloadID, err := agewrap.NewPayloadIdentity(control.PayloadIdentity[:])
|
|
if err != nil {
|
|
return out, in.fail(16, "payload identity", err)
|
|
}
|
|
detail := "I_PAYLOAD recovered"
|
|
if format.padded() {
|
|
if out.PaddedLength, err = PaddedLength(control.PayloadLength, control.Padding); err != nil {
|
|
return out, in.fail(16, "payload identity", err)
|
|
}
|
|
out.Padding = control.Padding
|
|
detail = fmt.Sprintf("I_PAYLOAD recovered; L = %d, padding %s, P = %d", control.PayloadLength, control.Padding, out.PaddedLength)
|
|
}
|
|
in.pass(16, "payload identity", detail)
|
|
|
|
// Step 17: open PAYLOAD_AGE with I_PAYLOAD. In format 2 the plaintext is
|
|
// exactly P bytes and its bytes L to P-1 are zero; only the first L are
|
|
// written, and the padding is read and checked, never written.
|
|
pr, err := age.Decrypt(st.payload, payloadID)
|
|
if err != nil {
|
|
return out, in.fail(17, "open payload", classify("PAYLOAD_AGE", ageHeaderFailure, err))
|
|
}
|
|
if format == Format3 {
|
|
out.PayloadLength = control.PayloadLength
|
|
if err := openBody(pr, control.PayloadLength, out.PaddedLength, opts.Sink, opts.Extensions, out); err != nil {
|
|
return out, in.fail(17, "open payload", err)
|
|
}
|
|
in.pass(17, "open payload", fmt.Sprintf("payload authenticated; BODY of %d bytes, %d files, area of %d bytes", control.PayloadLength, len(out.Head.Files), out.AreaLen))
|
|
in.pass(18, "commit", fmt.Sprintf("%d files", len(out.Head.Files)))
|
|
return out, nil
|
|
}
|
|
w := &plaintextWriter{w: dst}
|
|
var n int64
|
|
if format == Format2 {
|
|
n, err = io.CopyN(w, pr, int64(control.PayloadLength))
|
|
if err == io.EOF {
|
|
err = fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, shorter than P = %d: %w", n, out.PaddedLength, datekeys.ErrIntegrity)
|
|
} else if err == nil {
|
|
err = checkPadding(pr, uint64(n), out.PaddedLength)
|
|
}
|
|
} else {
|
|
n, err = io.Copy(w, pr)
|
|
}
|
|
out.PayloadLength = uint64(n)
|
|
if err != nil {
|
|
switch {
|
|
case w.err != nil:
|
|
// dst failed, not age: the caller's own error keeps its text.
|
|
err = fmt.Errorf("capsule: PAYLOAD_AGE: writing the plaintext: %w: %w", w.err, datekeys.ErrIntegrity)
|
|
case datekeys.Code(err) == "":
|
|
err = classify("PAYLOAD_AGE", ageStreamFailure, err)
|
|
}
|
|
return out, in.fail(17, "open payload", err)
|
|
}
|
|
if format == Format2 {
|
|
in.pass(17, "open payload", fmt.Sprintf("payload authenticated; %d bytes of plaintext, zero after byte %d", out.PaddedLength, n))
|
|
} else {
|
|
in.pass(17, "open payload", "payload authenticated")
|
|
}
|
|
|
|
// Step 18: every check of step 17 passed.
|
|
in.pass(18, "commit", fmt.Sprintf("%d bytes of content", n))
|
|
return out, nil
|
|
}
|
|
|
|
// accessSlots is the number of stanzas INNER_ACCESS_AGE must hold in a
|
|
// capsule of format f, 0 meaning one or more (spec §33, §39).
|
|
func accessSlots(f Format) int {
|
|
if f.padded() {
|
|
return agewrap.AccessSlots
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// checkPadding reads the rest of the plaintext of a format 2 PAYLOAD_AGE,
|
|
// after its first l bytes, and checks that it is the padding: exactly p - l
|
|
// zero bytes (spec §29.1, §63 step 17). It fails as soon as the plaintext
|
|
// goes past P or a byte is not zero; a plaintext shorter than P is found at
|
|
// the end of the STREAM. Every failure is ErrIntegrity, whatever the moment
|
|
// it is found (spec §69.1).
|
|
func checkPadding(r io.Reader, l, p uint64) error {
|
|
buf := make([]byte, 16<<10)
|
|
for at := l; ; {
|
|
n, err := r.Read(buf)
|
|
for i, b := range buf[:n] {
|
|
switch {
|
|
case at+uint64(i) >= p:
|
|
return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is longer than P = %d: %w", p, datekeys.ErrIntegrity)
|
|
case b != 0:
|
|
return fmt.Errorf("capsule: PAYLOAD_AGE: byte %d of the plaintext is padding and is not zero: %w", at+uint64(i), datekeys.ErrIntegrity)
|
|
}
|
|
}
|
|
at += uint64(n)
|
|
switch {
|
|
case err == io.EOF && at < p:
|
|
return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, shorter than P = %d: %w", at, p, datekeys.ErrIntegrity)
|
|
case err == io.EOF:
|
|
return nil
|
|
case err != nil:
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// sourceFailure is the error of a release source as step 9 reports it:
|
|
// ErrReleaseUnavailable, the only code of spec §63 step 9 for a source that
|
|
// delivers no release. An error that wraps it and no other normative error
|
|
// is kept as it is, with any other cause it wraps, such as a context error.
|
|
// Any other error, whether it carries another normative code or none, is
|
|
// kept as text only, so that the result wraps exactly one code.
|
|
func sourceFailure(err error) error {
|
|
if onlyCode(err, datekeys.ErrReleaseUnavailable) {
|
|
return err
|
|
}
|
|
return fmt.Errorf("capsule: release source: %v: %w", err, datekeys.ErrReleaseUnavailable)
|
|
}
|
|
|
|
// onlyCode reports whether code is the one normative error that err wraps.
|
|
func onlyCode(err error, code *datekeys.Error) bool {
|
|
for _, e := range datekeys.All() {
|
|
if errors.Is(err, e) != (e == code) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkAccessKey validates a .dkk before it is used and returns its
|
|
// identity. The .dkk is checked as an object first, its semantic fields in
|
|
// ascending key order (spec §69.1, layer 4): access_type and access_material
|
|
// (keys 4 and 5), then its critical extensions as in step 4 (key 7). Only
|
|
// then its binding to this capsule: the capsule_id (key 3). The
|
|
// capsule_digest (key 6), a binding too, is checked last, by the caller.
|
|
func checkAccessKey(k *accesskey.AccessKey, h *Header, reg extension.Registry) (age.Identity, error) {
|
|
id, err := k.Identity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := extension.CheckCriticalIn(extension.AccessKey, k.Critical, reg); err != nil {
|
|
return nil, fmt.Errorf("capsule: .dkk: %w", err)
|
|
}
|
|
if k.CapsuleID != h.CapsuleID {
|
|
return nil, fmt.Errorf("capsule: the .dkk is for capsule %x, this is %x: %w", k.CapsuleID, h.CapsuleID, datekeys.ErrAccessInvalid)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// checkCapsuleDigest compares the .dkk capsule_digest with SHA-256 of the
|
|
// .dkc (spec §43), then repositions r at the payload. The digest is a fast
|
|
// failure for a wrong file, not a security property.
|
|
func checkCapsuleDigest(r io.ReadSeeker, start, payloadOffset int64, want []byte) (io.Reader, error) {
|
|
if _, err := r.Seek(start, io.SeekStart); err != nil {
|
|
return nil, fmt.Errorf("capsule: %w", err)
|
|
}
|
|
sum := sha256.New()
|
|
if _, err := io.Copy(sum, r); err != nil {
|
|
return nil, fmt.Errorf("capsule: %w", err)
|
|
}
|
|
if !hmac.Equal(sum.Sum(nil), want) {
|
|
return nil, fmt.Errorf("capsule: the .dkk capsule_digest does not match this .dkc: %w", datekeys.ErrAccessInvalid)
|
|
}
|
|
if _, err := r.Seek(start+payloadOffset, io.SeekStart); err != nil {
|
|
return nil, fmt.Errorf("capsule: %w", err)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// decryptAll opens a bounded, in-memory age file. The plaintext is never
|
|
// longer than the ciphertext, so it is read into one buffer of that size:
|
|
// the plaintext may hold I_PAYLOAD, and no outgrown buffer is left behind.
|
|
// The caller wipes the result; on error it is wiped here.
|
|
func decryptAll(ciphertext []byte, id age.Identity) ([]byte, error) {
|
|
r, err := age.Decrypt(bytes.NewReader(ciphertext), id)
|
|
if err != nil {
|
|
return nil, classify("age", ageHeaderFailure, err)
|
|
}
|
|
// Not io.ReadFull: it would turn the age reader's io.ErrUnexpectedEOF,
|
|
// a truncated STREAM, into the end of a short read.
|
|
out := make([]byte, len(ciphertext))
|
|
for n := 0; ; {
|
|
m, err := r.Read(out[n:])
|
|
n += m
|
|
switch {
|
|
case err == io.EOF:
|
|
clear(out[n:]) // Read may use the whole of its argument as scratch space.
|
|
return out[:n], nil
|
|
case err != nil:
|
|
clear(out)
|
|
return nil, classify("age", ageStreamFailure, err)
|
|
case n == len(out):
|
|
return out, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// The failures of age that no identity reports, by the phase in which they
|
|
// happen: age.Decrypt, which parses the header and checks its MAC once an
|
|
// identity has unwrapped the file key, and then reading the STREAM.
|
|
const (
|
|
ageHeaderFailure = "the age header is malformed or truncated, or its MAC does not verify"
|
|
ageStreamFailure = "the age payload is truncated, has trailing data or fails STREAM authentication"
|
|
)
|
|
|
|
// classify keeps the normative error an identity returned from Unwrap, and
|
|
// maps every other failure of age to ErrIntegrity with the fixed reason of
|
|
// its phase. The text of age's errors is not copied, as in package agewrap.
|
|
func classify(what, reason string, err error) error {
|
|
if datekeys.Code(err) != "" {
|
|
return fmt.Errorf("capsule: %s: %w", what, err)
|
|
}
|
|
return fmt.Errorf("capsule: %s: %s: %w", what, reason, datekeys.ErrIntegrity)
|
|
}
|
|
|
|
// plaintextWriter records the first error of the writer of the plaintext, so
|
|
// that a failure of the caller's writer is not reported as one of age.
|
|
type plaintextWriter struct {
|
|
w io.Writer
|
|
err error
|
|
}
|
|
|
|
func (p *plaintextWriter) Write(b []byte) (int, error) {
|
|
n, err := p.w.Write(b)
|
|
if err != nil && p.err == nil {
|
|
p.err = err
|
|
}
|
|
return n, err
|
|
}
|