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/encrypt3.go

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
}

Powered by TurnKey Linux.