From f04064a41bfd562fdf676d669618ab015883bdd8 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sun, 17 Jan 2021 12:09:07 +0100 Subject: [PATCH] age: add NoIdentityMatchError Closes #147 --- age.go | 21 ++++++++++++++++++--- cmd/age/encrypted_keys.go | 12 +++++++----- scrypt.go | 4 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/age.go b/age.go index 7c90f42..810ba8c 100644 --- a/age.go +++ b/age.go @@ -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 { diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index 0be899e..7b8b54a 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -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 diff --git a/scrypt.go b/scrypt.go index 25bc42e..e7c5000 100644 --- a/scrypt.go +++ b/scrypt.go @@ -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 {