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/capsule/open3.go

284 lines
9.3 KiB

package capsule
import (
"crypto/hmac"
"crypto/sha256"
"errors"
"fmt"
"io"
"time"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/extension"
)
// Sink receives the files of a format 3 capsule (spec §56, §63 steps 17 and
// 18). Open calls Begin once the head is validated, then Create for each
// file in the order of the head, and then Commit, only after every check of
// step 17 has passed. After any failure that follows a successful Begin, a
// failure of Commit included, it calls Abort, once; a Begin that fails
// cleans up after itself. Nothing a Sink receives before Commit may be
// presented as valid: write to a temporary place and publish it in Commit.
type Sink interface {
// Begin receives the validated head. Its files are h.Files.
Begin(h *Head) error
// Create returns the writer of file i of the head. Open closes it after
// writing its Size bytes, before checking its SHA-256. Size, Start and
// End are declared, not received: a Sink must not reserve memory or disk
// by them (spec §57).
Create(i int) (io.WriteCloser, error)
// Commit publishes the files: step 18.
Commit() error
// Abort discards everything the Sink received.
Abort()
}
// ErrSinkRequired is the error of Open for a format 3 capsule without
// OpenOptions.Sink: its content is several files, which one io.Writer cannot
// receive. It is an error of the caller, with no normative code: Open returns
// it right after step 2, before any request, and never reports the capsule
// as ErrUnsupportedVersion, because it is valid (spec §70).
var ErrSinkRequired = errors.New("capsule: a format 3 capsule holds files: OpenOptions.Sink is required")
// errWriterRequired is the error of Open for a capsule of format 1 or 2 with
// a nil dst.
var errWriterRequired = errors.New("capsule: a capsule of format 1 or 2 holds one content: dst is required")
// plainReader reads the plaintext of PAYLOAD_AGE and counts its bytes.
type plainReader struct {
r io.Reader
n uint64
}
func (p *plainReader) Read(b []byte) (int, error) {
n, err := p.r.Read(b)
p.n += uint64(n)
return n, err
}
// plaintextFailure is the error of a read of the plaintext that stopped
// before it wanted: a failure of age, or a plaintext that ends before P.
func plaintextFailure(err error, n, p uint64) error {
if err == io.EOF {
return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, shorter than P = %d: %w", n, p, datekeys.ErrIntegrity)
}
return classify("PAYLOAD_AGE", ageStreamFailure, err)
}
// readN reads exactly n bytes of the plaintext. Its memory grows with the
// bytes received, not with n, a length that BODY declares (spec §57). Not
// io.ReadFull: it would turn the io.ErrUnexpectedEOF of a truncated STREAM
// into a short read.
func readN(r *plainReader, n int, p uint64) ([]byte, error) {
var buf []byte
part := make([]byte, min(n, 32<<10))
for len(buf) < n {
m, err := r.Read(part[:min(n-len(buf), len(part))])
buf = append(buf, part[:m]...)
if len(buf) == n {
break
}
if err != nil {
return nil, plaintextFailure(err, r.n, p)
}
}
return buf, nil
}
// drain reads the rest of the plaintext to EOF. A failure of age, or a
// plaintext whose length is not P, prevails over the failure of any substep
// of step 17 (spec §63).
func drain(r *plainReader, p uint64) error {
buf := make([]byte, 32<<10)
for {
_, err := r.Read(buf)
switch {
case err == io.EOF && r.n != p:
return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, not P = %d: %w", r.n, p, datekeys.ErrIntegrity)
case err == io.EOF:
return nil
case err != nil:
return classify("PAYLOAD_AGE", ageStreamFailure, err)
}
}
}
// copyFile writes the next size bytes of the plaintext to w, hashing them.
func copyFile(w io.Writer, r *plainReader, size, p uint64) ([]byte, error) {
sum := sha256.New()
buf := make([]byte, 32<<10)
for left := size; left > 0; {
m := uint64(len(buf))
if m > left {
m = left
}
n, err := r.Read(buf[:m])
if n > 0 {
sum.Write(buf[:n])
if _, werr := w.Write(buf[:n]); werr != nil {
return nil, &sinkError{werr}
}
left -= uint64(n)
}
if left > 0 && err != nil {
return nil, plaintextFailure(err, r.n, p)
}
}
return sum.Sum(nil), nil
}
// refused is the error of OpenOptions.Accept: every check of step 17 passed,
// and the caller refused to publish the files.
type refused struct{ err error }
func (e *refused) Error() string { return e.err.Error() }
func (e *refused) Unwrap() error { return e.err }
// sinkError is a failure of the caller's Sink, not of the capsule.
type sinkError struct{ err error }
func (e *sinkError) Error() string { return e.err.Error() }
func (e *sinkError) Unwrap() error { return e.err }
// sinkFailure reports a failure of the Sink as the caller's own error, with
// ErrIntegrity as its code, as a failure of dst is in formats 1 and 2.
func sinkFailure(what string, err error) error {
return fmt.Errorf("capsule: PAYLOAD_AGE: %s: %w: %w", what, err, datekeys.ErrIntegrity)
}
// newSecurityContext is the context of the verdicts of a capsule of format f
// whose control is c, opened at the round time unlock (spec v0.11, §29.7).
// The head digest is added when the head is read. A control that cannot be
// encoded leaves control_commit at zero: no signature verifies then, and F2
// is the verdict, though DecodeControl has already decoded it.
func newSecurityContext(c *Control, f Format, unlock time.Time, keys map[string]string) *SecurityContext {
sc := &SecurityContext{RoundTime: unlock, AuthorKeys: keys}
if cc, err := ControlCommit(c, f); err == nil {
sc.ControlCommit = cc
}
return sc
}
// openBody runs step 17 of a format 3 capsule and step 18 (spec §63): pr is
// the plaintext of PAYLOAD_AGE, whose BODY is l bytes long and whose
// padding goes up to p. The substeps, in order:
//
// 17.2 the frame of BODY and the area (§29.2), ErrIntegrity;
// 17.3 security, layers 2 and 3 (§29.3), without a code;
// 17.4 the head, layers 2 to 4 (§29.4 to §29.6);
// 17.5 the files fill CONTENT, ErrIntegrity;
// 17.6 the verdicts of security (§29.7), without a code;
// 17.7 the SHA-256 of each file, ErrIntegrity;
// 17.8 the padding, ErrIntegrity.
//
// A failure of age after its header, or a plaintext whose length is not P,
// is ErrIntegrity and prevails; otherwise the first substep that fails
// decides. openBody stops early only with ErrIntegrity when that is already
// the final code: on a failure of age, or of a substep of ErrIntegrity with
// no earlier failure of another code. After a failure of another code, at
// 17.4, it reads PAYLOAD_AGE to EOF before reporting it.
func openBody(pr io.Reader, l, p uint64, sink Sink, reg extension.Registry, sc *SecurityContext, accept func(Verdicts) error, out *Opened) (err error) {
r := &plainReader{r: pr}
begun := false
defer func() {
if err != nil && begun {
sink.Abort()
}
}()
// 17.2: the frame of BODY and the area.
b, err := readN(r, BodyFrameSize, p)
if err != nil {
return err
}
frame, err := ParseBodyFrame(b, l)
if err != nil {
return err
}
area, err := readN(r, int(frame.AreaLen), p)
if err != nil {
return err
}
if err := CheckArea(area, frame.SecurityLen); err != nil {
return err
}
out.AreaLen = frame.AreaLen
// 17.3 and 17.6: security never fails; its verdicts are shown after
// step 18 only.
security := area[:frame.SecurityLen]
// 17.4: the head. Its codes other than ErrIntegrity are reported only
// after reading to EOF.
hb, err := readN(r, int(frame.HeadLen), p)
if err != nil {
return err
}
// The verdicts need the head digest, so they are evaluated once the head
// bytes are read; they never fail, and no decoding of the head changes them.
sc.HeadDigest = HeadDigest(hb)
verdicts := EvaluateSecurityIn(security, sc)
h, err := DecodeHead(hb, reg)
if err != nil {
if derr := drain(r, p); derr != nil {
return derr
}
return err
}
// 17.5: the files fill CONTENT.
if err := CheckHeadEnd(h, frame.ContentLength(l)); err != nil {
return err
}
// 17.7: each file, to the Sink, with its SHA-256.
if err := sink.Begin(h); err != nil {
return sinkFailure("beginning the files", err)
}
begun = true
for i := range h.Files {
f := &h.Files[i]
w, err := sink.Create(i)
if err != nil {
return sinkFailure(fmt.Sprintf("creating file %d", i+1), err)
}
sum, err := copyFile(w, r, f.Size, p)
if cerr := w.Close(); err == nil && cerr != nil {
err = &sinkError{cerr}
}
var se *sinkError
switch {
case errors.As(err, &se):
return sinkFailure(fmt.Sprintf("writing file %d", i+1), se.err)
case err != nil:
return err
case !hmac.Equal(sum, f.SHA256[:]):
return fmt.Errorf("capsule: PAYLOAD_AGE: file %d: its SHA-256 is not the one of the head: %w", i+1, datekeys.ErrIntegrity)
}
}
// 17.8: the padding, up to P, and nothing after it.
if err := checkPadding(r, r.n, p); err != nil {
if datekeys.Code(err) == "" {
err = classify("PAYLOAD_AGE", ageStreamFailure, err)
}
return err
}
// The caller sees the verdicts before anything is published
// (OpenOptions.Accept).
if accept != nil {
if err := accept(verdicts); err != nil {
return &refused{err}
}
}
// Step 18.
if err := sink.Commit(); err != nil {
return sinkFailure("committing the files", err)
}
out.Head, out.Verdicts = h, verdicts
out.UnusableHeadExtensions = extension.CheckNoncriticalIn(extension.Head, h.Noncritical, reg)
return nil
}

Powered by TurnKey Linux.