Skip to content

Commit 5e3e39d

Browse files
authored
Improve error message for decryption with a session key (#237)
* feat: Improve error message for decryption with a session key * chore: Add explicit error message strings to tests * feat: Update SEIPD test vectors * chore: Move expected error to the old position * feat: Unify mdc integrity error * feat: Add aead test for missing last auth tag * docs: Add comments when handling parsing errors * feat: Unify parsing errors in SEIPDv1 decryption
1 parent 33a08b3 commit 5e3e39d

9 files changed

+216
-85
lines changed

openpgp/errors/errors.go

+40-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import (
99
"strconv"
1010
)
1111

12+
var (
13+
// ErrDecryptSessionKeyParsing is a generic error message for parsing errors in decrypted data
14+
// to reduce the risk of oracle attacks.
15+
ErrDecryptSessionKeyParsing = DecryptWithSessionKeyError("parsing error")
16+
// ErrAEADTagVerification is returned if one of the tag verifications in SEIPDv2 fails
17+
ErrAEADTagVerification error = DecryptWithSessionKeyError("AEAD tag verification failed")
18+
// ErrMDCHashMismatch
19+
ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
20+
// ErrMDCMissing
21+
ErrMDCMissing error = SignatureError("MDC packet not found")
22+
)
23+
1224
// A StructuralError is returned when OpenPGP data is found to be syntactically
1325
// invalid.
1426
type StructuralError string
@@ -17,6 +29,34 @@ func (s StructuralError) Error() string {
1729
return "openpgp: invalid data: " + string(s)
1830
}
1931

32+
// A DecryptWithSessionKeyError is returned when a failure occurs when reading from symmetrically decrypted data or
33+
// an authentication tag verification fails.
34+
// Such an error indicates that the supplied session key is likely wrong or the data got corrupted.
35+
type DecryptWithSessionKeyError string
36+
37+
func (s DecryptWithSessionKeyError) Error() string {
38+
return "openpgp: decryption with session key failed: " + string(s)
39+
}
40+
41+
// HandleSensitiveParsingError handles parsing errors when reading data from potentially decrypted data.
42+
// The function makes parsing errors generic to reduce the risk of oracle attacks in SEIPDv1.
43+
func HandleSensitiveParsingError(err error, decrypted bool) error {
44+
if !decrypted {
45+
// Data was not encrypted so we return the inner error.
46+
return err
47+
}
48+
// The data is read from a stream that decrypts using a session key;
49+
// therefore, we need to handle parsing errors appropriately.
50+
// This is essential to mitigate the risk of oracle attacks.
51+
if decError, ok := err.(*DecryptWithSessionKeyError); ok {
52+
return decError
53+
}
54+
if decError, ok := err.(DecryptWithSessionKeyError); ok {
55+
return decError
56+
}
57+
return ErrDecryptSessionKeyParsing
58+
}
59+
2060
// UnsupportedError indicates that, although the OpenPGP data is valid, it
2161
// makes use of currently unimplemented features.
2262
type UnsupportedError string
@@ -41,9 +81,6 @@ func (b SignatureError) Error() string {
4181
return "openpgp: invalid signature: " + string(b)
4282
}
4383

44-
var ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
45-
var ErrMDCMissing error = SignatureError("MDC packet not found")
46-
4784
type signatureExpiredError int
4885

4986
func (se signatureExpiredError) Error() string {

openpgp/packet/aead_crypter.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
147147
nonce := ar.computeNextNonce()
148148
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
149149
if err != nil {
150-
return nil, err
150+
return nil, errors.ErrAEADTagVerification
151151
}
152152
ar.bytesProcessed += len(plainChunk)
153153
if err = ar.aeadCrypter.incrementIndex(); err != nil {
@@ -172,8 +172,10 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
172172
// ... and total number of encrypted octets
173173
adata = append(adata, amountBytes...)
174174
nonce := ar.computeNextNonce()
175-
_, err := ar.aead.Open(nil, nonce, tag, adata)
176-
return err
175+
if _, err := ar.aead.Open(nil, nonce, tag, adata); err != nil {
176+
return errors.ErrAEADTagVerification
177+
}
178+
return nil
177179
}
178180

179181
// aeadEncrypter encrypts and writes bytes. It encrypts when necessary according

openpgp/packet/symmetric_key_encrypted_data_test.go

+19-9
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ package packet
44
// by an integrity protected packet (SEIPD v1 or v2).
55

66
type packetSequence struct {
7-
password string
8-
packets string
9-
contents string
7+
password string
8+
packets string
9+
contents string
10+
faultyDataPacket string
1011
}
1112

1213
func keyAndIpePackets() []*packetSequence {
1314
if V5Disabled {
14-
return []*packetSequence{symEncTestv6}
15+
return []*packetSequence{symEncRFC9580, symEncRFC4880}
1516
}
16-
return []*packetSequence{symEncTestv6, aeadEaxRFC, aeadOcbRFC}
17+
return []*packetSequence{symEncRFC9580, symEncRFC4880, aeadEaxRFC, aeadOcbRFC}
1718
}
1819

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

33-
// OpenPGP crypto refresh A.7.1.
34-
var symEncTestv6 = &packetSequence{
34+
// From OpenPGP RFC9580 A.9 https://www.rfc-editor.org/rfc/rfc9580.html#name-sample-aead-eax-encryption-
35+
var symEncRFC9580 = &packetSequence{
3536
password: "password",
36-
packets: "c33c061a07030b0308e9d39785b2070008ffb42e7c483ef4884457cb3726b9b3db9ff776e5f4d9a40952e2447298851abfff7526df2dd554417579a7799fd26902070306fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9ffc6fc6d65bbfd24dcd0790966e6d1e85a30053784cb1d8b6a0699ef12155a7b2ad6258531b57651fd7777912fa95e35d9b40216f69a4c248db28ff4331f1632907399e6ff9",
37-
contents: "cb1362000000000048656c6c6f2c20776f726c6421d50e1ce2269a9eddef81032172b7ed7c",
37+
packets: "c340061e07010b0308a5ae579d1fc5d82bff69224f919993b3506fa3b59a6a73cff8c5efc5f41c57fb54e1c226815d7828f5f92c454eb65ebe00ab5986c68e6e7c55d269020701069ff90e3b321964f3a42913c8dcc6619325015227efb7eaeaa49f04c2e674175d4a3d226ed6afcb9ca9ac122c1470e11c63d4c0ab241c6a938ad48bf99a5a99b90bba8325de61047540258ab7959a95ad051dda96eb15431dfef5f5e2255ca78261546e339a",
38+
contents: "cb1362000000000048656c6c6f2c20776f726c6421d50eae5bf0cd6705500355816cb0c8ff",
39+
// Missing last authentication chunk
40+
faultyDataPacket: "d259020701069ff90e3b321964f3a42913c8dcc6619325015227efb7eaeaa49f04c2e674175d4a3d226ed6afcb9ca9ac122c1470e11c63d4c0ab241c6a938ad48bf99a5a99b90bba8325de61047540258ab7959a95ad051dda96eb",
41+
}
42+
43+
// From the OpenPGP interoperability test suite (Test: S2K mechanisms, iterated min + esk)
44+
var symEncRFC4880 = &packetSequence{
45+
password: "password",
46+
packets: "c32e0409030873616c7a6967657200080674a0d96a4a6e122b1d5bbaa3fac117b9cbb46c7e38f12967386b57e2f79d11d23f01cee77ceed8544e6d52c78bd33c81bd366c8673b68955ddbd1ade98fe6a9b4e27ae54cd10dda7cd3a4637f44e0ead895ebebdcf0c679f1342745628f104e7",
47+
contents: "cb1462000000000048656c6c6f20576f726c64203a29",
3848
}

openpgp/packet/symmetric_key_encrypted_test.go

+112-19
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
mathrand "math/rand"
1313
"testing"
1414

15+
"github.com/ProtonMail/go-crypto/openpgp/errors"
1516
"github.com/ProtonMail/go-crypto/openpgp/s2k"
1617
)
1718

@@ -20,25 +21,12 @@ const maxPassLen = 64
2021
// Tests against RFC vectors
2122
func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) {
2223
for _, testCase := range keyAndIpePackets() {
23-
// Key
24-
buf := readerFromHex(testCase.packets)
25-
packet, err := Read(buf)
26-
if err != nil {
27-
t.Fatalf("failed to read SymmetricKeyEncrypted: %s", err)
28-
}
29-
ske, ok := packet.(*SymmetricKeyEncrypted)
30-
if !ok {
31-
t.Fatal("didn't find SymmetricKeyEncrypted packet")
32-
}
33-
// Decrypt key
34-
key, cipherFunc, err := ske.Decrypt([]byte(testCase.password))
35-
if err != nil {
36-
t.Fatal(err)
37-
}
38-
packet, err = Read(buf)
39-
if err != nil {
40-
t.Fatalf("failed to read SymmetricallyEncrypted: %s", err)
41-
}
24+
// Read and verify the key packet
25+
ske, dataPacket := readSymmetricKeyEncrypted(t, testCase.packets)
26+
key, cipherFunc := decryptSymmetricKey(t, ske, []byte(testCase.password))
27+
28+
packet := readSymmetricallyEncrypted(t, dataPacket)
29+
4230
// Decrypt contents
4331
var edp EncryptedDataPacket
4432
switch p := packet.(type) {
@@ -49,6 +37,7 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) {
4937
default:
5038
t.Fatal("no integrity protected packet")
5139
}
40+
5241
r, err := edp.Decrypt(cipherFunc, key)
5342
if err != nil {
5443
t.Fatal(err)
@@ -66,6 +55,110 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) {
6655
}
6756
}
6857

58+
func TestTagVerificationError(t *testing.T) {
59+
for _, testCase := range keyAndIpePackets() {
60+
ske, dataPacket := readSymmetricKeyEncrypted(t, testCase.packets)
61+
key, cipherFunc := decryptSymmetricKey(t, ske, []byte(testCase.password))
62+
63+
// Corrupt chunk
64+
tmp := make([]byte, len(dataPacket))
65+
copy(tmp, dataPacket)
66+
tmp[38] += 1
67+
packet := readSymmetricallyEncrypted(t, tmp)
68+
// Decrypt contents and check integrity
69+
checkIntegrityError(t, packet, cipherFunc, key)
70+
71+
// Corrupt final tag or mdc
72+
dataPacket[len(dataPacket)-1] += 1
73+
packet = readSymmetricallyEncrypted(t, dataPacket)
74+
// Decrypt contents and check integrity
75+
checkIntegrityError(t, packet, cipherFunc, key)
76+
77+
if len(testCase.faultyDataPacket) > 0 {
78+
dataPacket, err := hex.DecodeString(testCase.faultyDataPacket)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
packet = readSymmetricallyEncrypted(t, dataPacket)
83+
// Decrypt contents and check integrity
84+
checkIntegrityError(t, packet, cipherFunc, key)
85+
}
86+
}
87+
}
88+
89+
func readSymmetricKeyEncrypted(t *testing.T, packetHex string) (*SymmetricKeyEncrypted, []byte) {
90+
t.Helper()
91+
92+
buf := readerFromHex(packetHex)
93+
packet, err := Read(buf)
94+
if err != nil {
95+
t.Fatalf("failed to read SymmetricKeyEncrypted: %s", err)
96+
}
97+
98+
ske, ok := packet.(*SymmetricKeyEncrypted)
99+
if !ok {
100+
t.Fatal("didn't find SymmetricKeyEncrypted packet")
101+
}
102+
103+
dataPacket, err := io.ReadAll(buf)
104+
if err != nil {
105+
t.Fatalf("failed to read data packet: %s", err)
106+
}
107+
return ske, dataPacket
108+
}
109+
110+
func decryptSymmetricKey(t *testing.T, ske *SymmetricKeyEncrypted, password []byte) ([]byte, CipherFunction) {
111+
t.Helper()
112+
113+
key, cipherFunc, err := ske.Decrypt(password)
114+
if err != nil {
115+
t.Fatalf("failed to decrypt symmetric key: %s", err)
116+
}
117+
118+
return key, cipherFunc
119+
}
120+
121+
func readSymmetricallyEncrypted(t *testing.T, dataPacket []byte) Packet {
122+
t.Helper()
123+
packet, err := Read(bytes.NewReader(dataPacket))
124+
if err != nil {
125+
t.Fatalf("failed to read SymmetricallyEncrypted: %s", err)
126+
}
127+
return packet
128+
}
129+
130+
func checkIntegrityError(t *testing.T, packet Packet, cipherFunc CipherFunction, key []byte) {
131+
t.Helper()
132+
133+
switch p := packet.(type) {
134+
case *SymmetricallyEncrypted:
135+
edp := p
136+
data, err := edp.Decrypt(cipherFunc, key)
137+
if err != nil {
138+
t.Fatal(err)
139+
}
140+
141+
_, err = io.ReadAll(data)
142+
if err == nil {
143+
err = data.Close()
144+
}
145+
if err != nil {
146+
if edp.Version == 1 && err != errors.ErrMDCHashMismatch {
147+
t.Fatalf("no integrity error (expected MDC hash mismatch)")
148+
}
149+
if edp.Version == 2 && err != errors.ErrAEADTagVerification {
150+
t.Fatalf("no integrity error (expected AEAD tag verification failure)")
151+
}
152+
} else {
153+
t.Fatalf("no error (expected integrity check failure)")
154+
}
155+
case *AEADEncrypted:
156+
return
157+
default:
158+
t.Fatal("no integrity protected packet found")
159+
}
160+
}
161+
69162
func TestSerializeSymmetricKeyEncryptedV6RandomizeSlow(t *testing.T) {
70163
ciphers := map[string]CipherFunction{
71164
"AES128": CipherAES128,

openpgp/packet/symmetrically_encrypted_mdc.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ const mdcPacketTagByte = byte(0x80) | 0x40 | 19
148148

149149
func (ser *seMDCReader) Close() error {
150150
if ser.error {
151-
return errors.ErrMDCMissing
151+
return errors.ErrMDCHashMismatch
152152
}
153153

154154
for !ser.eof {
@@ -159,7 +159,7 @@ func (ser *seMDCReader) Close() error {
159159
break
160160
}
161161
if err != nil {
162-
return errors.ErrMDCMissing
162+
return errors.ErrMDCHashMismatch
163163
}
164164
}
165165

@@ -172,7 +172,7 @@ func (ser *seMDCReader) Close() error {
172172
// The hash already includes the MDC header, but we still check its value
173173
// to confirm encryption correctness
174174
if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size {
175-
return errors.ErrMDCMissing
175+
return errors.ErrMDCHashMismatch
176176
}
177177
return nil
178178
}

openpgp/read.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ FindKey:
233233
}
234234
mdFinal, sensitiveParsingErr := readSignedMessage(packets, md, keyring, config)
235235
if sensitiveParsingErr != nil {
236-
return nil, errors.StructuralError("parsing error")
236+
return nil, errors.HandleSensitiveParsingError(sensitiveParsingErr, md.decrypted != nil)
237237
}
238238
return mdFinal, nil
239239
}
@@ -368,7 +368,7 @@ func (cr *checkReader) Read(buf []byte) (int, error) {
368368
}
369369

370370
if sensitiveParsingError != nil {
371-
return n, errors.StructuralError("parsing error")
371+
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
372372
}
373373

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

395+
readsDecryptedData := scr.md.decrypted != nil
395396
if sensitiveParsingError == io.EOF {
396397
var p packet.Packet
397398
var readError error
@@ -434,16 +435,15 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
434435
// unsigned hash of its own. In order to check this we need to
435436
// close that Reader.
436437
if scr.md.decrypted != nil {
437-
mdcErr := scr.md.decrypted.Close()
438-
if mdcErr != nil {
439-
return n, mdcErr
438+
if sensitiveParsingError := scr.md.decrypted.Close(); sensitiveParsingError != nil {
439+
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
440440
}
441441
}
442442
return n, io.EOF
443443
}
444444

445445
if sensitiveParsingError != nil {
446-
return n, errors.StructuralError("parsing error")
446+
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, readsDecryptedData)
447447
}
448448

449449
return n, nil

openpgp/read_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ func TestCorruptedMessageInvalidSigHeader(t *testing.T) {
819819
promptFunc := func(keys []Key, symmetric bool) ([]byte, error) {
820820
return passphrase, nil
821821
}
822-
const expectedErr string = "openpgp: invalid data: parsing error"
822+
const expectedErr string = "openpgp: decryption with session key failed: parsing error"
823823
_, observedErr := ReadMessage(raw.Body, nil, promptFunc, nil)
824824
if observedErr.Error() != expectedErr {
825825
t.Errorf("Expected error '%s', but got error '%s'", expectedErr, observedErr)
@@ -833,7 +833,7 @@ func TestCorruptedMessageWrongLength(t *testing.T) {
833833
promptFunc := func(keys []Key, symmetric bool) ([]byte, error) {
834834
return passphrase, nil
835835
}
836-
const expectedErr string = "openpgp: invalid data: parsing error"
836+
const expectedErr string = "openpgp: decryption with session key failed: parsing error"
837837

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

0 commit comments

Comments
 (0)