diff --git a/CHANGELOG.md b/CHANGELOG.md index 91afc3f..3552191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +- Key and KeyRing methods to check if a key/keyring can Encrypt or Verify +```go +(key *Key) CanVerify() bool +(key *Key) CanEncrypt() bool +(keyRing *KeyRing) CanVerify() bool +(keyRing *KeyRing) CanEncrypt() bool +``` +- SessionKey methods to encrypt/decrypt and simultaneously sign/verify with an asymmetric key (embedded signature) +```go +(sk *SessionKey) EncryptAndSign(message *PlainMessage, signKeyRing *KeyRing) ([]byte, error) +(sk *SessionKey) DecryptAndVerify(dataPacket []byte, verifyKeyRing *KeyRing, verifyTime int64) (*PlainMessage, error) +``` +- The mobile helper `DecryptSessionKeyExplicitVerify` to allow using session key decryption + verification operations via gomobile + ## [2.1.7] 2021-03-30 ### Added diff --git a/crypto/sessionkey.go b/crypto/sessionkey.go index a823c53..33f037f 100644 --- a/crypto/sessionkey.go +++ b/crypto/sessionkey.go @@ -124,7 +124,30 @@ func (sk *SessionKey) Encrypt(message *PlainMessage) ([]byte, error) { DefaultCipher: dc, } - return encryptWithSessionKey(message, sk, config) + return encryptWithSessionKey(message, sk, nil, config) +} + +// EncryptAndSign encrypts a PlainMessage to PGPMessage with a SessionKey and signs it with a Private key. +// * message : The plain data as a PlainMessage. +// * signKeyRing: The KeyRing to sign the message +// * output : The encrypted data as PGPMessage. +func (sk *SessionKey) EncryptAndSign(message *PlainMessage, signKeyRing *KeyRing) ([]byte, error) { + dc, err := sk.GetCipherFunc() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") + } + + config := &packet.Config{ + Time: getTimeGenerator(), + DefaultCipher: dc, + } + + signEntity, err := signKeyRing.getSigningEntity() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to sign") + } + + return encryptWithSessionKey(message, sk, signEntity, config) } // EncryptWithCompression encrypts with compression support a PlainMessage to PGPMessage with a SessionKey. @@ -143,14 +166,14 @@ func (sk *SessionKey) EncryptWithCompression(message *PlainMessage) ([]byte, err CompressionConfig: &packet.CompressionConfig{Level: constants.DefaultCompressionLevel}, } - return encryptWithSessionKey(message, sk, config) + return encryptWithSessionKey(message, sk, nil, config) } -func encryptWithSessionKey(message *PlainMessage, sk *SessionKey, config *packet.Config) ([]byte, error) { - var encBuf bytes.Buffer - var encryptWriter io.WriteCloser +func encryptWithSessionKey(message *PlainMessage, sk *SessionKey, signEntity *openpgp.Entity, config *packet.Config) ([]byte, error) { + var encBuf = new(bytes.Buffer) + var encryptWriter, signWriter io.WriteCloser - encryptWriter, err := packet.SerializeSymmetricallyEncrypted(&encBuf, config.Cipher(), sk.Key, config) + encryptWriter, err := packet.SerializeSymmetricallyEncrypted(encBuf, config.Cipher(), sk.Key, config) if err != nil { return nil, errors.Wrap(err, "gopenpgp: unable to encrypt") } @@ -162,37 +185,70 @@ func encryptWithSessionKey(message *PlainMessage, sk *SessionKey, config *packet } } - encryptWriter, err = packet.SerializeLiteral( - encryptWriter, - message.IsBinary(), - message.Filename, - message.Time, - ) + if signEntity != nil { // nolint:nestif + hints := &openpgp.FileHints{ + IsBinary: message.IsBinary(), + FileName: message.Filename, + ModTime: message.getFormattedTime(), + } - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to serialize") - } + signWriter, err = openpgp.Sign(encryptWriter, signEntity, hints, config) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to sign") + } - _, err = encryptWriter.Write(message.GetBinary()) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in writing message") + _, err = signWriter.Write(message.GetBinary()) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in writing signed message") + } + + err = signWriter.Close() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in closing signing writer") + } + } else { + encryptWriter, err = packet.SerializeLiteral( + encryptWriter, + message.IsBinary(), + message.Filename, + message.Time, + ) + + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to serialize") + } + + _, err = encryptWriter.Write(message.GetBinary()) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in writing message") + } } err = encryptWriter.Close() if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in closing message") + return nil, errors.Wrap(err, "gopenpgp: error in closing encryption writer") } return encBuf.Bytes(), nil } -// Decrypt decrypts password protected pgp binary messages. +// Decrypt decrypts pgp data packets using directly a session key. // * encrypted: PGPMessage. // * output: PlainMessage. func (sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) { + return sk.DecryptAndVerify(dataPacket, nil, 0) +} + +// DecryptAndVerify decrypts pgp data packets using directly a session key and verifies embedded signatures. +// * encrypted: PGPMessage. +// * verifyKeyRing: KeyRing with verification public keys +// * verifyTime: when should the signature be valid, as timestamp. If 0 time verification is disabled. +// * output: PlainMessage. +func (sk *SessionKey) DecryptAndVerify(dataPacket []byte, verifyKeyRing *KeyRing, verifyTime int64) (*PlainMessage, error) { var messageReader = bytes.NewReader(dataPacket) var decrypted io.ReadCloser var decBuf bytes.Buffer + var keyring openpgp.EntityList // Read symmetrically encrypted data packet packets := packet.NewReader(messageReader) @@ -227,7 +283,13 @@ func (sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) { } // Push decrypted packet as literal packet and use openpgp's reader - keyring := openpgp.EntityList{} // Ignore signatures, since we have no private key + + if verifyKeyRing != nil { + keyring = verifyKeyRing.entities + } else { + keyring = openpgp.EntityList{} + } + md, err := openpgp.ReadMessage(&decBuf, keyring, nil, config) if err != nil { return nil, errors.Wrap(err, "gopenpgp: unable to decode symmetric packet") @@ -239,12 +301,17 @@ func (sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) { return nil, errors.Wrap(err, "gopenpgp: error in reading message body") } + if verifyKeyRing != nil { + processSignatureExpiration(md, verifyTime) + err = verifyDetailsSignature(md, verifyKeyRing) + } + return &PlainMessage{ Data: messageBuf.Bytes(), TextType: !md.LiteralData.IsBinary, Filename: md.LiteralData.FileName, Time: md.LiteralData.Time, - }, nil + }, err } func (sk *SessionKey) checkSize() error { diff --git a/crypto/sessionkey_test.go b/crypto/sessionkey_test.go index f2739bd..27525a2 100644 --- a/crypto/sessionkey_test.go +++ b/crypto/sessionkey_test.go @@ -2,6 +2,7 @@ package crypto import ( "encoding/base64" + "errors" "testing" "github.com/ProtonMail/gopenpgp/v2/constants" @@ -162,6 +163,90 @@ func TestDataPacketEncryption(t *testing.T) { assert.Exactly(t, message.GetString(), finalMessage.GetString()) } +func TestDataPacketEncryptionAndSignature(t *testing.T) { + var message = NewPlainMessageFromString( + "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", + ) + + // Encrypt data with session key + dataPacket, err := testSessionKey.EncryptAndSign(message, keyRingTestPrivate) + if err != nil { + t.Fatal("Expected no error when encrypting and signing, got:", err) + } + + // Decrypt data with wrong session key + wrongKey := SessionKey{ + Key: []byte("wrong pass"), + Algo: constants.AES256, + } + _, err = wrongKey.Decrypt(dataPacket) + assert.NotNil(t, err) + + // Decrypt data with the good session key + decrypted, err := testSessionKey.Decrypt(dataPacket) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) + + // Decrypt & verify data with the good session key but bad keyring + ecKeyRing, err := NewKeyRing(keyTestEC) + if err != nil { + t.Fatal("Unable to generate EC keyring, got:", err) + } + + castedErr := &SignatureVerificationError{} + _, err = testSessionKey.DecryptAndVerify(dataPacket, ecKeyRing, GetUnixTime()) + if err == nil || !errors.As(err, castedErr) { + t.Fatal("No error or wrong error returned for verification failure", err) + } + + // Decrypt & verify data with the good session key and keyring + decrypted, err = testSessionKey.DecryptAndVerify(dataPacket, keyRingTestPublic, GetUnixTime()) + if err != nil { + t.Fatal("Expected no error when decrypting & verifying, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) + + // Encrypt session key + assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) + keyPacket, err := keyRingTestMultiple.EncryptSessionKey(testSessionKey) + if err != nil { + t.Fatal("Unable to encrypt key packet, got:", err) + } + + // Join key packet and data packet in single message + splitMessage := NewPGPSplitMessage(keyPacket, dataPacket) + + // Armor and un-armor message. In alternative it can also be done with NewPgpMessage(splitMessage.GetBinary()) + armored, err := splitMessage.GetArmored() + if err != nil { + t.Fatal("Unable to armor split message, got:", err) + } + + pgpMessage, err := NewPGPMessageFromArmored(armored) + if err != nil { + t.Fatal("Unable to unarmor pgp message, got:", err) + } + ids, ok := pgpMessage.GetEncryptionKeyIDs() + assert.True(t, ok) + assert.Exactly(t, 3, len(ids)) + + // Test with bad verification key succeeds + _, err = keyRingTestPrivate.Decrypt(pgpMessage, ecKeyRing, GetUnixTime()) + if err == nil || !errors.As(err, castedErr) { + t.Fatal("No error or wrong error returned for verification failure") + } + + // Test if final decryption & verification succeeds + finalMessage, err := keyRingTestPrivate.Decrypt(pgpMessage, keyRingTestPublic, GetUnixTime()) + if err != nil { + t.Fatal("Unable to decrypt and verify joined keypacket and datapacket, got:", err) + } + + assert.Exactly(t, message.GetString(), finalMessage.GetString()) +} + func TestDataPacketDecryption(t *testing.T) { pgpMessage, err := NewPGPMessageFromArmored(readTestFile("message_signed", false)) if err != nil { diff --git a/helper/mobile.go b/helper/mobile.go index b319682..d4cd9ad 100644 --- a/helper/mobile.go +++ b/helper/mobile.go @@ -14,18 +14,33 @@ type ExplicitVerifyMessage struct { SignatureVerificationError *crypto.SignatureVerificationError } -// DecryptExplicitVerify decrypts an armored PGP message given a private key -// and its passphrase and verifies the embedded signature. Returns the plain -// data or an error on signature verification failure. +// DecryptExplicitVerify decrypts a PGP message given a private keyring +// and a public keyring to verify the embedded signature. Returns the plain +// data and an error on signature verification failure. func DecryptExplicitVerify( pgpMessage *crypto.PGPMessage, privateKeyRing, publicKeyRing *crypto.KeyRing, verifyTime int64, ) (*ExplicitVerifyMessage, error) { - var explicitVerify *ExplicitVerifyMessage - message, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, verifyTime) + return newExplicitVerifyMessage(message, err) +} +// DecryptSessionKeyExplicitVerify decrypts a PGP data packet given a session key +// and a public keyring to verify the embedded signature. Returns the plain data and +// an error on signature verification failure. +func DecryptSessionKeyExplicitVerify( + dataPacket []byte, + sessionKey *crypto.SessionKey, + publicKeyRing *crypto.KeyRing, + verifyTime int64, +) (*ExplicitVerifyMessage, error) { + message, err := sessionKey.DecryptAndVerify(dataPacket, publicKeyRing, verifyTime) + return newExplicitVerifyMessage(message, err) +} + +func newExplicitVerifyMessage(message *crypto.PlainMessage, err error) (*ExplicitVerifyMessage, error) { + var explicitVerify *ExplicitVerifyMessage if err != nil { castedErr := &crypto.SignatureVerificationError{} isType := goerrors.As(err, castedErr) diff --git a/helper/mobile_test.go b/helper/mobile_test.go index 49d4136..c8eec57 100644 --- a/helper/mobile_test.go +++ b/helper/mobile_test.go @@ -54,6 +54,57 @@ func TestMobileSignedMessageDecryption(t *testing.T) { assert.Nil(t, decrypted) } +func TestMobileSignedMessageDecryptionWithSessionKey(t *testing.T) { + var message = crypto.NewPlainMessageFromString( + "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", + ) + + privateKey, _ := crypto.NewKeyFromArmored(readTestFile("keyring_privateKey", false)) + // Password defined in base_test + privateKey, err := privateKey.Unlock(testMailboxPassword) + if err != nil { + t.Fatal("Expected no error unlocking privateKey, got:", err) + } + testPrivateKeyRing, _ := crypto.NewKeyRing(privateKey) + + publicKey, _ := crypto.NewKeyFromArmored(readTestFile("keyring_publicKey", false)) + testPublicKeyRing, _ := crypto.NewKeyRing(publicKey) + + sk, err := crypto.GenerateSessionKey() + if err != nil { + t.Fatal("Expected no error generating session key, got:", err) + } + + pgpMessage, err := sk.Encrypt(message) + if err != nil { + t.Fatal("Expected no error when unarmoring, got:", err) + } + + decrypted, err := DecryptSessionKeyExplicitVerify(pgpMessage, sk, testPublicKeyRing, crypto.GetUnixTime()) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, constants.SIGNATURE_NO_VERIFIER, decrypted.SignatureVerificationError.Status) + assert.Exactly(t, message.GetString(), decrypted.Message.GetString()) + + publicKey, _ = crypto.NewKeyFromArmored(readTestFile("keyring_publicKey", false)) + testPublicKeyRing, _ = crypto.NewKeyRing(publicKey) + + pgpMessage, err = sk.EncryptAndSign(message, testPrivateKeyRing) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + decrypted, err = DecryptSessionKeyExplicitVerify(pgpMessage, sk, testPublicKeyRing, crypto.GetUnixTime()) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Nil(t, decrypted.SignatureVerificationError) + assert.Exactly(t, message.GetString(), decrypted.Message.GetString()) +} + func TestGetJsonSHA256FingerprintsV4(t *testing.T) { sha256Fingerprints, err := GetJsonSHA256Fingerprints(readTestFile("keyring_publicKey", false)) if err != nil {