mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 13:35: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
|
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
|
||||||
// use the filippo.io/age/agessh package.
|
// 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.
|
// 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
|
package age
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
33
age_test.go
33
age_test.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"filippo.io/age"
|
"filippo.io/age"
|
||||||
@@ -162,3 +163,35 @@ func TestEncryptDecryptScrypt(t *testing.T) {
|
|||||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
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},
|
&LazyScryptIdentity{passphrasePrompt},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use the default location if no arguments are provided:
|
// TODO: check the default SSH location if no arguments are provided
|
||||||
// os.UserConfigDir()/age/keys.txt, ~/.ssh/id_rsa, ~/.ssh/id_ed25519
|
// (~/.ssh/id_rsa, ~/.ssh/id_ed25519).
|
||||||
for _, name := range keys {
|
for _, name := range keys {
|
||||||
ids, err := parseIdentitiesFile(name)
|
ids, err := parseIdentitiesFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logFatalf("Error: %v", err)
|
logFatalf("Error reading %q: %v", name, err)
|
||||||
}
|
}
|
||||||
identities = append(identities, ids...)
|
identities = append(identities, ids...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -31,8 +30,6 @@ func parseRecipient(arg string) (age.Recipient, error) {
|
|||||||
return nil, fmt.Errorf("unknown recipient type: %q", arg)
|
return nil, fmt.Errorf("unknown recipient type: %q", arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
|
||||||
|
|
||||||
func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||||
f, err := os.Open(name)
|
f, err := os.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,46 +37,29 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||||
}
|
}
|
||||||
if len(contents) == privateKeySizeLimit {
|
res := make([]age.Identity, 0, len(ids))
|
||||||
return nil, fmt.Errorf("failed to read %q: file too long", name)
|
for _, id := range ids {
|
||||||
|
res = append(res, id)
|
||||||
}
|
}
|
||||||
|
return res, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||||
|
|||||||
38
x25519.go
38
x25519.go
@@ -7,6 +7,7 @@
|
|||||||
package age
|
package age
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -146,18 +147,49 @@ func GenerateX25519Identity() (*X25519Identity, error) {
|
|||||||
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
||||||
t, k, err := bech32.Decode(s)
|
t, k, err := bech32.Decode(s)
|
||||||
if err != nil {
|
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-" {
|
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)
|
r, err := newX25519IdentityFromScalar(k)
|
||||||
if err != nil {
|
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
|
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) {
|
func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) {
|
||||||
if block.Type != "X25519" {
|
if block.Type != "X25519" {
|
||||||
return nil, ErrIncorrectIdentity
|
return nil, ErrIncorrectIdentity
|
||||||
|
|||||||
Reference in New Issue
Block a user