Files
age/x25519.go
2020-09-20 12:17:15 +02:00

241 lines
7.0 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 age
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"strings"
"filippo.io/age/internal/bech32"
"filippo.io/age/internal/format"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
)
const x25519Label = "age-encryption.org/v1/X25519"
// X25519Recipient is the standard age public key. Messages encrypted to this
// recipient can be decrypted with the corresponding X25519Identity.
//
// This recipient is anonymous, in the sense that an attacker can't tell from
// the message alone if it is encrypted to a certain recipient.
type X25519Recipient struct {
theirPublicKey []byte
}
var _ Recipient = &X25519Recipient{}
func (*X25519Recipient) Type() string { return "X25519" }
// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.
func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {
if len(publicKey) != curve25519.PointSize {
return nil, errors.New("invalid X25519 public key")
}
r := &X25519Recipient{
theirPublicKey: make([]byte, curve25519.PointSize),
}
copy(r.theirPublicKey, publicKey)
return r, nil
}
// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key
// encoding with the "age1" prefix.
func ParseX25519Recipient(s string) (*X25519Recipient, error) {
t, k, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
}
if t != "age" {
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
}
r, err := newX25519RecipientFromPoint(k)
if err != nil {
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
}
return r, nil
}
func (r *X25519Recipient) Wrap(fileKey []byte) (*Stanza, error) {
ephemeral := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(ephemeral); err != nil {
return nil, err
}
ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)
if err != nil {
return nil, err
}
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
if err != nil {
return nil, err
}
l := &Stanza{
Type: "X25519",
Args: []string{format.EncodeToString(ourPublicKey)},
}
salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))
salt = append(salt, ourPublicKey...)
salt = append(salt, r.theirPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, wrappingKey); err != nil {
return nil, err
}
wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)
if err != nil {
return nil, err
}
l.Body = wrappedKey
return l, nil
}
// String returns the Bech32 public key encoding of r.
func (r *X25519Recipient) String() string {
s, _ := bech32.Encode("age", r.theirPublicKey)
return s
}
// X25519Identity is the standard age private key, which can decrypt messages
// encrypted to the corresponding X25519Recipient.
type X25519Identity struct {
secretKey, ourPublicKey []byte
}
var _ Identity = &X25519Identity{}
func (*X25519Identity) Type() string { return "X25519" }
// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.
func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {
if len(secretKey) != curve25519.ScalarSize {
return nil, errors.New("invalid X25519 secret key")
}
i := &X25519Identity{
secretKey: make([]byte, curve25519.ScalarSize),
}
copy(i.secretKey, secretKey)
i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)
return i, nil
}
// GenerateX25519Identity randomly generates a new X25519Identity.
func GenerateX25519Identity() (*X25519Identity, error) {
secretKey := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(secretKey); err != nil {
return nil, fmt.Errorf("internal error: %v", err)
}
return newX25519IdentityFromScalar(secretKey)
}
// ParseX25519Identity returns a new X25519Identity from a Bech32 private key
// encoding with the "AGE-SECRET-KEY-1" prefix.
func ParseX25519Identity(s string) (*X25519Identity, error) {
t, k, err := bech32.Decode(s)
if err != nil {
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)
}
r, err := newX25519IdentityFromScalar(k)
if err != nil {
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
}
if len(block.Args) != 1 {
return nil, errors.New("invalid X25519 recipient block")
}
publicKey, err := format.DecodeString(block.Args[0])
if err != nil {
return nil, fmt.Errorf("failed to parse X25519 recipient: %v", err)
}
if len(publicKey) != curve25519.PointSize {
return nil, errors.New("invalid X25519 recipient block")
}
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
if err != nil {
return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
}
salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))
salt = append(salt, publicKey...)
salt = append(salt, i.ourPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, wrappingKey); err != nil {
return nil, err
}
fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)
if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil
}
// Recipient returns the public X25519Recipient value corresponding to i.
func (i *X25519Identity) Recipient() *X25519Recipient {
r := &X25519Recipient{}
r.theirPublicKey = i.ourPublicKey
return r
}
// String returns the Bech32 private key encoding of i.
func (i *X25519Identity) String() string {
s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey)
return strings.ToUpper(s)
}