Skip to content

Commit 91ae818

Browse files
committed
Add decryption quick check for SEIPDv2
1 parent 987923f commit 91ae818

File tree

4 files changed

+188
-56
lines changed

4 files changed

+188
-56
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/ProtonMail/gopenpgp/v2
33
go 1.15
44

55
require (
6-
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95
6+
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371
77
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f
88
github.com/davecgh/go-spew v1.1.1 // indirect
99
github.com/pkg/errors v0.9.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
2-
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
1+
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
2+
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
33
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
44
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
55
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=

helper/decrypt_check.go

Lines changed: 134 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import (
44
"bytes"
55
"crypto/aes"
66
"crypto/cipher"
7+
"crypto/sha256"
8+
"encoding/binary"
79
"io"
10+
"io/ioutil"
811

12+
"github.com/ProtonMail/go-crypto/eax"
13+
"github.com/ProtonMail/go-crypto/ocb"
914
"github.com/ProtonMail/go-crypto/openpgp/packet"
1015
"github.com/ProtonMail/gopenpgp/v2/crypto"
1116
"github.com/pkg/errors"
17+
"golang.org/x/crypto/hkdf"
1218
)
1319

14-
const AES_BLOCK_SIZE = 16
20+
const aesBlockSize = 16
21+
const copyChunkSize = 1024
1522

1623
func supported(cipher packet.CipherFunction) bool {
1724
switch cipher {
@@ -26,7 +33,7 @@ func supported(cipher packet.CipherFunction) bool {
2633
func blockSize(cipher packet.CipherFunction) int {
2734
switch cipher {
2835
case packet.CipherAES128, packet.CipherAES192, packet.CipherAES256:
29-
return AES_BLOCK_SIZE
36+
return aesBlockSize
3037
case packet.CipherCAST5, packet.Cipher3DES:
3138
return 0
3239
}
@@ -43,44 +50,147 @@ func blockCipher(cipher packet.CipherFunction, key []byte) (cipher.Block, error)
4350
return nil, errors.New("gopenpgp: unknown cipher")
4451
}
4552

46-
// QuickCheckDecryptReader checks with high probability if the provided session key
47-
// can decrypt a data packet given its 24 byte long prefix.
48-
// The method reads up to but not exactly 24 bytes from the prefixReader.
49-
// NOTE: Only works for SEIPDv1 packets with AES.
50-
func QuickCheckDecryptReader(sessionKey *crypto.SessionKey, prefixReader crypto.Reader) (bool, error) {
51-
algo, err := sessionKey.GetCipherFunc()
53+
func aeadMode(mode packet.AEADMode, block cipher.Block) (alg cipher.AEAD, err error) {
54+
switch mode {
55+
case packet.AEADModeEAX:
56+
alg, err = eax.NewEAX(block)
57+
case packet.AEADModeOCB:
58+
alg, err = ocb.NewOCB(block)
59+
case packet.AEADModeGCM:
60+
alg, err = cipher.NewGCM(block)
61+
}
5262
if err != nil {
53-
return false, errors.New("gopenpgp: cipher algorithm not found")
63+
return nil, err
5464
}
55-
if !supported(algo) {
56-
return false, errors.New("gopenpgp: cipher not supported for quick check")
65+
return
66+
}
67+
68+
func getSymmetricallyEncryptedAeadInstance(c packet.CipherFunction, mode packet.AEADMode, inputKey, salt, associatedData []byte) (aead cipher.AEAD, nonce []byte, err error) {
69+
hkdfReader := hkdf.New(sha256.New, inputKey, salt, associatedData)
70+
encryptionKey := make([]byte, c.KeySize())
71+
_, _ = io.ReadFull(hkdfReader, encryptionKey)
72+
nonce = make([]byte, mode.IvLength()-8)
73+
_, _ = io.ReadFull(hkdfReader, nonce)
74+
blockCipher, err := blockCipher(c, encryptionKey)
75+
if err != nil {
76+
return
5777
}
58-
packetParser := packet.NewReader(prefixReader)
59-
_, err = packetParser.Next()
78+
aead, err = aeadMode(mode, blockCipher)
79+
return
80+
}
81+
82+
func checkSEIPDv1Decrypt(
83+
sessionKey *crypto.SessionKey,
84+
prefixReader crypto.Reader,
85+
) (bool, error) {
86+
cipher, err := sessionKey.GetCipherFunc()
6087
if err != nil {
61-
return false, errors.New("gopenpgp: failed to parse packet prefix")
88+
return false, errors.New("gopenpgp: cipher algorithm not found")
89+
}
90+
if !supported(cipher) {
91+
return false, errors.New("gopenpgp: cipher not supported for quick check")
6292
}
6393

64-
blockSize := blockSize(algo)
94+
blockSize := blockSize(cipher)
6595
encryptedData := make([]byte, blockSize+2)
66-
_, err = io.ReadFull(prefixReader, encryptedData)
67-
if err != nil {
96+
if _, err := io.ReadFull(prefixReader, encryptedData); err != nil {
6897
return false, errors.New("gopenpgp: prefix is too short to check")
6998
}
7099

71-
blockCipher, err := blockCipher(algo, sessionKey.Key)
100+
blockCipher, err := blockCipher(cipher, sessionKey.Key)
72101
if err != nil {
73102
return false, errors.New("gopenpgp: failed to initialize the cipher")
74103
}
75-
_ = packet.NewOCFBDecrypter(blockCipher, encryptedData, packet.OCFBNoResync)
104+
packet.NewOCFBDecrypter(blockCipher, encryptedData, packet.OCFBNoResync)
76105
return encryptedData[blockSize-2] == encryptedData[blockSize] &&
77106
encryptedData[blockSize-1] == encryptedData[blockSize+1], nil
78107
}
79108

109+
func checkSEIPDv2Decrypt(
110+
sessionKey *crypto.SessionKey,
111+
symPacket *packet.SymmetricallyEncrypted,
112+
) (bool, error) {
113+
if !supported(symPacket.Cipher) {
114+
return false, errors.New("gopenpgp: cipher not supported for quick check")
115+
}
116+
buffer := new(bytes.Buffer)
117+
aeadTagLength := symPacket.Mode.TagLength()
118+
reader := symPacket.Contents
119+
var totalDataRead int64
120+
for {
121+
// Read up to copyChunkSize bytes into the buffer
122+
written, err := io.CopyN(buffer, reader, copyChunkSize-int64(buffer.Len()))
123+
totalDataRead += written
124+
// Discard all data from the buffer except last tag length bytes
125+
_, _ = io.CopyN(ioutil.Discard, buffer, int64(buffer.Len())-int64(aeadTagLength))
126+
if errors.Is(err, io.EOF) {
127+
break
128+
}
129+
if err != nil {
130+
return false, err
131+
}
132+
}
133+
totalDataRead -= int64(aeadTagLength)
134+
aeadChunkSize := int64(1 << (int64(symPacket.ChunkSizeByte) + 6))
135+
aeadChunkAndTagLength := aeadChunkSize + int64(aeadTagLength)
136+
numberOfChunks := totalDataRead / aeadChunkAndTagLength
137+
if totalDataRead%aeadChunkAndTagLength != 0 {
138+
numberOfChunks += 1
139+
}
140+
plaintextLength := totalDataRead - numberOfChunks*int64(aeadTagLength)
141+
142+
var amountBytes [8]byte
143+
var index [8]byte
144+
binary.BigEndian.PutUint64(amountBytes[:], uint64(plaintextLength))
145+
binary.BigEndian.PutUint64(index[:], uint64(numberOfChunks))
146+
147+
adata := []byte{
148+
0xD2,
149+
byte(symPacket.Version),
150+
byte(symPacket.Cipher),
151+
byte(symPacket.Mode),
152+
symPacket.ChunkSizeByte,
153+
}
154+
155+
aead, nonce, err := getSymmetricallyEncryptedAeadInstance(symPacket.Cipher, symPacket.Mode, sessionKey.Key, symPacket.Salt[:], adata)
156+
if err != nil {
157+
return false, errors.New("gopenpgp: failed to instantiate aead cipher")
158+
}
159+
adata = append(adata, amountBytes[:]...)
160+
nonce = append(nonce, index[:]...)
161+
authenticationTag := buffer.Bytes()
162+
_, err = aead.Open(nil, nonce, authenticationTag, adata)
163+
return err == nil, nil
164+
}
165+
166+
// QuickCheckDecryptReader checks with high probability if the provided session key
167+
// can decrypt a data packet.
168+
// For SEIPDv1 it only uses a 24 byte long prefix of the data packet.
169+
// Thus, the function reads up to but not exactly 24 bytes from the prefixReader.
170+
// For SEIPDv2 the function reads the whole data packet.
171+
// NOTE: the function only works for data packets encrypted with AES.
172+
func QuickCheckDecryptReader(sessionKey *crypto.SessionKey, dataPacketReader crypto.Reader) (bool, error) {
173+
packetParser := packet.NewReader(dataPacketReader)
174+
p, err := packetParser.Next()
175+
if err != nil {
176+
return false, errors.New("gopenpgp: failed to parse packet prefix")
177+
}
178+
if symPacket, ok := p.(*packet.SymmetricallyEncrypted); ok {
179+
switch symPacket.Version {
180+
case 1:
181+
return checkSEIPDv1Decrypt(sessionKey, dataPacketReader)
182+
case 2:
183+
return checkSEIPDv2Decrypt(sessionKey, symPacket)
184+
}
185+
}
186+
return false, errors.New("gopenpgp: no SEIPD packet found")
187+
}
188+
80189
// QuickCheckDecrypt checks with high probability if the provided session key
81-
// can decrypt the encrypted data packet given its 24 byte long prefix.
82-
// The method only considers the first 24 bytes of the prefix slice (prefix[:24]).
83-
// NOTE: Only works for SEIPDv1 packets with AES.
84-
func QuickCheckDecrypt(sessionKey *crypto.SessionKey, prefix []byte) (bool, error) {
85-
return QuickCheckDecryptReader(sessionKey, bytes.NewReader(prefix))
190+
// can decrypt the data packet.
191+
// For SEIPDv1 it only uses a 24 byte long prefix of the data packet (dataPacket[:24]).
192+
// For SEIPDv2 the function reads the whole data packet.
193+
// NOTE: the function only works for data packets encrypted with AES.
194+
func QuickCheckDecrypt(sessionKey *crypto.SessionKey, dataPacket []byte) (bool, error) {
195+
return QuickCheckDecryptReader(sessionKey, bytes.NewReader(dataPacket))
86196
}

helper/decrypt_check_test.go

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,59 @@ import (
77
"github.com/ProtonMail/gopenpgp/v2/crypto"
88
)
99

10-
const testQuickCheckSessionKey = `038c9cb9d408074e36bac22c6b90973082f86e5b01f38b787da3927000365a81`
11-
const testQuickCheckSessionKeyAlg = "aes256"
12-
const testQuickCheckDataPacket = `d2540152ab2518950f282d98d901eb93c00fb55a3bb30b3b517d6a356f57884bac6963060ebb167ffc3296e5e99ec058aeff5003a4784a0734a62861ae56d2921b9b790d50586cd21cad45e2d84ac93fb5d8af2ce6c5`
13-
1410
func TestCheckDecrypt(t *testing.T) {
15-
sessionKeyData, err := hex.DecodeString(testQuickCheckSessionKey)
16-
if err != nil {
17-
t.Error(err)
18-
}
19-
dataPacket, err := hex.DecodeString(testQuickCheckDataPacket)
20-
if err != nil {
21-
t.Error(err)
22-
}
23-
sessionKey := &crypto.SessionKey{
24-
Key: sessionKeyData,
25-
Algo: testQuickCheckSessionKeyAlg,
26-
}
27-
ok, err := QuickCheckDecrypt(sessionKey, dataPacket[:22])
28-
if err != nil {
29-
t.Error(err)
30-
}
31-
if !ok {
32-
t.Error("should be able to decrypt")
11+
tests := map[string]struct {
12+
testQuickCheckSessionKey string
13+
testQuickCheckSessionKeyAlg string
14+
testQuickCheckDataPacket string
15+
}{
16+
"SEIPDv1": {
17+
testQuickCheckSessionKey: `038c9cb9d408074e36bac22c6b90973082f86e5b01f38b787da3927000365a81`,
18+
testQuickCheckSessionKeyAlg: "aes256",
19+
testQuickCheckDataPacket: `d2540152ab2518950f282d98d901eb93c00fb55a3bb30b3b517d6a356f57884bac6963060ebb167ffc3296e5e99ec058aeff5003a4784a0734a62861ae56d2921b9b790d50586cd21cad45e2d84ac93fb5d8af2ce6c5`,
20+
},
21+
"SEIPDv2": {
22+
testQuickCheckSessionKey: `52d777d38bb5d01e84b9b2881f0fb8e7e7cd2dbace86cb4d258c61c1b796f334`,
23+
testQuickCheckSessionKeyAlg: "aes256",
24+
testQuickCheckDataPacket: `d26f0209020c7725b56eb4aa8032bb8583003d6491e0867dd8f1b74900e8d1c173f46da63c2ec75c89e259aaccbe51ae95c8ac3e950d5045bfca4fce33faa8cf22d577a443b1a49c168d080356691a8953a322c87ec939664b8f406fe4ecbfd8c93610862da36cc815e2d5e919aefe07c5`,
25+
},
26+
"SEIPDv2_large": {
27+
testQuickCheckSessionKey: `bf910864856e7bcaeabd82edc27fac687af1dd166b779028c3bbaefd574156d4`,
28+
testQuickCheckSessionKeyAlg: "aes256",
29+
testQuickCheckDataPacket: `d2ea0209020cc3d915192d75065eb5da4ee2f2ce1da3ce441754eae4f48a3d3fa7e495cf1b1f5fcb3e2784ded10f5bc691b151fda867406d8f159065df28db844bc548d2195958ea2412ec50bdea39343ad4efe3607d48937bd98c2b7c2695dbe9fe3f7f6a6e67be6491dbfaa4272cd6a4d0387f71ec78783133968793631d305fedc5776e17bff413b8f9c17e5d55e94da1fd735a7bb6b3a4880f8541e3efa5969c220cf609fe3ed0d75ef83a7819ff542eafe596ccc0867bf70dc98e666e36016e119882f34fb950594040e2fd03096bb11c571d87bc4d08f9d10903b4c46dd9afd26724695bdb9e75e948d749c473e700c17f198c345ddac94c48438d1a3ed643483524361a96d79ead8fe3ae3f0015fdca0c82bd5e7f9c06c4efe16f26b0bf89807d04ee27f55eda2a10e0f09af48a2a740b8f82aae14cacd17183fbc64cdbac102b21c6d89470e0f5bf0073ffc48871600530af2de36a93545004fb445700fe0c7add0756247d1457ff60e3de48ce551be7ee1da0b3b8ef996188a8be304213e59a95b33d4f95d33a923e93dce3a287c35b8e9dd01b0acded222666bb20d6b2f50eaf906b4a74f09e3bc4126da5589b0044425e068daddffab50633fe3c1bb29778faaae5e54d4b4e779d94ff023ff5eb8de12510fff2483ec3e51ca92dd07eb499a5ec32bd1033195bad2c944d76c2d01c9b27c1497be830a7b389e1cb1b1fdabfb2ec35638d83502c8b07bc9fb104b16ffd328b58c002ac758170aa42f63f77d83deda1018677621b8da0300930668578dec42d048aa79dba7d83d9e6516efe10fdb6e87da06c72ad5566b7e70d510d671dc21b5669ec1144c53822fc3c22e76623ed872560b2b374c204abb410478cdaed169f35b78889785d86d46b84fe50a73ef89ae237439e82b59fac01282b8ecdac63ae251d1334e7f97be83ffceadf347b1fe6bcfcd5d06cf73cdb27191ba5e9c6aea040486ef7cff3565985e50639a7defce695af40a5350f1d084d58618488075a4122e64910f103498fc3f2ccf8d37d48ddd61fca3f7be4e5e88549f53b94bfb3613a88a77549ada595ea041fffc5e6aae30bdf4a7323965cd6fe69f3abf9eb7380e0cceaed21fe52f5308dc762837bdccebaffa82910db071507ee47bb1b92295c6fde0e16e3fd6c407f35ff1c973e4de4217fc33424e22ea228a478ff3b35eabb1245732e423263ca890f3c3ca063846f69390ec7790f7f7af2341b003065750f2fc9859de92104ce1d8f2c178bab4745153685a1c86cc3fe751613af9ac8285632bf5db647b54300031be92b8725efb9d3469ddcff3fbc1570aebde2d8eed13ca08680b2120faae59b30a4b768a6b5f1944a8e482576fcdf629eb7a49c69e1d17af189f9ef18c3944def6e503e0fb02c6e7cbda9144a71c5238e7795ae7c1d5c9d6453ee3de62aab60bf7bad901de03d8eb05d6be446206fa4e65d6873177195322bd032ce1d64f3f20d864e73cb2e26c0e49aa84aa20a130d1dcfe27592956e69c9b7cb5088c9791f93c13b3cbfbd8073c137db6ba008cbadd29100839198cd3b25f58dd2e9734336cb06bac377b35451cb44a88a7675913ba92c7055fb9aecdd2c68428d81f7616d7a16bce58e23e03d4b893c6bb182fbae575b6df6e38180b29932a9f8c2d8231edf25c260edc1e90417ead711620ab872`,
30+
},
3331
}
32+
for name, data := range tests {
33+
testData := data
34+
t.Run(name, func(t *testing.T) {
35+
sessionKeyData, err := hex.DecodeString(testData.testQuickCheckSessionKey)
36+
if err != nil {
37+
t.Error(err)
38+
}
39+
dataPacket, err := hex.DecodeString(testData.testQuickCheckDataPacket)
40+
if err != nil {
41+
t.Error(err)
42+
}
43+
sessionKey := &crypto.SessionKey{
44+
Key: sessionKeyData,
45+
Algo: testData.testQuickCheckSessionKeyAlg,
46+
}
47+
ok, err := QuickCheckDecrypt(sessionKey, dataPacket)
48+
if err != nil {
49+
t.Error(err)
50+
}
51+
if !ok {
52+
t.Error("should be able to decrypt")
53+
}
3454

35-
sessionKey.Key[0] += 1
36-
ok, err = QuickCheckDecrypt(sessionKey, dataPacket[:22])
37-
if err != nil {
38-
t.Error(err)
39-
}
40-
if ok {
41-
t.Error("should no be able to decrypt")
55+
sessionKey.Key[0] += 1
56+
ok, err = QuickCheckDecrypt(sessionKey, dataPacket)
57+
if err != nil {
58+
t.Error(err)
59+
}
60+
if ok {
61+
t.Error("should no be able to decrypt")
62+
}
63+
})
4264
}
4365
}

0 commit comments

Comments
 (0)