Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error message for decryption with a session key #237

Merged
merged 8 commits into from
Nov 18, 2024
43 changes: 40 additions & 3 deletions openpgp/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import (
"strconv"
)

var (
twiss marked this conversation as resolved.
Show resolved Hide resolved
// ErrDecryptSessionKeyParsing is a generic error message for parsing errors in decrypted data
// to reduce the risk of oracle attacks.
ErrDecryptSessionKeyParsing = DecryptWithSessionKeyError("parsing error")
// ErrAEADTagVerification is returned if one of the tag verifications in SEIPDv2 fails
ErrAEADTagVerification error = DecryptWithSessionKeyError("AEAD tag verification failed")
// ErrMDCHashMismatch
ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
// ErrMDCMissing
ErrMDCMissing error = SignatureError("MDC packet not found")
)

// A StructuralError is returned when OpenPGP data is found to be syntactically
// invalid.
type StructuralError string
Expand All @@ -17,6 +29,34 @@ func (s StructuralError) Error() string {
return "openpgp: invalid data: " + string(s)
}

// A DecryptWithSessionKeyError is returned when a failure occurs when reading from symmetrically decrypted data or
// an authentication tag verification fails.
// Such an error indicates that the supplied session key is likely wrong or the data got corrupted.
type DecryptWithSessionKeyError string

func (s DecryptWithSessionKeyError) Error() string {
return "openpgp: decryption with session key failed: " + string(s)
}

// HandleSensitiveParsingError handles parsing errors when reading data from potentially decrypted data.
// The function makes parsing errors generic to reduce the risk of oracle attacks in SEIPDv1.
func HandleSensitiveParsingError(err error, decrypted bool) error {
if !decrypted {
// Data was not encrypted so we return the inner error.
return err
}
// The data is read from a stream that decrypts using a session key;
// therefore, we need to handle parsing errors appropriately.
// This is essential to mitigate the risk of oracle attacks.
if decError, ok := err.(*DecryptWithSessionKeyError); ok {
return decError
}
if decError, ok := err.(DecryptWithSessionKeyError); ok {
return decError
}
return ErrDecryptSessionKeyParsing
}

// UnsupportedError indicates that, although the OpenPGP data is valid, it
// makes use of currently unimplemented features.
type UnsupportedError string
Expand All @@ -41,9 +81,6 @@ func (b SignatureError) Error() string {
return "openpgp: invalid signature: " + string(b)
}

var ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
var ErrMDCMissing error = SignatureError("MDC packet not found")

type signatureExpiredError int

