diff --git a/internal/age/age.go b/internal/age/age.go index 0177f71..b067c04 100644 --- a/internal/age/age.go +++ b/internal/age/age.go @@ -150,6 +150,10 @@ RecipientsLoop: return nil, errors.New("bad header MAC") } + if hdr.Armor { + payload = format.ArmoredReader(payload) + } + nonce := make([]byte, 16) if _, err := io.ReadFull(payload, nonce); err != nil { return nil, fmt.Errorf("failed to read nonce: %v", err) diff --git a/internal/format/armor.go b/internal/format/armor.go index 2bf54dd..8328b8f 100644 --- a/internal/format/armor.go +++ b/internal/format/armor.go @@ -6,9 +6,13 @@ package format -import "io" - -import "encoding/base64" +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "io" +) type newlineWriter struct { dst io.Writer @@ -50,6 +54,8 @@ func (nopCloser) Close() error { return nil } func NopCloser(w io.Writer) io.WriteCloser { return nopCloser{w} } +var endOfArmor = []byte("--- end of file ---\n") + func ArmoredWriter(dst io.Writer) io.WriteCloser { // TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps. w := base64.NewEncoder(b64, &newlineWriter{dst: dst}) @@ -62,8 +68,91 @@ func ArmoredWriter(dst io.Writer) io.WriteCloser { if err := w.Close(); err != nil { return err } - _, err := dst.Write([]byte("\n--- end of file ---\n")) + if _, err := dst.Write([]byte("\n")); err != nil { + return err + } + _, err := dst.Write(endOfArmor) return err }), } } + +type armoredReader struct { + r *bufio.Reader + unread []byte // backed by buf + buf [bytesPerLine]byte + err error +} + +func ArmoredReader(r io.Reader) io.Reader { + return &armoredReader{r: bufio.NewReader(r)} +} + +func (r *armoredReader) Read(p []byte) (int, error) { + if len(r.unread) > 0 { + n := copy(p, r.unread) + r.unread = r.unread[n:] + return n, nil + } + if r.err != nil { + return 0, r.err + } + + getLine := func() ([]byte, error) { + line, err := r.r.ReadBytes('\n') + if err != nil { + if err == io.EOF { + err = errors.New("invalid input") + } + return nil, err + } + // Unconditionally accept CRLF because the line ending context of the + // header is lost at the ArmoredReader caller. =( + if bytes.HasSuffix(line, []byte("\r\n")) { + line[len(line)-2] = '\n' + line = line[:len(line)-1] + } + return line, nil + } + + line, err := getLine() + if err != nil { + return 0, r.setErr(err) + } + if bytes.Equal(line, endOfArmor) { + return 0, r.setErr(io.EOF) + } + line = bytes.TrimSuffix(line, []byte("\n")) + if bytes.Contains(line, []byte("\r")) { + return 0, r.setErr(errors.New("invalid input")) + } + if len(line) > columnsPerLine { + return 0, r.setErr(errors.New("invalid input")) + } + r.unread = r.buf[:] + n, err := b64.Decode(r.unread, line) + if err != nil { + return 0, r.setErr(err) + } + r.unread = r.unread[:n] + + if n < bytesPerLine { + line, err := getLine() + if err != nil { + return 0, r.setErr(err) + } + if !bytes.Equal(line, endOfArmor) { + return 0, r.setErr(errors.New("invalid input")) + } + r.err = io.EOF + } + + nn := copy(p, r.unread) + r.unread = r.unread[nn:] + return nn, nil +} + +func (r *armoredReader) setErr(err error) error { + r.err = err + return err +} diff --git a/internal/format/format.go b/internal/format/format.go index 57a89ab..f8ac3e9 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -46,6 +46,7 @@ const bytesPerLine = columnsPerLine / 4 * 3 const intro = "This is a file encrypted with age-tool.com, version 1\n" const introWithArmor = "This is an armored file encrypted with age-tool.com, version 1\n" +const introWithArmorCRLF = "This is an armored file encrypted with age-tool.com, version 1\r\n" var recipientPrefix = []byte("->") var footerPrefix = []byte("---") @@ -121,7 +122,15 @@ func Parse(input io.Reader) (*Header, io.Reader, error) { if err != nil { return nil, nil, errorf("failed to read intro: %v", err) } - if line != intro { + var normalizeCRLF bool + switch line { + case intro: + case introWithArmor: + h.Armor = true + case introWithArmorCRLF: + h.Armor = true + normalizeCRLF = true + default: return nil, nil, errorf("unexpected intro: %q", line) } @@ -131,6 +140,13 @@ func Parse(input io.Reader) (*Header, io.Reader, error) { if err != nil { return nil, nil, errorf("failed to read header: %v", err) } + if normalizeCRLF { + if !bytes.HasSuffix(line, []byte("\r\n")) { + return nil, nil, errorf("unexpected LF in CRLF input") + } + line[len(line)-2] = '\n' + line = line[:len(line)-1] + } if bytes.HasPrefix(line, footerPrefix) { prefix, args := splitArgs(line)