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.
294 lines
11 KiB
294 lines
11 KiB
package capsule
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"g.activething.com/go/DateKeys/internal/pathrule"
|
|
)
|
|
|
|
// Source is a file that EncryptFiles writes into a format 3 capsule.
|
|
type Source struct {
|
|
// Path is the path of the file in the capsule, relative, with '/'
|
|
// between its segments (spec §29.5). It is stored as given: EncryptFiles
|
|
// rejects a path that breaks a rule, with a message that names the rule
|
|
// and the character, and never corrects it (spec §62.1 rule 15).
|
|
Path string
|
|
// Size is the number of bytes of the file. EncryptFiles checks it in
|
|
// each of its two readings.
|
|
Size int64
|
|
// ModTime is the modification time of the file at its source, taken
|
|
// when it is loaded, as os.FileInfo.ModTime gives it, or the zero Time
|
|
// when unknown. It is stored in seconds when it falls from 1970-01-01 to
|
|
// 9999-12-31T23:59:59Z, and omitted otherwise, never clipped (spec
|
|
// §62.1 rule 16). It is informative: it proves nothing.
|
|
ModTime time.Time
|
|
// Open returns a reader of the file from its start. EncryptFiles calls
|
|
// it twice, and closes each reader.
|
|
Open func() (io.ReadCloser, error)
|
|
}
|
|
|
|
// EncryptFiles writes a format 3 .dkc holding the files of sources and the
|
|
// comment and declared author of opts (spec §29.2 to §29.6, §61, §62,
|
|
// §62.1). It needs no network: the round is resolved locally and tlock uses
|
|
// only the pinned public key.
|
|
//
|
|
// It reads each file twice, and writes nothing to dst before the second
|
|
// reading. First it checks the paths and the texts with the rules of the
|
|
// reader, measures L with a head whose salt and SHA-256 are zero, as long as
|
|
// the final one, and hashes each file. Then it seals the control, with L,
|
|
// and streams PAYLOAD_AGE, reading each file again: a file whose size or
|
|
// SHA-256 has changed makes it fail (spec §62.1 rule 18), and dst then holds
|
|
// a partial capsule that must be discarded and never presented as a capsule
|
|
// (rule 9). The files go in the byte order of their paths, whatever the
|
|
// order of sources (R8), without the empty folders, which a path cannot
|
|
// name.
|
|
//
|
|
// The head, the control and the security area are decoded with the rules of
|
|
// the reader before anything is written (spec §62.1 rule 17), and the
|
|
// self-checks of Encrypt apply too. The security area is the empty one of
|
|
// this version, in an area of 512 bytes, whatever the options (rule 13).
|
|
//
|
|
// opts is as for Encrypt, with the head in Comment, Author and the head
|
|
// extensions, and with Length 0: L is the length of BODY.
|
|
func EncryptFiles(dst io.Writer, sources []Source, opts EncryptOptions) (*Result, error) {
|
|
if opts.Length != 0 {
|
|
return nil, errors.New("capsule: EncryptOptions.Length is for Encrypt: EncryptFiles computes L from the files")
|
|
}
|
|
s, err := newSealer(opts, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
h, order, err := newHead(sources, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Step 2 of spec §61: L, with a head as long as the final one.
|
|
measured, err := EncodeHead(h)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(measured) > MaxHeadLen {
|
|
return nil, fmt.Errorf("capsule: the head is %d bytes, more than %d: fewer files or shorter paths", len(measured), MaxHeadLen)
|
|
}
|
|
if err := selfCheckHead(measured); err != nil {
|
|
return nil, err
|
|
}
|
|
var content uint64
|
|
if n := len(h.Files); n > 0 {
|
|
content = h.Files[n-1].End
|
|
}
|
|
// newHead bounds content by MaxPayloadLength: the sum does not overflow.
|
|
length := BodyFrameSize + AreaLen + uint64(len(measured)) + content
|
|
if _, err := PaddedLength(length, s.code); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Step 3: the first reading, for the SHA-256 of each file.
|
|
for i := range h.Files {
|
|
if h.Files[i].SHA256, err = readSource(nil, sources[order[i]], h.Files[i].Size, false); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Step 12: the head, with a fresh salt, and SECURITY_CBOR, decoded with
|
|
// the rules of the reader; write decodes CONTROL_CBOR.
|
|
_, _ = rand.Read(h.Salt[:]) // never fails since Go 1.24
|
|
head, err := EncodeHead(h)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(head) != len(measured) {
|
|
return nil, fmt.Errorf("capsule: internal error: the head is %d bytes, measured %d", len(head), len(measured))
|
|
}
|
|
if err := selfCheckHead(head); err != nil {
|
|
return nil, err
|
|
}
|
|
security := EncodeSecurity()
|
|
if v := EvaluateSecurity(security); v != (Verdicts{Signature: VerdictNoSignature, Seal: VerdictNoSeal}) {
|
|
return nil, fmt.Errorf("capsule: self-check: the reader finds the verdicts %s and %s in this security area", v.Signature, v.Seal)
|
|
}
|
|
frame := BodyFrame{AreaLen: AreaLen, SecurityLen: uint32(len(security)), HeadLen: uint32(len(head))}
|
|
fb := frame.Bytes()
|
|
if _, err := ParseBodyFrame(fb[:], length); err != nil {
|
|
return nil, fmt.Errorf("capsule: self-check: %w", err)
|
|
}
|
|
if err := CheckHeadEnd(h, frame.ContentLength(length)); err != nil {
|
|
return nil, fmt.Errorf("capsule: self-check: %w", err)
|
|
}
|
|
|
|
// Step 16: BODY, and the second reading of each file.
|
|
res, err := s.write(dst, Format3, length, func(w io.Writer) error {
|
|
for _, b := range [][]byte{fb[:], security, make([]byte, AreaLen-len(security)), head} {
|
|
if _, err := w.Write(b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range h.Files {
|
|
f := &h.Files[i]
|
|
sum, err := readSource(w, sources[order[i]], f.Size, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sum != f.SHA256 {
|
|
return fmt.Errorf("capsule: file %q changed after its first reading: its SHA-256 is another", f.Path)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.Head = h
|
|
return res, nil
|
|
}
|
|
|
|
// newHead checks the files and the texts of opts with the rules of spec
|
|
// §29.4 to §29.6, in the words of a writer (spec §62.1 rule 15), and returns
|
|
// the head with the files in the byte order of their paths, their layout and
|
|
// mtime, and a zero salt and zero SHA-256; order[i] is the source of entry
|
|
// i. The comment has its CR LF, and any lone CR, turned into LF (§29.6).
|
|
func newHead(sources []Source, opts EncryptOptions) (*Head, []int, error) {
|
|
comment := strings.ReplaceAll(strings.ReplaceAll(opts.Comment, "\r\n", "\n"), "\r", "\n")
|
|
switch {
|
|
case len(sources) == 0 && comment == "":
|
|
return nil, nil, errors.New("capsule: a format 3 capsule holds at least one file or a comment (spec §62.1 rule 14)")
|
|
case len(sources) > MaxFiles:
|
|
return nil, nil, fmt.Errorf("capsule: %d files, more than %d", len(sources), MaxFiles)
|
|
}
|
|
if err := checkHeadText("comment", comment, MaxCommentLen, pathrule.CheckComment); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := checkHeadText("declared author", opts.Author, MaxAuthorLen, pathrule.CheckAuthor); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
order := make([]int, len(sources))
|
|
for i := range order {
|
|
order[i] = i
|
|
}
|
|
// R8: the byte order of the paths, which is the order of Go strings.
|
|
slices.SortStableFunc(order, func(a, b int) int { return strings.Compare(sources[a].Path, sources[b].Path) })
|
|
h := &Head{Comment: comment, Author: opts.Author, Critical: opts.HeadCritical, Noncritical: opts.HeadNoncritical}
|
|
paths := make([]string, len(order))
|
|
var end uint64
|
|
for i, j := range order {
|
|
src, p := sources[j], sources[j].Path
|
|
switch {
|
|
case i > 0 && p == paths[i-1]:
|
|
return nil, nil, fmt.Errorf("capsule: path %q given twice", p)
|
|
case !utf8.ValidString(p):
|
|
return nil, nil, fmt.Errorf("capsule: path %q: R1: not valid UTF-8", p)
|
|
case len(p) == 0 || len(p) > MaxPathLen:
|
|
return nil, nil, fmt.Errorf("capsule: path %q: R1: %d bytes, not 1 to %d", p, len(p), MaxPathLen)
|
|
case src.Size < 0:
|
|
return nil, nil, fmt.Errorf("capsule: file %q: negative size %d", p, src.Size)
|
|
case src.Open == nil:
|
|
return nil, nil, fmt.Errorf("capsule: file %q: Source.Open is nil", p)
|
|
case uint64(src.Size) > MaxPayloadLength-end:
|
|
return nil, nil, fmt.Errorf("capsule: the files add up to more than %d bytes, the maximum of L", uint64(MaxPayloadLength))
|
|
}
|
|
if err := pathrule.CheckPath(p); err != nil {
|
|
return nil, nil, fmt.Errorf("capsule: path %q: %w", p, err)
|
|
}
|
|
f := File{Path: p, Size: uint64(src.Size), Start: end, End: end + uint64(src.Size)}
|
|
if t := src.ModTime; !t.IsZero() {
|
|
if u := t.Unix(); u >= 0 && u <= MaxMTime {
|
|
f.MTime, f.HasMTime = uint64(u), true
|
|
}
|
|
}
|
|
h.Files = append(h.Files, f)
|
|
paths[i], end = p, f.End
|
|
}
|
|
if err := pathrule.CheckTree(paths); err != nil {
|
|
var e *pathrule.Error
|
|
if errors.As(err, &e) && e.Paths[0] > 0 {
|
|
return nil, nil, fmt.Errorf("capsule: paths %q and %q: %w", paths[e.Paths[1]-1], paths[e.Paths[0]-1], err)
|
|
}
|
|
return nil, nil, fmt.Errorf("capsule: paths: %w", err)
|
|
}
|
|
return h, order, nil
|
|
}
|
|
|
|
// checkHeadText checks the comment or the declared author, when present:
|
|
// valid UTF-8, at most max bytes, and the characters of spec §29.6.
|
|
func checkHeadText(what, s string, max int, check func(string) error) error {
|
|
switch {
|
|
case s == "":
|
|
return nil
|
|
case !utf8.ValidString(s):
|
|
return fmt.Errorf("capsule: %s: not valid UTF-8", what)
|
|
case len(s) > max:
|
|
return fmt.Errorf("capsule: %s: %d bytes, more than %d", what, len(s), max)
|
|
}
|
|
if err := check(s); err != nil {
|
|
return fmt.Errorf("capsule: %s: %w", what, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// selfCheckHead decodes HEAD_CBOR with the rules of the reader, but for the
|
|
// knowledge of its critical extensions, which depends on the reader, as
|
|
// selfCheckControl does with the control (spec §62.1 rule 17). A head that
|
|
// the reader rejects would only be found after the date.
|
|
func selfCheckHead(b []byte) error {
|
|
if _, err := decodeHead(b); err != nil {
|
|
return fmt.Errorf("capsule: self-check: the reader rejects this head: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readSource reads the file of src, which must be exactly size bytes, and
|
|
// returns its SHA-256; w, when not nil, receives its bytes. In the second
|
|
// reading, a file whose size differs has changed (spec §62.1 rule 18).
|
|
func readSource(w io.Writer, src Source, size uint64, second bool) ([32]byte, error) {
|
|
var sum [32]byte
|
|
mismatch := func(format string, args ...any) error {
|
|
if second {
|
|
return fmt.Errorf("capsule: file %q changed after its first reading: %s", src.Path, fmt.Sprintf(format, args...))
|
|
}
|
|
return fmt.Errorf("capsule: file %q: %s", src.Path, fmt.Sprintf(format, args...))
|
|
}
|
|
rc, err := src.Open()
|
|
if err != nil {
|
|
return sum, fmt.Errorf("capsule: file %q: %w", src.Path, err)
|
|
}
|
|
defer rc.Close()
|
|
h := sha256.New()
|
|
buf := make([]byte, 32<<10)
|
|
defer clear(buf)
|
|
var n uint64
|
|
for {
|
|
k, err := rc.Read(buf)
|
|
if uint64(k) > size-n {
|
|
return sum, mismatch("more than its size of %d bytes", size)
|
|
}
|
|
h.Write(buf[:k])
|
|
if w != nil && k > 0 {
|
|
if _, err := w.Write(buf[:k]); err != nil {
|
|
return sum, err
|
|
}
|
|
}
|
|
n += uint64(k)
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return sum, fmt.Errorf("capsule: file %q: %w", src.Path, err)
|
|
}
|
|
}
|
|
if n != size {
|
|
return sum, mismatch("%d bytes, not its size of %d", n, size)
|
|
}
|
|
h.Sum(sum[:0])
|
|
return sum, nil
|
|
}
|