diff --git a/age.go b/age.go index a603eee..948390f 100644 --- a/age.go +++ b/age.go @@ -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 { diff --git a/age_test.go b/age_test.go index 6e1245d..42f139e 100644 --- a/age_test.go +++ b/age_test.go @@ -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) } }) } diff --git a/cmd/age/parse.go b/cmd/age/parse.go index 622bd7a..e58f668 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -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) { diff --git a/testdata/keys.txt b/testdata/keys.txt new file mode 100644 index 0000000..5d30bcd --- /dev/null +++ b/testdata/keys.txt @@ -0,0 +1,2 @@ +# Test key for ExampleParseX25519Identities. +AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU diff --git a/x25519.go b/x25519.go index 4375ee8..5dac27e 100644 --- a/x25519.go +++ b/x25519.go @@ -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() {