mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
The Type() method was a mistake, as proven by the fact that I can remove it without losing any functionality. It gives special meaning to the "0th argument" of recipient stanzas, when actually it should be left up to Recipient implementations to make their own stanzas recognizable to their Identity counterparts. More importantly, there are totally reasonable Identity (and probably Recipient) implementations that don't know their own stanza type in advance. For example, a proxy plugin. Concretely, it was only used to special-case "scrypt" recipients, and to skip invoking Unwrap. The former can be done based on the returned recipient stanza, and the latter is best avoided entirely: the Identity should start by looking at the stanza and returning ErrIncorrectIdentity if it's of the wrong type. This is a breaking API change. However, we are still in beta, and none of the public downstreams look like they would be affected, as they only use Recipient and Identity implementations from this package, they only use them with the interfaces defined in this package, and they don't directly use the Type() method.
205 lines
5.9 KiB
Go
205 lines
5.9 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 (
|
|
"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{}
|
|
|
|
// 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{}
|
|
|
|
// 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: unknown type %q", t)
|
|
}
|
|
r, err := newX25519IdentityFromScalar(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed secret key: %v", err)
|
|
}
|
|
return r, 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)
|
|
}
|