internal/format: implement outer layer parsing and marshaling

This commit is contained in:
Filippo Valsorda
2019-08-03 18:42:59 -04:00
parent 06cbe4f91e
commit 52dbe9eecf
4 changed files with 186 additions and 0 deletions

9
Dockerfile.gofuzz Normal file
View File

@@ -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"]

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/FiloSottile/age
go 1.12

141
internal/format/format.go Normal file
View File

@@ -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:]
}

View File

@@ -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
}