age: replace ParseX25519Identities with ParseIdentities

The latter returns a []Identity that can be used with Decrypt directly.
This commit is contained in:
Filippo Valsorda
2020-09-20 12:42:43 +02:00
parent 65f171a239
commit 22e598d458
5 changed files with 53 additions and 21 deletions

16
age.go
View File

@@ -28,7 +28,7 @@
// There is no default path for age keys. Instead, they should be stored at
// application-specific paths. The CLI supports files where private keys are
// listed one per line, ignoring empty lines and lines starting with "#". These
// files can be parsed with ParseX25519Identities.
// files can be parsed with ParseIdentities.
//
// When integrating age into a new system, it's recommended that you only
// support X25519 keys, and not SSH keys. The latter are supported for manual
@@ -90,11 +90,13 @@ type Stanza struct {
const fileKeySize = 16
const streamNonceSize = 16
// Encrypt returns a WriteCloser. Writes to the returned value are encrypted and
// written to dst as an age file. Every recipient will be able to decrypt the file.
// Encrypt encrypts a file to one or more recipients.
//
// The caller must call Close on the returned value when done for the last chunk
// to be encrypted and flushed to dst.
// Writes to the returned WriteCloser are encrypted and written to dst as an age
// file. Every recipient will be able to decrypt the file.
//
// The caller must call Close on the WriteCloser when done for the last chunk to
// be encrypted and flushed to dst.
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
if len(recipients) == 0 {
return nil, errors.New("no recipients specified")
@@ -137,7 +139,9 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return stream.NewWriter(streamKey(fileKey, nonce), dst)
}
// Decrypt returns a Reader reading the decrypted plaintext of the age file read
// Decrypt decrypts a file encrypted to one or more identities.
//
// It returns a Reader reading the decrypted plaintext of the age file read
// from src. All identities will be tried until one successfully decrypts the file.
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
if len(identities) == 0 {

View File

@@ -13,6 +13,7 @@ import (
"io"
"io/ioutil"
"log"
"os"
"strings"
"testing"
@@ -78,6 +79,32 @@ func ExampleDecrypt() {
// File contents: "Black lives matter."
}
func ExampleParseIdentities() {
keyFile, err := os.Open("testdata/keys.txt")
if err != nil {
log.Fatalf("Failed to open private keys file: %v", err)
}
identities, err := age.ParseIdentities(keyFile)
if err != nil {
log.Fatalf("Failed to parse private key %q: %v", privateKey, err)
}
out := &bytes.Buffer{}
f := bytes.NewReader(fileContents)
r, err := age.Decrypt(f, identities...)
if err != nil {
log.Fatalf("Failed to open encrypted file: %v", err)
}
if _, err := io.Copy(out, r); err != nil {
log.Fatalf("Failed to read encrypted file: %v", err)
}
fmt.Printf("File contents: %q\n", out.Bytes())
// Output:
// File contents: "Black lives matter."
}
func ExampleGenerateX25519Identity() {
identity, err := age.GenerateX25519Identity()
if err != nil {
@@ -164,7 +191,7 @@ func TestEncryptDecryptScrypt(t *testing.T) {
}
}
func TestParseX25519Identities(t *testing.T) {
func TestParseIdentities(t *testing.T) {
tests := []struct {
name string
wantCount int
@@ -184,13 +211,13 @@ AGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := age.ParseX25519Identities(strings.NewReader(tt.file))
got, err := age.ParseIdentities(strings.NewReader(tt.file))
if (err != nil) != tt.wantErr {
t.Errorf("ParseX25519Identities() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("ParseIdentities() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(got) != tt.wantCount {
t.Errorf("ParseX25519Identities() returned %d identities, want %d", len(got), tt.wantCount)
t.Errorf("ParseIdentities() returned %d identities, want %d", len(got), tt.wantCount)
}
})
}

View File

@@ -51,15 +51,11 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
return parseSSHIdentity(name, contents)
}
ids, err := age.ParseX25519Identities(b)
ids, err := age.ParseIdentities(b)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
res := make([]age.Identity, 0, len(ids))
for _, id := range ids {
res = append(res, id)
}
return res, nil
return ids, nil
}
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {

2
testdata/keys.txt vendored Normal file
View File

@@ -0,0 +1,2 @@
# Test key for ExampleParseX25519Identities.
AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU

View File

@@ -150,7 +150,7 @@ func ParseX25519Identity(s string) (*X25519Identity, error) {
return nil, fmt.Errorf("malformed secret key: %v", err)
}
if t != "AGE-SECRET-KEY-" {
return nil, fmt.Errorf("malformed secret key: invalid type %q", t)
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
}
r, err := newX25519IdentityFromScalar(k)
if err != nil {
@@ -159,15 +159,18 @@ func ParseX25519Identity(s string) (*X25519Identity, error) {
return r, nil
}
// ParseX25519Identities parses a file with one or more Bech32 private key
// encodings, one per line. Empty lines and lines starting with "#" are ignored.
// ParseIdentities parses a file with one or more private key encodings, one per
// line. Empty lines and lines starting with "#" are ignored.
//
// This is the same syntax as the private key files accepted by the CLI, except
// the CLI also accepts SSH private keys, which are not recommended for the
// average application.
func ParseX25519Identities(f io.Reader) ([]*X25519Identity, error) {
//
// Currently, all returned values are of type X25519Identity, but different
// types might be returned in the future.
func ParseIdentities(f io.Reader) ([]Identity, error) {
const privateKeySizeLimit = 1 << 24 // 16 MiB
var ids []*X25519Identity
var ids []Identity
scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))
var n int
for scanner.Scan() {