mirror of
https://github.com/FiloSottile/age.git
synced 2026-03-11 08:55:41 +00:00
Now that we don't use Base64 in keys, let's just go back to the standard alphabet. Still in the spirit of reducing weirdness, use the PEM column count, so we can also reuse the lineWriter for the PEM armor. See https://groups.google.com/d/msg/age-dev/UAjkvLoCr9I/l4Q1h3OPAgAJ.
205 lines
4.9 KiB
Go
205 lines
4.9 KiB
Go
// 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 implements the age file format.
|
|
package format
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
type Header struct {
|
|
Armor bool
|
|
Recipients []*Recipient
|
|
MAC []byte
|
|
}
|
|
|
|
type Recipient struct {
|
|
Type string
|
|
Args []string
|
|
Body []byte
|
|
}
|
|
|
|
var b64 = base64.RawStdEncoding.Strict()
|
|
|
|
func DecodeString(s string) ([]byte, error) {
|
|
// CR and LF are ignored by DecodeString, but we don't want any malleability.
|
|
if strings.ContainsAny(s, "\n\r") {
|
|
return nil, errors.New(`unexpected newline character`)
|
|
}
|
|
return b64.DecodeString(s)
|
|
}
|
|
|
|
var EncodeToString = b64.EncodeToString
|
|
|
|
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("---")
|
|
|
|
func (r *Recipient) Marshal(w io.Writer) error {
|
|
if _, err := w.Write(recipientPrefix); err != nil {
|
|
return err
|
|
}
|
|
for _, a := range append([]string{r.Type}, r.Args...) {
|
|
if _, err := io.WriteString(w, " "+a); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
ww := base64.NewEncoder(b64, &newlineWriter{dst: w})
|
|
if _, err := ww.Write(r.Body); err != nil {
|
|
return err
|
|
}
|
|
if err := ww.Close(); err != nil {
|
|
return err
|
|
}
|
|
_, err := io.WriteString(w, "\n")
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
for _, r := range h.Recipients {
|
|
if err := r.Marshal(w); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := fmt.Fprintf(w, "%s", footerPrefix)
|
|
return err
|
|
}
|
|
|
|
func (h *Header) Marshal(w io.Writer) error {
|
|
if err := h.MarshalWithoutMAC(w); err != nil {
|
|
return err
|
|
}
|
|
mac := b64.EncodeToString(h.MAC)
|
|
_, err := fmt.Fprintf(w, " %s\n", mac)
|
|
return err
|
|
}
|
|
|
|
type ParseError string
|
|
|
|
func (e ParseError) Error() string {
|
|
return "parsing age header: " + string(e)
|
|
}
|
|
|
|
func errorf(format string, a ...interface{}) error {
|
|
return ParseError(fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
// Parse returns the header and a Reader that begins at the start of the
|
|
// payload.
|
|
func Parse(input io.Reader) (*Header, io.Reader, error) {
|
|
h := &Header{}
|
|
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:
|
|
return nil, nil, errorf("unexpected intro: %q", line)
|
|
}
|
|
|
|
var r *Recipient
|
|
for {
|
|
line, err := rr.ReadBytes('\n')
|
|
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)
|
|
if prefix != string(footerPrefix) || len(args) != 1 {
|
|
return nil, nil, errorf("malformed closing line: %q", line)
|
|
}
|
|
h.MAC, err = DecodeString(args[0])
|
|
if err != nil {
|
|
return nil, nil, errorf("malformed closing line %q: %v", line, err)
|
|
}
|
|
break
|
|
|
|
} else if bytes.HasPrefix(line, recipientPrefix) {
|
|
r = &Recipient{}
|
|
prefix, args := splitArgs(line)
|
|
if prefix != string(recipientPrefix) || len(args) < 1 {
|
|
return nil, nil, errorf("malformed recipient: %q", line)
|
|
}
|
|
r.Type = args[0]
|
|
r.Args = args[1:]
|
|
h.Recipients = append(h.Recipients, r)
|
|
|
|
} else if r != nil {
|
|
b, err := DecodeString(strings.TrimSuffix(string(line), "\n"))
|
|
if err != nil {
|
|
return nil, nil, errorf("malformed body line %q: %v", line, err)
|
|
}
|
|
if len(b) > bytesPerLine {
|
|
return nil, nil, errorf("malformed body line %q: too long", line)
|
|
}
|
|
r.Body = append(r.Body, b...)
|
|
if len(b) < bytesPerLine {
|
|
// Only the last line of a body can be short.
|
|
r = nil
|
|
}
|
|
|
|
} else {
|
|
return nil, nil, errorf("unexpected line: %q", line)
|
|
}
|
|
}
|
|
|
|
// Unwind the bufio overread and return the unbuffered input.
|
|
buf, err := rr.Peek(rr.Buffered())
|
|
if err != nil {
|
|
return nil, nil, errorf("internal error: %v", err)
|
|
}
|
|
payload := io.MultiReader(bytes.NewReader(buf), input)
|
|
|
|
return h, payload, nil
|
|
}
|
|
|
|
func splitArgs(line []byte) (string, []string) {
|
|
l := strings.TrimSuffix(string(line), "\n")
|
|
parts := strings.Split(l, " ")
|
|
return parts[0], parts[1:]
|
|
}
|