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:
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
+}