diff --git a/cmd/age/age.go b/cmd/age/age.go index 8159e2e..a2462a7 100644 --- a/cmd/age/age.go +++ b/cmd/age/age.go @@ -17,7 +17,7 @@ import ( "strings" "filippo.io/age/internal/age" - "filippo.io/age/internal/format" + "filippo.io/age/internal/armor" "golang.org/x/crypto/ssh/terminal" ) @@ -207,10 +207,10 @@ func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) { encrypt([]age.Recipient{r}, in, out, armor) } -func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, armor bool) { +func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) { ageEncrypt := age.Encrypt - if armor { - a := format.ArmoredWriter(out) + if withArmor { + a := armor.NewWriter(out) defer func() { if err := a.Close(); err != nil { logFatalf("Error: %v", err) @@ -248,9 +248,8 @@ func decrypt(keys []string, in io.Reader, out io.Writer) { } rr := bufio.NewReader(in) - armorHeader := "-----BEGIN AGE ENCRYPTED FILE-----" - if start, _ := rr.Peek(len(armorHeader)); string(start) == armorHeader { - in = format.ArmoredReader(rr) + if start, _ := rr.Peek(len(armor.Header)); string(start) == armor.Header { + in = armor.NewReader(rr) } else { in = rr } diff --git a/internal/format/armor.go b/internal/armor/armor.go similarity index 63% rename from internal/format/armor.go rename to internal/armor/armor.go index 6058c89..eebd244 100644 --- a/internal/format/armor.go +++ b/internal/armor/armor.go @@ -4,7 +4,12 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -package format +// Package armor provides a strict, streaming implementation of the ASCII +// armoring format for age files. +// +// It's PEM with type "AGE ENCRYPTED FILE", 64 character columns, no headers, +// and strict base64 decoding. +package armor import ( "bufio" @@ -12,50 +17,12 @@ import ( "encoding/base64" "errors" "io" + + "filippo.io/age/internal/format" ) -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-----" +const Header = "-----BEGIN AGE ENCRYPTED FILE-----" +const Footer = "-----END AGE ENCRYPTED FILE-----" type armoredWriter struct { started, closed bool @@ -65,7 +32,7 @@ type armoredWriter struct { func (a *armoredWriter) Write(p []byte) (int, error) { if !a.started { - if _, err := io.WriteString(a.dst, armorPreamble+"\n"); err != nil { + if _, err := io.WriteString(a.dst, Header+"\n"); err != nil { return 0, err } } @@ -81,26 +48,26 @@ func (a *armoredWriter) Close() error { if err := a.encoder.Close(); err != nil { return err } - _, err := io.WriteString(a.dst, "\n"+armorEnd+"\n") + _, err := io.WriteString(a.dst, "\n"+Footer+"\n") return err } -func ArmoredWriter(dst io.Writer) io.WriteCloser { +func NewWriter(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})} + format.NewlineWriter(dst))} } type armoredReader struct { r *bufio.Reader started bool unread []byte // backed by buf - buf [bytesPerLine]byte + buf [format.BytesPerLine]byte err error } -func ArmoredReader(r io.Reader) io.Reader { +func NewReader(r io.Reader) io.Reader { return &armoredReader{r: bufio.NewReader(r)} } @@ -130,7 +97,7 @@ func (r *armoredReader) Read(p []byte) (int, error) { if err != nil { return 0, r.setErr(err) } - if string(line) != armorPreamble { + if string(line) != Header { return 0, r.setErr(errors.New("invalid armor first line: " + string(line))) } r.started = true @@ -139,10 +106,10 @@ func (r *armoredReader) Read(p []byte) (int, error) { if err != nil { return 0, r.setErr(err) } - if string(line) == armorEnd { + if string(line) == Footer { return 0, r.setErr(io.EOF) } - if len(line) > columnsPerLine { + if len(line) > format.ColumnsPerLine { return 0, r.setErr(errors.New("invalid armor: column limit exceeded")) } r.unread = r.buf[:] @@ -152,12 +119,12 @@ func (r *armoredReader) Read(p []byte) (int, error) { } r.unread = r.unread[:n] - if n < bytesPerLine { + if n < format.BytesPerLine { line, err := getLine() if err != nil { return 0, r.setErr(err) } - if string(line) != armorEnd { + if string(line) != Footer { 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/armor/armor_test.go similarity index 86% rename from internal/format/armor_test.go rename to internal/armor/armor_test.go index f7831b6..a89466b 100644 --- a/internal/format/armor_test.go +++ b/internal/armor/armor_test.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -package format_test +package armor_test import ( "bytes" @@ -12,12 +12,12 @@ import ( "io/ioutil" "testing" - "filippo.io/age/internal/format" + "filippo.io/age/internal/armor" ) func TestArmor(t *testing.T) { buf := &bytes.Buffer{} - w := format.ArmoredWriter(buf) + w := armor.NewWriter(buf) plain := make([]byte, 611) if _, err := w.Write(plain); err != nil { t.Fatal(err) @@ -34,7 +34,7 @@ func TestArmor(t *testing.T) { t.Error("PEM decoded value doesn't match") } - r := format.ArmoredReader(buf) + r := armor.NewReader(buf) out, err := ioutil.ReadAll(r) if err != nil { t.Fatal(err) diff --git a/internal/format/format.go b/internal/format/format.go index 7c717c6..30ff48d 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -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 }