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

Add decryption quick check for SEIPDv2 #250

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
158 changes: 134 additions & 24 deletions helper/decrypt_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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))
}
80 changes: 51 additions & 29 deletions helper/decrypt_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})
}
}
Loading