mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
age: add ParseX25519Identities and key management docs
This commit is contained in:
23
age.go
23
age.go
@@ -12,8 +12,29 @@
|
||||
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
|
||||
// use the filippo.io/age/agessh package.
|
||||
//
|
||||
// Age encrypted files are binary and not malleable, for encoding them as text,
|
||||
// Age encrypted files are binary and not malleable. For encoding them as text,
|
||||
// use the filippo.io/age/armor package.
|
||||
//
|
||||
// Key management
|
||||
//
|
||||
// Age does not have a global keyring. Instead, since age keys are small,
|
||||
// textual, and cheap, you are encoraged to generate dedicated keys for each
|
||||
// task and application.
|
||||
//
|
||||
// Recipient public keys can be passed around as command line flags and in
|
||||
// config files, while secret keys should be stored in dedicated files, through
|
||||
// secret management systems, or as environment variables.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// encryption operations. If you need to tie into existing key management
|
||||
// infrastructure, you might want to consider implementing your own Recipient
|
||||
// and Identity.
|
||||
package age
|
||||
|
||||
import (
|
||||
|
||||
33
age_test.go
33
age_test.go
@@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
@@ -162,3 +163,35 @@ func TestEncryptDecryptScrypt(t *testing.T) {
|
||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseX25519Identities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wantCount int
|
||||
wantErr bool
|
||||
file string
|
||||
}{
|
||||
{"valid", 2, false, `
|
||||
# this is a comment
|
||||
# AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3
|
||||
#
|
||||
|
||||
AGE-SECRET-KEY-1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59QJ
|
||||
AGE-SECRET-KEY-19WUMFE89H3928FRJ5U3JYRNHM6CERQGKSQ584AQ8QY7T7R09D32SWE4DYH`},
|
||||
{"invalid", 0, true, `
|
||||
AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3
|
||||
AGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := age.ParseX25519Identities(strings.NewReader(tt.file))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseX25519Identities() 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,12 +239,12 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
&LazyScryptIdentity{passphrasePrompt},
|
||||
}
|
||||
|
||||
// TODO: use the default location if no arguments are provided:
|
||||
// os.UserConfigDir()/age/keys.txt, ~/.ssh/id_rsa, ~/.ssh/id_ed25519
|
||||
// TODO: check the default SSH location if no arguments are provided
|
||||
// (~/.ssh/id_rsa, ~/.ssh/id_ed25519).
|
||||
for _, name := range keys {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
logFatalf("Error reading %q: %v", name, err)
|
||||
}
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -31,8 +30,6 @@ func parseRecipient(arg string) (age.Recipient, error) {
|
||||
return nil, fmt.Errorf("unknown recipient type: %q", arg)
|
||||
}
|
||||
|
||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||
|
||||
func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
@@ -40,46 +37,29 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
contents, err := ioutil.ReadAll(io.LimitReader(f, privateKeySizeLimit))
|
||||
b := bufio.NewReader(f)
|
||||
const pemHeader = "-----BEGIN"
|
||||
if peeked, _ := b.Peek(len(pemHeader)); string(peeked) == pemHeader {
|
||||
const privateKeySizeLimit = 1 << 14 // 16 KiB
|
||||
contents, err := ioutil.ReadAll(io.LimitReader(b, privateKeySizeLimit))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
}
|
||||
if len(contents) == privateKeySizeLimit {
|
||||
return nil, fmt.Errorf("failed to read %q: file too long", name)
|
||||
}
|
||||
return parseSSHIdentity(name, contents)
|
||||
}
|
||||
|
||||
ids, err := age.ParseX25519Identities(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
}
|
||||
if len(contents) == privateKeySizeLimit {
|
||||
return nil, fmt.Errorf("failed to read %q: file too long", name)
|
||||
res := make([]age.Identity, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
res = append(res, id)
|
||||
}
|
||||
|
||||
var ids []age.Identity
|
||||
var ageParsingError error
|
||||
scanner := bufio.NewScanner(bytes.NewReader(contents))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "-----BEGIN") {
|
||||
return parseSSHIdentity(name, contents)
|
||||
}
|
||||
if ageParsingError != nil {
|
||||
continue
|
||||
}
|
||||
i, err := age.ParseX25519Identity(line)
|
||||
if err != nil {
|
||||
ageParsingError = fmt.Errorf("malformed secret keys file %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, i)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
}
|
||||
if ageParsingError != nil {
|
||||
return nil, ageParsingError
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("no secret keys found in %q", name)
|
||||
}
|
||||
return ids, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||
|
||||
38
x25519.go
38
x25519.go
@@ -7,6 +7,7 @@
|
||||
package age
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
@@ -146,18 +147,49 @@ func GenerateX25519Identity() (*X25519Identity, error) {
|
||||
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key %q: %v", s, err)
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
if t != "AGE-SECRET-KEY-" {
|
||||
return nil, fmt.Errorf("malformed secret key %q: invalid type %q", s, t)
|
||||
return nil, fmt.Errorf("malformed secret key: invalid type %q", t)
|
||||
}
|
||||
r, err := newX25519IdentityFromScalar(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key %q: %v", s, err)
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
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.
|
||||
//
|
||||
// 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) {
|
||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||
var ids []*X25519Identity
|
||||
scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))
|
||||
var n int
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
i, err := ParseX25519Identity(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error at line %d: %v", n, err)
|
||||
}
|
||||
ids = append(ids, i)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret keys file: %v", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("no secret keys found")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "X25519" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
|
||||
Reference in New Issue
Block a user