age

Simple, secure encryption with UNIX-style composability.
git clone git://git.sgregoratto.me/age
Log | Files | Refs | README | LICENSE

commit 52dbe9eecf8b2153f77b13157a382d53215b99df
parent 06cbe4f91ea984306996d1f7dbde1bb5ffd67fec
Author: Filippo Valsorda <hi@filippo.io>
Date:   Sat,  3 Aug 2019 18:42:59 -0400

internal/format: implement outer layer parsing and marshaling

Diffstat:
ADockerfile.gofuzz | 9+++++++++
Ago.mod | 3+++
Ainternal/format/format.go | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/format/format_gofuzz.go | 33+++++++++++++++++++++++++++++++++
4 files changed, 186 insertions(+), 0 deletions(-)

diff --git a/Dockerfile.gofuzz b/Dockerfile.gofuzz @@ -0,0 +1,9 @@ +FROM golang:1.12-alpine3.10 +RUN apk add --no-cache git +RUN go get github.com/dvyukov/go-fuzz/... +ADD . $GOPATH/src/github.com/FiloSottile/age/ +WORKDIR $GOPATH/src/github.com/FiloSottile/age +RUN GO111MODULE=on go mod vendor +RUN go-fuzz-build ./internal/format +VOLUME /workdir +ENTRYPOINT ["go-fuzz", "-workdir", "/workdir", "-bin", "format-fuzz.zip"] diff --git a/go.mod b/go.mod @@ -0,0 +1,3 @@ +module github.com/FiloSottile/age + +go 1.12 diff --git a/internal/format/format.go b/internal/format/format.go @@ -0,0 +1,141 @@ +package format + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "strings" +) + +type Header struct { + Recipients []*Recipient + AEAD string + MAC []byte +} + +type Recipient struct { + Type string + Args []string + Body []byte +} + +var b64 = base64.RawURLEncoding.Strict() + +func decodeString(s string) ([]byte, error) { + // CR and LF are ignored by DecodeString. LF is handled by the parser, + // but CR can introduce malleability. + if strings.Contains(s, "\r") { + return nil, errors.New(`invalid character: \r`) + } + return b64.DecodeString(s) +} + +const intro = "This is a file encrypted with age-tool.com, version 1\n" + +var recipientPrefix = []byte("->") +var footerPrefix = []byte("---") + +func (h *Header) Marshal(w io.Writer) error { + if _, err := io.WriteString(w, intro); err != nil { + return err + } + for _, r := range h.Recipients { + if _, err := w.Write(recipientPrefix); err != nil { + return err + } + for _, a := range append([]string{r.Type}, r.Args...) { + if _, err := io.WriteString(w, " "+a); err != nil { + return err + } + } + if _, err := io.WriteString(w, "\n"); err != nil { + return err + } + if _, err := w.Write(r.Body); err != nil { + return err + } + } + mac := b64.EncodeToString(h.MAC) + _, err := fmt.Fprintf(w, "%s %s %s\n", footerPrefix, h.AEAD, mac) + return err +} + +type ParseError string + +func (e ParseError) Error() string { + return "parsing age header: " + string(e) +} + +func errorf(format string, a ...interface{}) error { + return ParseError(fmt.Sprintf(format, a...)) +} + +// Parse returns the header and a Reader that begins at the start of the +// payload. +func Parse(input io.Reader) (*Header, io.Reader, error) { + h := &Header{} + rr := bufio.NewReader(input) + + line, err := rr.ReadString('\n') + if err != nil { + return nil, nil, errorf("failed to read intro: %v", err) + } + if line != intro { + return nil, nil, errorf("unexpected intro: %q", line) + } + + var r *Recipient + for { + line, err := rr.ReadBytes('\n') + if err != nil { + return nil, nil, errorf("failed to read header: %v", err) + } + + if bytes.HasPrefix(line, footerPrefix) { + prefix, args := splitArgs(line) + if prefix != string(footerPrefix) || len(args) != 2 { + return nil, nil, errorf("malformed closing line: %q", line) + } + h.AEAD = args[0] + h.MAC, err = decodeString(args[1]) + if err != nil { + return nil, nil, errorf("malformed closing line %q: %v", line, err) + } + break + + } else if bytes.HasPrefix(line, recipientPrefix) { + r = &Recipient{} + prefix, args := splitArgs(line) + if prefix != string(recipientPrefix) || len(args) < 1 { + return nil, nil, errorf("malformed recipient: %q", line) + } + r.Type = args[0] + r.Args = args[1:] + h.Recipients = append(h.Recipients, r) + + } else if r != nil { + r.Body = append(r.Body, line...) + + } else { + return nil, nil, errorf("unexpected line: %q", line) + } + } + + // Unwind the bufio overread and return the unbuffered input. + buf, err := rr.Peek(rr.Buffered()) + if err != nil { + return nil, nil, errorf("internal error: %v", err) + } + payload := io.MultiReader(bytes.NewReader(buf), input) + + return h, payload, nil +} + +func splitArgs(line []byte) (string, []string) { + l := strings.TrimSuffix(string(line), "\n") + parts := strings.Split(l, " ") + return parts[0], parts[1:] +} diff --git a/internal/format/format_gofuzz.go b/internal/format/format_gofuzz.go @@ -0,0 +1,33 @@ +package format + +import ( + "bytes" + "fmt" + "io" + "os" +) + +func Fuzz(data []byte) int { + h, payload, err := Parse(bytes.NewReader(data)) + if err != nil { + if h != nil { + panic("h != nil on error") + } + if payload != nil { + panic("payload != nil on error") + } + return 0 + } + w := &bytes.Buffer{} + if err := h.Marshal(w); err != nil { + panic(err) + } + if _, err := io.Copy(w, payload); err != nil { + panic(err) + } + if !bytes.Equal(w.Bytes(), data) { + fmt.Fprintf(os.Stderr, "%s\n%q\n%q\n\n", w, data, w) + panic("Marshal output different from input") + } + return 1 +}