internal/armor: new package

This commit is contained in:
Filippo Valsorda
2020-05-17 23:50:13 -04:00
parent a7c4274d23
commit c7c7f1870f
4 changed files with 71 additions and 70 deletions

View File

@@ -1,174 +0,0 @@
// 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
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"io"
)
type newlineWriter struct {
dst io.Writer
written int
}
func (w *newlineWriter) Write(p []byte) (n int, err error) {
for len(p) > 0 {
remainingInLine := columnsPerLine - (w.written % columnsPerLine)
if remainingInLine == columnsPerLine && w.written != 0 {
if _, err := w.dst.Write([]byte("\n")); err != nil {
return n, err
}
}
toWrite := remainingInLine
if toWrite > len(p) {
toWrite = len(p)
}
nn, err := w.dst.Write(p[:toWrite])
n += nn
w.written += nn
p = p[nn:]
if err != nil {
return n, err
}
}
return n, nil
}
type CloserFunc func() error
func (f CloserFunc) Close() error { return f() }
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func NopCloser(w io.Writer) io.WriteCloser { return nopCloser{w} }
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.
return &armoredWriter{dst: dst,
encoder: base64.NewEncoder(base64.StdEncoding.Strict(),
&newlineWriter{dst: dst})}
}
type armoredReader struct {
r *bufio.Reader
started bool
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 && len(line) == 0 {
if err == io.EOF {
err = errors.New("invalid armor: unexpected EOF")
}
return nil, err
}
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 string(line) == armorEnd {
return 0, r.setErr(io.EOF)
}
if len(line) > columnsPerLine {
return 0, r.setErr(errors.New("invalid armor: column limit exceeded"))
}
r.unread = r.buf[:]
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
if err != nil {
return 0, r.setErr(errors.New("invalid armor: " + err.Error()))
}
r.unread = r.unread[:n]
if n < bytesPerLine {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) != armorEnd {
return 0, r.setErr(errors.New("invalid armor closing line: " + string(line)))
}
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

@@ -1,45 +0,0 @@
// 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")
}
}

View File

@@ -40,8 +40,43 @@ func DecodeString(s string) ([]byte, error) {
var EncodeToString = b64.EncodeToString
const columnsPerLine = 64
const bytesPerLine = columnsPerLine / 4 * 3
const ColumnsPerLine = 64
const BytesPerLine = ColumnsPerLine / 4 * 3
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
// every ColumnsPerLine bytes. It does not insert a newline neither at the
// beginning nor at the end of the stream.
func NewlineWriter(dst io.Writer) io.Writer {
return &newlineWriter{dst: dst}
}
type newlineWriter struct {
dst io.Writer
written int
}
func (w *newlineWriter) Write(p []byte) (n int, err error) {
for len(p) > 0 {
remainingInLine := ColumnsPerLine - (w.written % ColumnsPerLine)
if remainingInLine == ColumnsPerLine && w.written != 0 {
if _, err := w.dst.Write([]byte("\n")); err != nil {
return n, err
}
}
toWrite := remainingInLine
if toWrite > len(p) {
toWrite = len(p)
}
nn, err := w.dst.Write(p[:toWrite])
n += nn
w.written += nn
p = p[nn:]
if err != nil {
return n, err
}
}
return n, nil
}
const intro = "age-encryption.org/v1\n"
@@ -63,7 +98,7 @@ func (r *Recipient) Marshal(w io.Writer) error {
if len(r.Body) == 0 {
return nil
}
ww := base64.NewEncoder(b64, &newlineWriter{dst: w})
ww := base64.NewEncoder(b64, NewlineWriter(w))
if _, err := ww.Write(r.Body); err != nil {
return err
}
@@ -158,14 +193,14 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
if err != nil {
return nil, nil, errorf("malformed body line %q: %v", line, err)
}
if len(b) > bytesPerLine {
if len(b) > BytesPerLine {
return nil, nil, errorf("malformed body line %q: too long", line)
}
if len(b) == 0 {
return nil, nil, errorf("malformed body line %q: line is empty", line)
}
r.Body = append(r.Body, b...)
if len(b) < bytesPerLine {
if len(b) < BytesPerLine {
// Only the last line of a body can be short.
r = nil
}