2021-11-24 10:29:15 +00:00
|
|
|
// Copyright 2019 The age Authors. All rights reserved.
|
2019-10-07 03:16:20 +00:00
|
|
|
// Use of this source code is governed by a BSD-style
|
2021-11-24 10:29:15 +00:00
|
|
|
// license that can be found in the LICENSE file.
|
2019-10-07 03:16:20 +00:00
|
|
|
|
2019-10-07 01:19:04 +00:00
|
|
|
package age
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2019-10-07 03:16:20 +00:00
|
|
|
"strings"
|
2019-10-07 01:19:04 +00:00
|
|
|
|
2019-12-26 12:16:51 +00:00
|
|
|
"filippo.io/age/internal/bech32"
|
2019-12-07 04:00:57 +00:00
|
|
|
"filippo.io/age/internal/format"
|
2019-10-07 01:19:04 +00:00
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
2019-11-28 02:49:40 +00:00
|
|
|
"golang.org/x/crypto/curve25519"
|
2019-10-07 01:19:04 +00:00
|
|
|
"golang.org/x/crypto/hkdf"
|
|
|
|
|
)
|
|
|
|
|
|
2019-12-26 16:59:20 +00:00
|
|
|
const x25519Label = "age-encryption.org/v1/X25519"
|
2019-10-07 01:19:04 +00:00
|
|
|
|
2025-11-17 11:32:50 +00:00
|
|
|
// X25519Recipient is the standard age pre-quantum public key. Messages
|
|
|
|
|
// encrypted to this recipient can be decrypted with the corresponding
|
|
|
|
|
// [X25519Identity]. For post-quantum resistance, use [HybridRecipient].
|
age: mitigate multi-key attacks on ChaCha20Poly1305
It's possible to craft ChaCha20Poly1305 ciphertexts that decrypt under
multiple keys. (I know, it's wild.)
The impact is different for different recipients, but in general only
applies to Chosen Ciphertext Attacks against online decryption oracles:
* With the scrypt recipient, it lets the attacker make a recipient
stanza that decrypts with multiple passwords, speeding up a bruteforce
in terms of oracle queries (but not scrypt work, which can be
precomputed) to logN by binary search.
Limiting the ciphertext size limits the keys to two, which makes this
acceptable: it's a loss of only one bit of security in a scenario
(online decryption oracles) that is not recommended.
* With the X25519 recipient, it lets the attacker search for accepted
public keys without using multiple recipient stanzas in the message.
That lets the attacker bypass the 20 recipients limit (which was not
actually intended to defend against deanonymization attacks).
This is not really in the threat model for age: we make no attempt to
provide anonymity in an online CCA scenario.
Anyway, limiting the keys to two by enforcing short ciphertexts
mitigates the attack: it only lets the attacker test 40 keys per
message instead of 20.
* With the ssh-ed25519 recipient, the attack should be irrelevant, since
the recipient stanza includes a 32-bit hash of the public key, making
it decidedly not anonymous.
Also to avoid breaking the abstraction in the agessh package, we don't
mitigate the attack for this recipient, but we document the lack of
anonymity.
This was reported by Paul Grubbs in the context of the upcoming paper
"Partitioning Oracle Attacks", USENIX Security 2021 (to appear), by
Julia Len, Paul Grubbs, and Thomas Ristenpart.
2020-09-19 16:18:59 +00:00
|
|
|
//
|
|
|
|
|
// 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.
|
2019-10-07 01:19:04 +00:00
|
|
|
type X25519Recipient struct {
|
2019-10-13 22:14:54 +00:00
|
|
|
theirPublicKey []byte
|
2019-10-07 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ Recipient = &X25519Recipient{}
|
|
|
|
|
|
2020-06-28 01:03:35 +00:00
|
|
|
// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.
|
|
|
|
|
func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {
|
2019-10-13 22:14:54 +00:00
|
|
|
if len(publicKey) != curve25519.PointSize {
|
2019-10-07 01:19:04 +00:00
|
|
|
return nil, errors.New("invalid X25519 public key")
|
|
|
|
|
}
|
2019-10-13 22:14:54 +00:00
|
|
|
r := &X25519Recipient{
|
|
|
|
|
theirPublicKey: make([]byte, curve25519.PointSize),
|
|
|
|
|
}
|
|
|
|
|
copy(r.theirPublicKey, publicKey)
|
2019-10-07 01:19:04 +00:00
|
|
|
return r, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 06:53:37 +00:00
|
|
|
// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key
|
|
|
|
|
// encoding with the "age1" prefix.
|
2019-10-07 03:16:20 +00:00
|
|
|
func ParseX25519Recipient(s string) (*X25519Recipient, error) {
|
2019-12-26 12:16:51 +00:00
|
|
|
t, k, err := bech32.Decode(s)
|
2019-10-07 03:16:20 +00:00
|
|
|
if err != nil {
|
2019-12-26 12:16:51 +00:00
|
|
|
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)
|
2019-10-07 03:16:20 +00:00
|
|
|
}
|
2020-06-28 01:03:35 +00:00
|
|
|
r, err := newX25519RecipientFromPoint(k)
|
2019-10-07 03:16:20 +00:00
|
|
|
if err != nil {
|
2019-12-26 12:16:51 +00:00
|
|
|
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
2019-10-07 03:16:20 +00:00
|
|
|
}
|
|
|
|
|
return r, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-31 20:59:06 +00:00
|
|
|
func (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error) {
|
2019-10-13 22:14:54 +00:00
|
|
|
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 {
|
2019-10-07 01:19:04 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-13 22:14:54 +00:00
|
|
|
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2019-10-07 01:19:04 +00:00
|
|
|
|
2020-06-27 23:44:26 +00:00
|
|
|
l := &Stanza{
|
2019-10-07 01:19:04 +00:00
|
|
|
Type: "X25519",
|
2019-10-13 22:14:54 +00:00
|
|
|
Args: []string{format.EncodeToString(ourPublicKey)},
|
2019-10-07 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
|
2019-10-13 22:14:54 +00:00
|
|
|
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))
|
2019-10-07 01:19:04 +00:00
|
|
|
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
|
|
|
|
|
}
|
2019-10-13 21:24:21 +00:00
|
|
|
l.Body = wrappedKey
|
2019-10-07 01:19:04 +00:00
|
|
|
|
2021-01-31 20:59:06 +00:00
|
|
|
return []*Stanza{l}, nil
|
2019-10-07 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
|
2020-05-18 06:53:37 +00:00
|
|
|
// String returns the Bech32 public key encoding of r.
|
2019-10-07 03:16:20 +00:00
|
|
|
func (r *X25519Recipient) String() string {
|
2019-12-26 12:16:51 +00:00
|
|
|
s, _ := bech32.Encode("age", r.theirPublicKey)
|
|
|
|
|
return s
|
2019-10-07 03:16:20 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:32:50 +00:00
|
|
|
// X25519Identity is the standard pre-quantum age private key, which can decrypt
|
|
|
|
|
// messages encrypted to the corresponding [X25519Recipient]. For post-quantum
|
|
|
|
|
// resistance, use [HybridIdentity].
|
2019-10-07 01:19:04 +00:00
|
|
|
type X25519Identity struct {
|
2019-10-13 22:14:54 +00:00
|
|
|
secretKey, ourPublicKey []byte
|
2019-10-07 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ Identity = &X25519Identity{}
|
|
|
|
|
|
2020-06-28 01:03:35 +00:00
|
|
|
// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.
|
|
|
|
|
func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {
|
2019-10-13 22:14:54 +00:00
|
|
|
if len(secretKey) != curve25519.ScalarSize {
|
2019-10-07 01:19:04 +00:00
|
|
|
return nil, errors.New("invalid X25519 secret key")
|
|
|
|
|
}
|
2019-10-13 22:14:54 +00:00
|
|
|
i := &X25519Identity{
|
|
|
|
|
secretKey: make([]byte, curve25519.ScalarSize),
|
|
|
|
|
}
|
|
|
|
|
copy(i.secretKey, secretKey)
|
|
|
|
|
i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)
|
2019-10-07 01:19:04 +00:00
|
|
|
return i, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-28 01:03:35 +00:00
|
|
|
// GenerateX25519Identity randomly generates a new X25519Identity.
|
2019-10-13 22:14:54 +00:00
|
|
|
func GenerateX25519Identity() (*X25519Identity, error) {
|
2019-11-28 02:49:40 +00:00
|
|
|
secretKey := make([]byte, curve25519.ScalarSize)
|
2019-10-13 22:14:54 +00:00
|
|
|
if _, err := rand.Read(secretKey); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("internal error: %v", err)
|
|
|
|
|
}
|
2020-06-28 01:03:35 +00:00
|
|
|
return newX25519IdentityFromScalar(secretKey)
|
2019-10-13 22:14:54 +00:00
|
|
|
}
|
|
|
|
|
|
age: mitigate multi-key attacks on ChaCha20Poly1305
It's possible to craft ChaCha20Poly1305 ciphertexts that decrypt under
multiple keys. (I know, it's wild.)
The impact is different for different recipients, but in general only
applies to Chosen Ciphertext Attacks against online decryption oracles:
* With the scrypt recipient, it lets the attacker make a recipient
stanza that decrypts with multiple passwords, speeding up a bruteforce
in terms of oracle queries (but not scrypt work, which can be
precomputed) to logN by binary search.
Limiting the ciphertext size limits the keys to two, which makes this
acceptable: it's a loss of only one bit of security in a scenario
(online decryption oracles) that is not recommended.
* With the X25519 recipient, it lets the attacker search for accepted
public keys without using multiple recipient stanzas in the message.
That lets the attacker bypass the 20 recipients limit (which was not
actually intended to defend against deanonymization attacks).
This is not really in the threat model for age: we make no attempt to
provide anonymity in an online CCA scenario.
Anyway, limiting the keys to two by enforcing short ciphertexts
mitigates the attack: it only lets the attacker test 40 keys per
message instead of 20.
* With the ssh-ed25519 recipient, the attack should be irrelevant, since
the recipient stanza includes a 32-bit hash of the public key, making
it decidedly not anonymous.
Also to avoid breaking the abstraction in the agessh package, we don't
mitigate the attack for this recipient, but we document the lack of
anonymity.
This was reported by Paul Grubbs in the context of the upcoming paper
"Partitioning Oracle Attacks", USENIX Security 2021 (to appear), by
Julia Len, Paul Grubbs, and Thomas Ristenpart.
2020-09-19 16:18:59 +00:00
|
|
|
// ParseX25519Identity returns a new X25519Identity from a Bech32 private key
|
2020-05-18 06:53:37 +00:00
|
|
|
// encoding with the "AGE-SECRET-KEY-1" prefix.
|
2019-10-07 03:16:20 +00:00
|
|
|
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
2019-12-26 12:16:51 +00:00
|
|
|
t, k, err := bech32.Decode(s)
|
2019-10-07 03:16:20 +00:00
|
|
|
if err != nil {
|
2020-09-20 10:17:15 +00:00
|
|
|
return nil, fmt.Errorf("malformed secret key: %v", err)
|
2019-12-26 12:16:51 +00:00
|
|
|
}
|
|
|
|
|
if t != "AGE-SECRET-KEY-" {
|
2020-09-20 10:42:43 +00:00
|
|
|
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
|
2019-10-07 03:16:20 +00:00
|
|
|
}
|
2020-06-28 01:03:35 +00:00
|
|
|
r, err := newX25519IdentityFromScalar(k)
|
2019-10-07 03:16:20 +00:00
|
|
|
if err != nil {
|
2020-09-20 10:17:15 +00:00
|
|
|
return nil, fmt.Errorf("malformed secret key: %v", err)
|
2019-10-07 03:16:20 +00:00
|
|
|
}
|
|
|
|
|
return r, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-31 20:59:06 +00:00
|
|
|
func (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
|
|
|
|
return multiUnwrap(i.unwrap, stanzas)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) {
|
2019-10-07 01:19:04 +00:00
|
|
|
if block.Type != "X25519" {
|
2019-11-25 00:15:53 +00:00
|
|
|
return nil, ErrIncorrectIdentity
|
2019-10-07 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
2019-10-13 22:14:54 +00:00
|
|
|
if len(publicKey) != curve25519.PointSize {
|
2019-10-07 01:19:04 +00:00
|
|
|
return nil, errors.New("invalid X25519 recipient block")
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-13 22:14:54 +00:00
|
|
|
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
|
|
|
|
|
}
|
2019-10-07 01:19:04 +00:00
|
|
|
|
2019-10-13 22:14:54 +00:00
|
|
|
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))
|
2019-10-07 01:19:04 +00:00
|
|
|
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
|
|
|
|
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
age: mitigate multi-key attacks on ChaCha20Poly1305
It's possible to craft ChaCha20Poly1305 ciphertexts that decrypt under
multiple keys. (I know, it's wild.)
The impact is different for different recipients, but in general only
applies to Chosen Ciphertext Attacks against online decryption oracles:
* With the scrypt recipient, it lets the attacker make a recipient
stanza that decrypts with multiple passwords, speeding up a bruteforce
in terms of oracle queries (but not scrypt work, which can be
precomputed) to logN by binary search.
Limiting the ciphertext size limits the keys to two, which makes this
acceptable: it's a loss of only one bit of security in a scenario
(online decryption oracles) that is not recommended.
* With the X25519 recipient, it lets the attacker search for accepted
public keys without using multiple recipient stanzas in the message.
That lets the attacker bypass the 20 recipients limit (which was not
actually intended to defend against deanonymization attacks).
This is not really in the threat model for age: we make no attempt to
provide anonymity in an online CCA scenario.
Anyway, limiting the keys to two by enforcing short ciphertexts
mitigates the attack: it only lets the attacker test 40 keys per
message instead of 20.
* With the ssh-ed25519 recipient, the attack should be irrelevant, since
the recipient stanza includes a 32-bit hash of the public key, making
it decidedly not anonymous.
Also to avoid breaking the abstraction in the agessh package, we don't
mitigate the attack for this recipient, but we document the lack of
anonymity.
This was reported by Paul Grubbs in the context of the upcoming paper
"Partitioning Oracle Attacks", USENIX Security 2021 (to appear), by
Julia Len, Paul Grubbs, and Thomas Ristenpart.
2020-09-19 16:18:59 +00:00
|
|
|
fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)
|
2021-02-02 12:51:35 +00:00
|
|
|
if err == errIncorrectCiphertextSize {
|
|
|
|
|
return nil, errors.New("invalid X25519 recipient block: incorrect file key size")
|
|
|
|
|
} else if err != nil {
|
2019-11-25 00:15:53 +00:00
|
|
|
return nil, ErrIncorrectIdentity
|
2019-10-07 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
return fileKey, nil
|
|
|
|
|
}
|
2019-10-07 03:16:20 +00:00
|
|
|
|
2020-05-18 06:53:37 +00:00
|
|
|
// Recipient returns the public X25519Recipient value corresponding to i.
|
2019-10-07 03:16:20 +00:00
|
|
|
func (i *X25519Identity) Recipient() *X25519Recipient {
|
|
|
|
|
r := &X25519Recipient{}
|
|
|
|
|
r.theirPublicKey = i.ourPublicKey
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 06:53:37 +00:00
|
|
|
// String returns the Bech32 private key encoding of i.
|
2019-10-07 03:16:20 +00:00
|
|
|
func (i *X25519Identity) String() string {
|
2019-12-26 12:16:51 +00:00
|
|
|
s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey)
|
|
|
|
|
return strings.ToUpper(s)
|
2019-10-07 03:16:20 +00:00
|
|
|
}
|