internal/age: support parsing armored files

This commit is contained in:
Filippo Valsorda
2019-11-24 22:28:57 -05:00
parent 4c4e446f72
commit 884b6f365d
3 changed files with 114 additions and 5 deletions

View File

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

View File

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

View File

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