diff --git a/internal/age/age.go b/internal/age/age.go index dd88381..eb3813f 100644 --- a/internal/age/age.go +++ b/internal/age/age.go @@ -36,14 +36,19 @@ type Recipient interface { } func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) { - return encrypt(dst, false, recipients...) + // stream.Writer takes a WriteCloser, and will propagate Close calls (so + // that the ArmoredWriter will get closed), but we don't want to expose + // that behavior to our caller. + dstCloser := format.NopCloser(dst) + return encrypt(dstCloser, recipients...) } func EncryptWithArmor(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) { - return encrypt(dst, true, recipients...) + dstCloser := format.ArmoredWriter(dst) + return encrypt(dstCloser, recipients...) } -func encrypt(dst io.Writer, armor bool, recipients ...Recipient) (io.WriteCloser, error) { +func encrypt(dst io.WriteCloser, recipients ...Recipient) (io.WriteCloser, error) { if len(recipients) == 0 { return nil, errors.New("no recipients specified") } @@ -53,7 +58,7 @@ func encrypt(dst io.Writer, armor bool, recipients ...Recipient) (io.WriteCloser return nil, err } - hdr := &format.Header{Armor: armor} + hdr := &format.Header{} for i, r := range recipients { if r.Type() == "scrypt" && len(recipients) != 1 { return nil, errors.New("an scrypt recipient must be the only one") @@ -74,25 +79,15 @@ func encrypt(dst io.Writer, armor bool, recipients ...Recipient) (io.WriteCloser return nil, fmt.Errorf("failed to write header: %v", err) } - var finalDst io.WriteCloser - if armor { - finalDst = format.ArmoredWriter(dst) - } else { - // stream.Writer takes a WriteCloser, and will propagate Close calls (so - // that the ArmoredWriter will get closed), but we don't want to expose - // that behavior to our caller. - finalDst = format.NopCloser(dst) - } - nonce := make([]byte, 16) if _, err := rand.Read(nonce); err != nil { return nil, err } - if _, err := finalDst.Write(nonce); err != nil { + if _, err := dst.Write(nonce); err != nil { return nil, fmt.Errorf("failed to write nonce: %v", err) } - return stream.NewWriter(streamKey(fileKey, nonce), finalDst) + return stream.NewWriter(streamKey(fileKey, nonce), dst) } func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) { @@ -150,10 +145,6 @@ 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 8328b8f..89953a5 100644 --- a/internal/format/armor.go +++ b/internal/format/armor.go @@ -54,34 +54,50 @@ func (nopCloser) Close() error { return nil } func NopCloser(w io.Writer) io.WriteCloser { return nopCloser{w} } -var endOfArmor = []byte("--- end of file ---\n") +const armorPreamble = "-----BEGIN AGE ENCRYPTED FILE-----" +const armorEnd = "-----END AGE ENCRYPTED FILE-----" + +type armoredWriter struct { + started, closed bool + encoder io.WriteCloser + dst io.Writer +} + +func (a *armoredWriter) Write(p []byte) (int, error) { + if !a.started { + if _, err := io.WriteString(a.dst, armorPreamble+"\n"); err != nil { + return 0, err + } + } + a.started = true + return a.encoder.Write(p) +} + +func (a *armoredWriter) Close() error { + if a.closed { + return errors.New("ArmoredWriter already closed") + } + a.closed = true + if err := a.encoder.Close(); err != nil { + return err + } + _, err := io.WriteString(a.dst, "\n"+armorEnd+"\n") + return err +} 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}) - return struct { - io.Writer - io.Closer - }{ - Writer: w, - Closer: CloserFunc(func() error { - if err := w.Close(); err != nil { - return err - } - if _, err := dst.Write([]byte("\n")); err != nil { - return err - } - _, err := dst.Write(endOfArmor) - return err - }), - } + return &armoredWriter{dst: dst, + encoder: base64.NewEncoder(base64.StdEncoding.Strict(), + &newlineWriter{dst: dst})} } type armoredReader struct { - r *bufio.Reader - unread []byte // backed by buf - buf [bytesPerLine]byte - err error + r *bufio.Reader + started bool + unread []byte // backed by buf + buf [bytesPerLine]byte + err error } func ArmoredReader(r io.Reader) io.Reader { @@ -100,37 +116,37 @@ func (r *armoredReader) Read(p []byte) (int, error) { getLine := func() ([]byte, error) { line, err := r.r.ReadBytes('\n') - if err != nil { + if err != nil && len(line) == 0 { if err == io.EOF { - err = errors.New("invalid input") + err = errors.New("invalid armor: unexpected EOF") } 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 + return bytes.TrimSpace(line), nil } + if !r.started { + line, err := getLine() + if err != nil { + return 0, r.setErr(err) + } + if string(line) != armorPreamble { + return 0, r.setErr(errors.New("invalid armor first line: " + string(line))) + } + r.started = true + } line, err := getLine() if err != nil { return 0, r.setErr(err) } - if bytes.Equal(line, endOfArmor) { + if string(line) == armorEnd { 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")) + return 0, r.setErr(errors.New("invalid armor: column limit exceeded")) } r.unread = r.buf[:] - n, err := b64.Decode(r.unread, line) + n, err := base64.StdEncoding.Strict().Decode(r.unread, line) if err != nil { return 0, r.setErr(err) } @@ -141,8 +157,8 @@ func (r *armoredReader) Read(p []byte) (int, error) { if err != nil { return 0, r.setErr(err) } - if !bytes.Equal(line, endOfArmor) { - return 0, r.setErr(errors.New("invalid input")) + if string(line) != armorEnd { + return 0, r.setErr(errors.New("invalid armor closing line: " + string(line))) } r.err = io.EOF } diff --git a/internal/format/armor_test.go b/internal/format/armor_test.go new file mode 100644 index 0000000..f7831b6 --- /dev/null +++ b/internal/format/armor_test.go @@ -0,0 +1,45 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package format_test + +import ( + "bytes" + "encoding/pem" + "io/ioutil" + "testing" + + "filippo.io/age/internal/format" +) + +func TestArmor(t *testing.T) { + buf := &bytes.Buffer{} + w := format.ArmoredWriter(buf) + plain := make([]byte, 611) + if _, err := w.Write(plain); err != nil { + t.Fatal(err) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + block, _ := pem.Decode(buf.Bytes()) + if block == nil { + t.Fatal("PEM decoding failed") + } + if !bytes.Equal(block.Bytes, plain) { + t.Error("PEM decoded value doesn't match") + } + + r := format.ArmoredReader(buf) + out, err := ioutil.ReadAll(r) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(out, plain) { + t.Error("decoded value doesn't match") + } +} diff --git a/internal/format/format.go b/internal/format/format.go index 101a366..00d7def 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -18,7 +18,6 @@ import ( ) type Header struct { - Armor bool Recipients []*Recipient MAC []byte } @@ -45,8 +44,6 @@ const columnsPerLine = 64 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("---") @@ -75,14 +72,8 @@ func (r *Recipient) Marshal(w io.Writer) error { } func (h *Header) MarshalWithoutMAC(w io.Writer) error { - if h.Armor { - if _, err := io.WriteString(w, introWithArmor); err != nil { - return err - } - } else { - if _, err := io.WriteString(w, intro); err != nil { - return err - } + if _, err := io.WriteString(w, intro); err != nil { + return err } for _, r := range h.Recipients { if err := r.Marshal(w); err != nil { @@ -118,19 +109,18 @@ func Parse(input io.Reader) (*Header, io.Reader, error) { h := &Header{} rr := bufio.NewReader(input) + // TODO: find a way to communicate to the caller that the file was armored, + // as they might not appreciate the malleability. + if start, _ := rr.Peek(len(armorPreamble)); string(start) == armorPreamble { + input = ArmoredReader(rr) + rr = bufio.NewReader(input) + } + line, err := rr.ReadString('\n') if err != nil { return nil, nil, errorf("failed to read intro: %v", err) } - var normalizeCRLF bool - switch line { - case intro: - case introWithArmor: - h.Armor = true - case introWithArmorCRLF: - h.Armor = true - normalizeCRLF = true - default: + if line != intro { return nil, nil, errorf("unexpected intro: %q", line) } @@ -140,13 +130,6 @@ 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)