age: add NoIdentityMatchError

Closes #147
This commit is contained in:
Filippo Valsorda
2021-01-17 12:09:07 +01:00
parent 0fa220e4d7
commit f04064a41b
3 changed files with 27 additions and 10 deletions

21
age.go
View File

@@ -51,8 +51,9 @@ import (
// An Identity is a private key or other value that can decrypt an opaque file
// key from a recipient stanza.
//
// Unwrap must return ErrIncorrectIdentity for recipient stanzas that don't
// match the identity, any other error might be considered fatal.
// Unwrap must return an error wrapping ErrIncorrectIdentity for recipient
// stanzas that don't match the identity, any other error will be considered
// fatal.
type Identity interface {
Unwrap(block *Stanza) (fileKey []byte, err error)
}
@@ -125,6 +126,18 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return stream.NewWriter(streamKey(fileKey, nonce), dst)
}
// NoIdentityMatchError is returned by Decrypt when none of the supplied
// identities match the encrypted file.
type NoIdentityMatchError struct {
// Errors is a slice of all the errors returned to Decrypt by the Unwrap
// calls it made. They all wrap ErrIncorrectIdentity.
Errors []error
}
func (*NoIdentityMatchError) Error() string {
return "no identity matched any of the recipients"
}
// Decrypt decrypts a file encrypted to one or more identities.
//
// It returns a Reader reading the decrypted plaintext of the age file read
@@ -142,6 +155,7 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
return nil, errors.New("too many recipients")
}
errNoMatch := &NoIdentityMatchError{}
var fileKey []byte
RecipientsLoop:
for _, r := range hdr.Recipients {
@@ -151,6 +165,7 @@ RecipientsLoop:
for _, i := range identities {
fileKey, err = i.Unwrap((*Stanza)(r))
if errors.Is(err, ErrIncorrectIdentity) {
errNoMatch.Errors = append(errNoMatch.Errors, err)
continue
}
if err != nil {
@@ -161,7 +176,7 @@ RecipientsLoop:
}
}
if fileKey == nil {
return nil, errors.New("no identity matched any of the recipients")
return nil, errNoMatch
}
if mac, err := headerMAC(fileKey, hdr); err != nil {

View File

@@ -7,6 +7,7 @@
package main
import (
"errors"
"fmt"
"os"
@@ -33,11 +34,12 @@ func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err erro
return nil, err
}
fileKey, err = ii.Unwrap(block)
if err == age.ErrIncorrectIdentity {
// The API will just ignore the identity if the passphrase is wrong, and
// move on, eventually returning "no identity matched a recipient".
// Since we only supply one identity from the CLI, make it a fatal
// error with a better message.
if errors.Is(err, age.ErrIncorrectIdentity) {
// ScryptIdentity returns ErrIncorrectIdentity for an incorrect
// passphrase, which would lead Decrypt to returning "no identity
// matched any recipient". That makes sense in the API, where there
// might be multiple configured ScryptIdentity. Since in cmd/age there
// can be only one, return a better error message.
return nil, fmt.Errorf("incorrect passphrase")
}
return fileKey, err

View File

@@ -155,9 +155,9 @@ func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
// This AEAD is not robust, so an attacker could craft a message that
// decrypts under two different keys (meaning two different passphrases) and
// then use an error side-channel in an online decryption oracle to learn if
// either key is correct. This is deemed acceptable because the usa case (an
// either key is correct. This is deemed acceptable because the use case (an
// online decryption oracle) is not recommended, and the security loss is
// only one bit. This also does not bypass any scrypt work, but that work
// only one bit. This also does not bypass any scrypt work, although that work
// can be precomputed in an online oracle scenario.
fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
if err != nil {