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 }