From 91ae818efb43688eeeccf0f61414ecf6da098fdf Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Mon, 28 Aug 2023 10:11:27 +0200 Subject: [PATCH] Add decryption quick check for SEIPDv2 --- go.mod | 2 +- go.sum | 4 +- helper/decrypt_check.go | 158 +++++++++++++++++++++++++++++------ helper/decrypt_check_test.go | 80 +++++++++++------- 4 files changed, 188 insertions(+), 56 deletions(-) diff --git a/go.mod b/go.mod index fdd3515a..e4d6a74d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ProtonMail/gopenpgp/v2 go 1.15 require ( - github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f github.com/davecgh/go-spew v1.1.1 // indirect github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 89bf4e71..a2099c5d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= diff --git a/helper/decrypt_check.go b/helper/decrypt_check.go index eeff1089..521d9dad 100644 --- a/helper/decrypt_check.go +++ b/helper/decrypt_check.go @@ -4,14 +4,21 @@ import ( "bytes" "crypto/aes" "crypto/cipher" + "crypto/sha256" + "encoding/binary" "io" + "io/ioutil" + "github.com/ProtonMail/go-crypto/eax" + "github.com/ProtonMail/go-crypto/ocb" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/pkg/errors" + "golang.org/x/crypto/hkdf" ) -const AES_BLOCK_SIZE = 16 +const aesBlockSize = 16 +const copyChunkSize = 1024 func supported(cipher packet.CipherFunction) bool { switch cipher { @@ -26,7 +33,7 @@ func supported(cipher packet.CipherFunction) bool { func blockSize(cipher packet.CipherFunction) int { switch cipher { case packet.CipherAES128, packet.CipherAES192, packet.CipherAES256: - return AES_BLOCK_SIZE + return aesBlockSize case packet.CipherCAST5, packet.Cipher3DES: return 0 } @@ -43,44 +50,147 @@ func blockCipher(cipher packet.CipherFunction, key []byte) (cipher.Block, error) return nil, errors.New("gopenpgp: unknown cipher") } -// QuickCheckDecryptReader checks with high probability if the provided session key -// can decrypt a data packet given its 24 byte long prefix. -// The method reads up to but not exactly 24 bytes from the prefixReader. -// NOTE: Only works for SEIPDv1 packets with AES. -func QuickCheckDecryptReader(sessionKey *crypto.SessionKey, prefixReader crypto.Reader) (bool, error) { - algo, err := sessionKey.GetCipherFunc() +func aeadMode(mode packet.AEADMode, block cipher.Block) (alg cipher.AEAD, err error) { + switch mode { + case packet.AEADModeEAX: + alg, err = eax.NewEAX(block) + case packet.AEADModeOCB: + alg, err = ocb.NewOCB(block) + case packet.AEADModeGCM: + alg, err = cipher.NewGCM(block) + } if err != nil { - return false, errors.New("gopenpgp: cipher algorithm not found") + return nil, err } - if !supported(algo) { - return false, errors.New("gopenpgp: cipher not supported for quick check") + return +} + +func getSymmetricallyEncryptedAeadInstance(c packet.CipherFunction, mode packet.AEADMode, inputKey, salt, associatedData []byte) (aead cipher.AEAD, nonce []byte, err error) { + hkdfReader := hkdf.New(sha256.New, inputKey, salt, associatedData) + encryptionKey := make([]byte, c.KeySize()) + _, _ = io.ReadFull(hkdfReader, encryptionKey) + nonce = make([]byte, mode.IvLength()-8) + _, _ = io.ReadFull(hkdfReader, nonce) + blockCipher, err := blockCipher(c, encryptionKey) + if err != nil { + return } - packetParser := packet.NewReader(prefixReader) - _, err = packetParser.Next() + aead, err = aeadMode(mode, blockCipher) + return +} + +func checkSEIPDv1Decrypt( + sessionKey *crypto.SessionKey, + prefixReader crypto.Reader, +) (bool, error) { + cipher, err := sessionKey.GetCipherFunc() if err != nil { - return false, errors.New("gopenpgp: failed to parse packet prefix") + return false, errors.New("gopenpgp: cipher algorithm not found") + } + if !supported(cipher) { + return false, errors.New("gopenpgp: cipher not supported for quick check") } - blockSize := blockSize(algo) + blockSize := blockSize(cipher) encryptedData := make([]byte, blockSize+2) - _, err = io.ReadFull(prefixReader, encryptedData) - if err != nil { + if _, err := io.ReadFull(prefixReader, encryptedData); err != nil { return false, errors.New("gopenpgp: prefix is too short to check") } - blockCipher, err := blockCipher(algo, sessionKey.Key) + blockCipher, err := blockCipher(cipher, sessionKey.Key) if err != nil { return false, errors.New("gopenpgp: failed to initialize the cipher") } - _ = packet.NewOCFBDecrypter(blockCipher, encryptedData, packet.OCFBNoResync) + packet.NewOCFBDecrypter(blockCipher, encryptedData, packet.OCFBNoResync) return encryptedData[blockSize-2] == encryptedData[blockSize] && encryptedData[blockSize-1] == encryptedData[blockSize+1], nil } +func checkSEIPDv2Decrypt( + sessionKey *crypto.SessionKey, + symPacket *packet.SymmetricallyEncrypted, +) (bool, error) { + if !supported(symPacket.Cipher) { + return false, errors.New("gopenpgp: cipher not supported for quick check") + } + buffer := new(bytes.Buffer) + aeadTagLength := symPacket.Mode.TagLength() + reader := symPacket.Contents + var totalDataRead int64 + for { + // Read up to copyChunkSize bytes into the buffer + written, err := io.CopyN(buffer, reader, copyChunkSize-int64(buffer.Len())) + totalDataRead += written + // Discard all data from the buffer except last tag length bytes + _, _ = io.CopyN(ioutil.Discard, buffer, int64(buffer.Len())-int64(aeadTagLength)) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return false, err + } + } + totalDataRead -= int64(aeadTagLength) + aeadChunkSize := int64(1 << (int64(symPacket.ChunkSizeByte) + 6)) + aeadChunkAndTagLength := aeadChunkSize + int64(aeadTagLength) + numberOfChunks := totalDataRead / aeadChunkAndTagLength + if totalDataRead%aeadChunkAndTagLength != 0 { + numberOfChunks += 1 + } + plaintextLength := totalDataRead - numberOfChunks*int64(aeadTagLength) + + var amountBytes [8]byte + var index [8]byte + binary.BigEndian.PutUint64(amountBytes[:], uint64(plaintextLength)) + binary.BigEndian.PutUint64(index[:], uint64(numberOfChunks)) + + adata := []byte{ + 0xD2, + byte(symPacket.Version), + byte(symPacket.Cipher), + byte(symPacket.Mode), + symPacket.ChunkSizeByte, + } + + aead, nonce, err := getSymmetricallyEncryptedAeadInstance(symPacket.Cipher, symPacket.Mode, sessionKey.Key, symPacket.Salt[:], adata) + if err != nil { + return false, errors.New("gopenpgp: failed to instantiate aead cipher") + } + adata = append(adata, amountBytes[:]...) + nonce = append(nonce, index[:]...) + authenticationTag := buffer.Bytes() + _, err = aead.Open(nil, nonce, authenticationTag, adata) + return err == nil, nil +} + +// QuickCheckDecryptReader checks with high probability if the provided session key +// can decrypt a data packet. +// For SEIPDv1 it only uses a 24 byte long prefix of the data packet. +// Thus, the function reads up to but not exactly 24 bytes from the prefixReader. +// For SEIPDv2 the function reads the whole data packet. +// NOTE: the function only works for data packets encrypted with AES. +func QuickCheckDecryptReader(sessionKey *crypto.SessionKey, dataPacketReader crypto.Reader) (bool, error) { + packetParser := packet.NewReader(dataPacketReader) + p, err := packetParser.Next() + if err != nil { + return false, errors.New("gopenpgp: failed to parse packet prefix") + } + if symPacket, ok := p.(*packet.SymmetricallyEncrypted); ok { + switch symPacket.Version { + case 1: + return checkSEIPDv1Decrypt(sessionKey, dataPacketReader) + case 2: + return checkSEIPDv2Decrypt(sessionKey, symPacket) + } + } + return false, errors.New("gopenpgp: no SEIPD packet found") +} + // QuickCheckDecrypt checks with high probability if the provided session key -// can decrypt the encrypted data packet given its 24 byte long prefix. -// The method only considers the first 24 bytes of the prefix slice (prefix[:24]). -// NOTE: Only works for SEIPDv1 packets with AES. -func QuickCheckDecrypt(sessionKey *crypto.SessionKey, prefix []byte) (bool, error) { - return QuickCheckDecryptReader(sessionKey, bytes.NewReader(prefix)) +// can decrypt the data packet. +// For SEIPDv1 it only uses a 24 byte long prefix of the data packet (dataPacket[:24]). +// For SEIPDv2 the function reads the whole data packet. +// NOTE: the function only works for data packets encrypted with AES. +func QuickCheckDecrypt(sessionKey *crypto.SessionKey, dataPacket []byte) (bool, error) { + return QuickCheckDecryptReader(sessionKey, bytes.NewReader(dataPacket)) } diff --git a/helper/decrypt_check_test.go b/helper/decrypt_check_test.go index f7341114..62a29ff6 100644 --- a/helper/decrypt_check_test.go +++ b/helper/decrypt_check_test.go @@ -7,37 +7,59 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" ) -const testQuickCheckSessionKey = `038c9cb9d408074e36bac22c6b90973082f86e5b01f38b787da3927000365a81` -const testQuickCheckSessionKeyAlg = "aes256" -const testQuickCheckDataPacket = `d2540152ab2518950f282d98d901eb93c00fb55a3bb30b3b517d6a356f57884bac6963060ebb167ffc3296e5e99ec058aeff5003a4784a0734a62861ae56d2921b9b790d50586cd21cad45e2d84ac93fb5d8af2ce6c5` - func TestCheckDecrypt(t *testing.T) { - sessionKeyData, err := hex.DecodeString(testQuickCheckSessionKey) - if err != nil { - t.Error(err) - } - dataPacket, err := hex.DecodeString(testQuickCheckDataPacket) - if err != nil { - t.Error(err) - } - sessionKey := &crypto.SessionKey{ - Key: sessionKeyData, - Algo: testQuickCheckSessionKeyAlg, - } - ok, err := QuickCheckDecrypt(sessionKey, dataPacket[:22]) - if err != nil { - t.Error(err) - } - if !ok { - t.Error("should be able to decrypt") + tests := map[string]struct { + testQuickCheckSessionKey string + testQuickCheckSessionKeyAlg string + testQuickCheckDataPacket string + }{ + "SEIPDv1": { + testQuickCheckSessionKey: `038c9cb9d408074e36bac22c6b90973082f86e5b01f38b787da3927000365a81`, + testQuickCheckSessionKeyAlg: "aes256", + testQuickCheckDataPacket: `d2540152ab2518950f282d98d901eb93c00fb55a3bb30b3b517d6a356f57884bac6963060ebb167ffc3296e5e99ec058aeff5003a4784a0734a62861ae56d2921b9b790d50586cd21cad45e2d84ac93fb5d8af2ce6c5`, + }, + "SEIPDv2": { + testQuickCheckSessionKey: `52d777d38bb5d01e84b9b2881f0fb8e7e7cd2dbace86cb4d258c61c1b796f334`, + testQuickCheckSessionKeyAlg: "aes256", + testQuickCheckDataPacket: `d26f0209020c7725b56eb4aa8032bb8583003d6491e0867dd8f1b74900e8d1c173f46da63c2ec75c89e259aaccbe51ae95c8ac3e950d5045bfca4fce33faa8cf22d577a443b1a49c168d080356691a8953a322c87ec939664b8f406fe4ecbfd8c93610862da36cc815e2d5e919aefe07c5`, + }, + "SEIPDv2_large": { + testQuickCheckSessionKey: `bf910864856e7bcaeabd82edc27fac687af1dd166b779028c3bbaefd574156d4`, + testQuickCheckSessionKeyAlg: "aes256", + testQuickCheckDataPacket: `d2ea0209020cc3d915192d75065eb5da4ee2f2ce1da3ce441754eae4f48a3d3fa7e495cf1b1f5fcb3e2784ded10f5bc691b151fda867406d8f159065df28db844bc548d2195958ea2412ec50bdea39343ad4efe3607d48937bd98c2b7c2695dbe9fe3f7f6a6e67be6491dbfaa4272cd6a4d0387f71ec78783133968793631d305fedc5776e17bff413b8f9c17e5d55e94da1fd735a7bb6b3a4880f8541e3efa5969c220cf609fe3ed0d75ef83a7819ff542eafe596ccc0867bf70dc98e666e36016e119882f34fb950594040e2fd03096bb11c571d87bc4d08f9d10903b4c46dd9afd26724695bdb9e75e948d749c473e700c17f198c345ddac94c48438d1a3ed643483524361a96d79ead8fe3ae3f0015fdca0c82bd5e7f9c06c4efe16f26b0bf89807d04ee27f55eda2a10e0f09af48a2a740b8f82aae14cacd17183fbc64cdbac102b21c6d89470e0f5bf0073ffc48871600530af2de36a93545004fb445700fe0c7add0756247d1457ff60e3de48ce551be7ee1da0b3b8ef996188a8be304213e59a95b33d4f95d33a923e93dce3a287c35b8e9dd01b0acded222666bb20d6b2f50eaf906b4a74f09e3bc4126da5589b0044425e068daddffab50633fe3c1bb29778faaae5e54d4b4e779d94ff023ff5eb8de12510fff2483ec3e51ca92dd07eb499a5ec32bd1033195bad2c944d76c2d01c9b27c1497be830a7b389e1cb1b1fdabfb2ec35638d83502c8b07bc9fb104b16ffd328b58c002ac758170aa42f63f77d83deda1018677621b8da0300930668578dec42d048aa79dba7d83d9e6516efe10fdb6e87da06c72ad5566b7e70d510d671dc21b5669ec1144c53822fc3c22e76623ed872560b2b374c204abb410478cdaed169f35b78889785d86d46b84fe50a73ef89ae237439e82b59fac01282b8ecdac63ae251d1334e7f97be83ffceadf347b1fe6bcfcd5d06cf73cdb27191ba5e9c6aea040486ef7cff3565985e50639a7defce695af40a5350f1d084d58618488075a4122e64910f103498fc3f2ccf8d37d48ddd61fca3f7be4e5e88549f53b94bfb3613a88a77549ada595ea041fffc5e6aae30bdf4a7323965cd6fe69f3abf9eb7380e0cceaed21fe52f5308dc762837bdccebaffa82910db071507ee47bb1b92295c6fde0e16e3fd6c407f35ff1c973e4de4217fc33424e22ea228a478ff3b35eabb1245732e423263ca890f3c3ca063846f69390ec7790f7f7af2341b003065750f2fc9859de92104ce1d8f2c178bab4745153685a1c86cc3fe751613af9ac8285632bf5db647b54300031be92b8725efb9d3469ddcff3fbc1570aebde2d8eed13ca08680b2120faae59b30a4b768a6b5f1944a8e482576fcdf629eb7a49c69e1d17af189f9ef18c3944def6e503e0fb02c6e7cbda9144a71c5238e7795ae7c1d5c9d6453ee3de62aab60bf7bad901de03d8eb05d6be446206fa4e65d6873177195322bd032ce1d64f3f20d864e73cb2e26c0e49aa84aa20a130d1dcfe27592956e69c9b7cb5088c9791f93c13b3cbfbd8073c137db6ba008cbadd29100839198cd3b25f58dd2e9734336cb06bac377b35451cb44a88a7675913ba92c7055fb9aecdd2c68428d81f7616d7a16bce58e23e03d4b893c6bb182fbae575b6df6e38180b29932a9f8c2d8231edf25c260edc1e90417ead711620ab872`, + }, } + for name, data := range tests { + testData := data + t.Run(name, func(t *testing.T) { + sessionKeyData, err := hex.DecodeString(testData.testQuickCheckSessionKey) + if err != nil { + t.Error(err) + } + dataPacket, err := hex.DecodeString(testData.testQuickCheckDataPacket) + if err != nil { + t.Error(err) + } + sessionKey := &crypto.SessionKey{ + Key: sessionKeyData, + Algo: testData.testQuickCheckSessionKeyAlg, + } + ok, err := QuickCheckDecrypt(sessionKey, dataPacket) + if err != nil { + t.Error(err) + } + if !ok { + t.Error("should be able to decrypt") + } - sessionKey.Key[0] += 1 - ok, err = QuickCheckDecrypt(sessionKey, dataPacket[:22]) - if err != nil { - t.Error(err) - } - if ok { - t.Error("should no be able to decrypt") + sessionKey.Key[0] += 1 + ok, err = QuickCheckDecrypt(sessionKey, dataPacket) + if err != nil { + t.Error(err) + } + if ok { + t.Error("should no be able to decrypt") + } + }) } }