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

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

View File

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

View File

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

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
}