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/hmac"
|
|
|
|
|
"crypto/sha256"
|
2021-02-02 12:51:35 +00:00
|
|
|
"errors"
|
2019-10-07 01:19:04 +00:00
|
|
|
"io"
|
|
|
|
|
|
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"
|
|
|
|
|
"golang.org/x/crypto/hkdf"
|
|
|
|
|
)
|
|
|
|
|
|
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
|
|
|
// aeadEncrypt encrypts a message with a one-time key.
|
2019-10-07 01:19:04 +00:00
|
|
|
func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
|
|
|
|
aead, err := chacha20poly1305.New(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2019-12-28 01:24:44 +00:00
|
|
|
// The nonce is fixed because this function is only used in places where the
|
|
|
|
|
// spec guarantees each key is only used once (by deriving it from values
|
|
|
|
|
// that include fresh randomness), allowing us to save the overhead.
|
|
|
|
|
// For the code that encrypts the actual payload, look at the
|
|
|
|
|
// filippo.io/age/internal/stream package.
|
2019-10-07 01:19:04 +00:00
|
|
|
nonce := make([]byte, chacha20poly1305.NonceSize)
|
|
|
|
|
return aead.Seal(nil, nonce, plaintext, nil), nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-02 12:51:35 +00:00
|
|
|
var errIncorrectCiphertextSize = errors.New("encrypted value has unexpected length")
|
|
|
|
|
|
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
|
|
|
// aeadDecrypt decrypts a message of an expected fixed size.
|
|
|
|
|
//
|
|
|
|
|
// The message size is limited to mitigate multi-key attacks, where a ciphertext
|
|
|
|
|
// can be crafted that decrypts successfully under multiple keys. Short
|
|
|
|
|
// ciphertexts can only target two keys, which has limited impact.
|
|
|
|
|
func aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) {
|
2019-10-07 01:19:04 +00:00
|
|
|
aead, err := chacha20poly1305.New(key)
|
|
|
|
|
if 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
|
|
|
if len(ciphertext) != size+aead.Overhead() {
|
2021-02-02 12:51:35 +00:00
|
|
|
return nil, errIncorrectCiphertextSize
|
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
|
|
|
}
|
2019-10-07 01:19:04 +00:00
|
|
|
nonce := make([]byte, chacha20poly1305.NonceSize)
|
|
|
|
|
return aead.Open(nil, nonce, ciphertext, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func headerMAC(fileKey []byte, hdr *format.Header) ([]byte, error) {
|
|
|
|
|
h := hkdf.New(sha256.New, fileKey, nil, []byte("header"))
|
|
|
|
|
hmacKey := make([]byte, 32)
|
|
|
|
|
if _, err := io.ReadFull(h, hmacKey); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
hh := hmac.New(sha256.New, hmacKey)
|
|
|
|
|
if err := hdr.MarshalWithoutMAC(hh); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return hh.Sum(nil), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func streamKey(fileKey, nonce []byte) []byte {
|
|
|
|
|
h := hkdf.New(sha256.New, fileKey, nonce, []byte("payload"))
|
|
|
|
|
streamKey := make([]byte, chacha20poly1305.KeySize)
|
|
|
|
|
if _, err := io.ReadFull(h, streamKey); err != nil {
|
|
|
|
|
panic("age: internal error: failed to read from HKDF: " + err.Error())
|
|
|
|
|
}
|
|
|
|
|
return streamKey
|
|
|
|
|
}
|