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.
284 lines
9.3 KiB
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
|
|
}
|