func (se signatureExpiredError) Error() string {
Expand Down
8 changes: 5 additions & 3 deletions openpgp/packet/aead_crypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
nonce := ar.computeNextNonce()
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
if err != nil {
return nil, err
return nil, errors.ErrAEADTagVerification
}
ar.bytesProcessed += len(plainChunk)
if err = ar.aeadCrypter.incrementIndex(); err != nil {
Expand All @@ -172,8 +172,10 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
// ... and total number of encrypted octets
adata = append(adata, amountBytes...)
nonce := ar.computeNextNonce()
_, err := ar.aead.Open(nil, nonce, tag, adata)
return err
if _, err := ar.aead.Open(nil, nonce, tag, adata); err != nil {
return errors.ErrAEADTagVerification
}
return nil
}

// aeadEncrypter encrypts and writes bytes. It encrypts when necessary according
Expand Down
28 changes: 19 additions & 9 deletions openpgp/packet/symmetric_key_encrypted_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ package packet
// by an integrity protected packet (SEIPD v1 or v2).

type packetSequence struct {
password string
packets string
contents string
password string
packets string
contents string
faultyDataPacket string
}

func keyAndIpePackets() []*packetSequence {
if V5Disabled {
return []*packetSequence{symEncTestv6}
return []*packetSequence{symEncRFC9580, symEncRFC4880}
}
return []*packetSequence{symEncTestv6, aeadEaxRFC, aeadOcbRFC}
return []*packetSequence{symEncRFC9580, symEncRFC4880, aeadEaxRFC, aeadOcbRFC}
}

// https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-complete-aead-eax-encrypted-
Expand All @@ -30,9 +31,18 @@ var aeadOcbRFC = &packetSequence{
contents: "cb1462000000000048656c6c6f2c20776f726c64210a",
}

// OpenPGP crypto refresh A.7.1.
var symEncTestv6 = &packetSequence{
// From OpenPGP RFC9580 A.9 https://www.rfc-editor.org/rfc/rfc9580.html#name-sample-aead-eax-encryption-
var symEncRFC9580 = &packetSequence{
password: "password",
packets: "c33c061a07030b0308e9d39785b2070008ffb42e7c483ef4884457cb3726b9b3db9ff776e5f4d9a40952e2447298851abfff7526df2dd554417579a7799fd26902070306fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9ffc6fc6d65bbfd24dcd0790966e6d1e85a30053784cb1d8b6a0699ef12155a7b2ad6258531b57651fd7777912fa95e35d9b40216f69a4c248db28ff4331f1632907399e6ff9",
contents: "cb1362000000000048656c6c6f2c20776f726c6421d50e1ce2269a9eddef81032172b7ed7c",
packets: "c340061e07010b0308a5ae579d1fc5d82bff69224f919993b3506fa3b59a6a73cff8c5efc5f41c57fb54e1c226815d7828f5f92c454eb65ebe00ab5986c68e6e7c55d269020701069ff90e3b321964f3a42913c8dcc6619325015227efb7eaeaa49f04c2e674175d4a3d226ed6afcb9ca9ac122c1470e11c63d4c0ab241c6a938ad48bf99a5a99b90bba8325de61047540258ab7959a95ad051dda96eb15431dfef5f5e2255ca78261546e339a",
contents: "cb1362000000000048656c6c6f2c20776f726c6421d50eae5bf0cd6705500355816cb0c8ff",
// Missing last authentication chunk
faultyDataPacket: "d259020701069ff90e3b321964f3a42913c8dcc6619325015227efb7eaeaa49f04c2e674175d4a3d226ed6afcb9ca9ac122c1470e11c63d4c0ab241c6a938ad48bf99a5a99b90bba8325de61047540258ab7959a95ad051dda96eb",
}

// From the OpenPGP interoperability test suite (Test: S2K mechanisms, iterated min + esk)
var symEncRFC4880 = &packetSequence{
password: "password",
packets: "c32e0409030873616c7a6967657200080674a0d96a4a6e122b1d5bbaa3fac117b9cbb46c7e38f12967386b57e2f79d11d23f01cee77ceed8544e6d52c78bd33c81bd366c8673b68955ddbd1ade98fe6a9b4e27ae54cd10dda7cd3a4637f44e0ead895ebebdcf0c679f1342745628f104e7",
contents: "cb1462000000000048656c6c6f20576f726c64203a29",
}
131 changes: 112 additions & 19 deletions openpgp/packet/symmetric_key_encrypted_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
mathrand "math/rand"
"testing"

"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/s2k"
)

Expand All @@ -20,25 +21,12 @@ const maxPassLen = 64
// Tests against RFC vectors
func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) {
for _, testCase := range keyAndIpePackets() {
// Key
buf := readerFromHex(testCase.packets)
packet, err := Read(buf)
if err != nil {
t.Fatalf("failed to read SymmetricKeyEncrypted: %s", err)
}
ske, ok := packet.(*SymmetricKeyEncrypted)
if !ok {
t.Fatal("didn't find SymmetricKeyEncrypted packet")
}
// Decrypt key
key, cipherFunc, err := ske.Decrypt([]byte(testCase.password))
if err != nil {
t.Fatal(err)
}
packet, err = Read(buf)
if err != nil {
t.Fatalf("failed to read SymmetricallyEncrypted: %s", err)
}
// Read and verify the key packet
ske, dataPacket := readSymmetricKeyEncrypted(t, testCase.packets)
key, cipherFunc := decryptSymmetricKey(t, ske, []byte(testCase.password))

packet := readSymmetricallyEncrypted(t, dataPacket)

// Decrypt contents
var edp EncryptedDataPacket
switch p := packet.(type) {
Expand All @@ -49,6 +37,7 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) {
default:
t.Fatal("no integrity protected packet")
}

r, err := edp.Decrypt(cipherFunc, key)
if err != nil {
t.Fatal(err)
Expand All @@ -66,6 +55,110 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) {
}
}

func TestTagVerificationError(t *testing.T) {
for _, testCase := range keyAndIpePackets() {
ske, dataPacket := readSymmetricKeyEncrypted(t, testCase.packets)
key, cipherFunc := decryptSymmetricKey(t, ske, []byte(testCase.password))

// Corrupt chunk
tmp := make([]byte, len(dataPacket))
copy(tmp, dataPacket)
tmp[38] += 1
packet := readSymmetricallyEncrypted(t, tmp)
// Decrypt contents and check integrity
checkIntegrityError(t, packet, cipherFunc, key)

// Corrupt final tag or mdc
dataPacket[len(dataPacket)-1] += 1
packet = readSymmetricallyEncrypted(t, dataPacket)
// Decrypt contents and check integrity
checkIntegrityError(t, packet, cipherFunc, key)

if len(testCase.faultyDataPacket) > 0 {
dataPacket, err := hex.DecodeString(testCase.faultyDataPacket)
if err != nil {
t.Fatal(err)
}
packet = readSymmetricallyEncrypted(t, dataPacket)
// Decrypt contents and check integrity
checkIntegrityError(t, packet, cipherFunc, key)
}
}
}

func readSymmetricKeyEncrypted(t *testing.T, packetHex string) (*SymmetricKeyEncrypted, []byte) {
t.Helper()

buf := readerFromHex(packetHex)
packet, err := Read(buf)
if err != nil {
t.Fatalf("failed to read SymmetricKeyEncrypted: %s", err)
}

ske, ok := packet.(*SymmetricKeyEncrypted)
if !ok {
t.Fatal("didn't find SymmetricKeyEncrypted packet")
}

dataPacket, err := io.ReadAll(buf)
if err != nil {
t.Fatalf("failed to read data packet: %s", err)
}
return ske, dataPacket
}

func decryptSymmetricKey(t *testing.T, ske *SymmetricKeyEncrypted, password []byte) ([]byte, CipherFunction) {
t.Helper()

key, cipherFunc, err := ske.Decrypt(password)
if err != nil {
t.Fatalf("failed to decrypt symmetric key: %s", err)
}

return key, cipherFunc
}

func readSymmetricallyEncrypted(t *testing.T, dataPacket []byte) Packet {
t.Helper()
packet, err := Read(bytes.NewReader(dataPacket))
if err != nil {
t.Fatalf("failed to read SymmetricallyEncrypted: %s", err)
}
return packet
}

func checkIntegrityError(t *testing.T, packet Packet, cipherFunc CipherFunction, key []byte) {
t.Helper()

switch p := packet.(type) {
case *SymmetricallyEncrypted:
edp := p
data, err := edp.Decrypt(cipherFunc, key)
if err != nil {
t.Fatal(err)
}

_, err = io.ReadAll(data)
if err == nil {
err = data.Close()
}
if err != nil {
if edp.Version == 1 && err != errors.ErrMDCHashMismatch {
t.Fatalf("no integrity error (expected MDC hash mismatch)")
}
if edp.Version == 2 && err != errors.ErrAEADTagVerification {
t.Fatalf("no integrity error (expected AEAD tag verification failure)")
}
} else {
t.Fatalf("no error (expected integrity check failure)")
}
case *AEADEncrypted:
return
default:
t.Fatal("no integrity protected packet found")
}
}

func TestSerializeSymmetricKeyEncryptedV6RandomizeSlow(t *testing.T) {
ciphers := map[string]CipherFunction{
"AES128": CipherAES128,
Expand Down
6 changes: 3 additions & 3 deletions openpgp/packet/symmetrically_encrypted_mdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const mdcPacketTagByte = byte(0x80) | 0x40 | 19

func (ser *seMDCReader) Close() error {
if ser.error {
return errors.ErrMDCMissing
return errors.ErrMDCHashMismatch
}

for !ser.eof {
Expand All @@ -159,7 +159,7 @@ func (ser *seMDCReader) Close() error {
break
}
if err != nil {
return errors.ErrMDCMissing
return errors.ErrMDCHashMismatch
}
}

Expand All @@ -172,7 +172,7 @@ func (ser *seMDCReader) Close() error {
// The hash already includes the MDC header, but we still check its value
// to confirm encryption correctness
if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size {
return errors.ErrMDCMissing
return errors.ErrMDCHashMismatch
}
return nil
}
Expand Down
12 changes: 6 additions & 6 deletions openpgp/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ FindKey:
}
mdFinal, sensitiveParsingErr := readSignedMessage(packets, md, keyring, config)
if sensitiveParsingErr != nil {
return nil, errors.StructuralError("parsing error")
return nil, errors.HandleSensitiveParsingError(sensitiveParsingErr, md.decrypted != nil)
}
return mdFinal, nil
}
Expand Down Expand Up @@ -368,7 +368,7 @@ func (cr *checkReader) Read(buf []byte) (int, error) {
}

if sensitiveParsingError != nil {
return n, errors.StructuralError("parsing error")
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
}

return n, nil
Expand All @@ -392,6 +392,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
scr.wrappedHash.Write(buf[:n])
}

readsDecryptedData := scr.md.decrypted != nil
if sensitiveParsingError == io.EOF {
var p packet.Packet
var readError error
Expand Down Expand Up @@ -434,16 +435,15 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
// unsigned hash of its own. In order to check this we need to
// close that Reader.
if scr.md.decrypted != nil {
mdcErr := scr.md.decrypted.Close()
if mdcErr != nil {
return n, mdcErr
if sensitiveParsingError := scr.md.decrypted.Close(); sensitiveParsingError != nil {
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
}
}
return n, io.EOF
}

if sensitiveParsingError != nil {
return n, errors.StructuralError("parsing error")
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, readsDecryptedData)
}

return n, nil
Expand Down
4 changes: 2 additions & 2 deletions openpgp/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ func TestCorruptedMessageInvalidSigHeader(t *testing.T) {
promptFunc := func(keys []Key, symmetric bool) ([]byte, error) {
return passphrase, nil
}
const expectedErr string = "openpgp: invalid data: parsing error"
const expectedErr string = "openpgp: decryption with session key failed: parsing error"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add similar tests for SEIPDv2 to test (and "demo") what kind of errors are thrown with AEAD (hopefully the AEAD chunks are not released/parsed even with streaming).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test for a missing last aead authentication tag:
6525f78

_, observedErr := ReadMessage(raw.Body, nil, promptFunc, nil)
if observedErr.Error() != expectedErr {
t.Errorf("Expected error '%s', but got error '%s'", expectedErr, observedErr)
Expand All @@ -833,7 +833,7 @@ func TestCorruptedMessageWrongLength(t *testing.T) {
promptFunc := func(keys []Key, symmetric bool) ([]byte, error) {
return passphrase, nil
}
const expectedErr string = "openpgp: invalid data: parsing error"
const expectedErr string = "openpgp: decryption with session key failed: parsing error"

file, err := os.Open("test_data/sym-corrupted-message-long-length.asc")
if err != nil {
Expand Down
Loading
Loading