diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 128db669..5cf18b71 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -23,6 +23,12 @@ jobs: go-version: ^1.16 id: go + - name: Install NDK + uses: nttld/setup-ndk@v1 + with: + ndk-version: r23c + link-to-sdk: true + - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2bcffd..7e1d1cbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ 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). +## [2.6.0] 2023-03-15 + +### Added +- API to get signature key IDs for mobile: + ```go + func (msg *PGPMessage) GetHexSignatureKeyIDsJson() []byte + ``` +- API to get encryption key IDs for mobile: + ```go + func (msg *PGPMessage) GetHexEncryptionKeyIDsJson() []byte + ``` +- API to get the number of key packets in a PGP message: + ```go + func (msg *PGPSplitMessage) GetNumberOfKeyPackets() (int, error) + ``` +- API in package `helper` to encrypt a PGP message to an additional key: + ```go + func EncryptPGPMessageToAdditionalKey(messageToModify *crypto.PGPSplitMessage, keyRing *crypto.KeyRing, additionalKey *crypto.KeyRing) error + ``` + ## [2.7.4] 2023-10-27 ### Fixed - Ensure that `(SessionKey).Decrypt` functions return an error if no integrity protection is present in the encrypted input. To protect SEIPDv1 encrypted messages, SED packets must not be allowed in decryption. diff --git a/constants/armor.go b/constants/armor.go index e6cc9561..0d2c1925 100644 --- a/constants/armor.go +++ b/constants/armor.go @@ -3,7 +3,7 @@ package constants // Constants for armored data. const ( - ArmorHeaderVersion = "GopenPGP 2.7.4" + ArmorHeaderVersion = "GopenPGP 2.7.5" ArmorHeaderComment = "https://gopenpgp.org" PGPMessageHeader = "PGP MESSAGE" PGPSignatureHeader = "PGP SIGNATURE" diff --git a/constants/version.go b/constants/version.go index a998d9bd..cca8c39c 100644 --- a/constants/version.go +++ b/constants/version.go @@ -1,3 +1,3 @@ package constants -const Version = "2.7.4" +const Version = "2.7.5" diff --git a/crypto/message.go b/crypto/message.go index 4f34be4d..284098fe 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -3,6 +3,7 @@ package crypto import ( "bytes" "encoding/base64" + "encoding/json" goerrors "errors" "io" "io/ioutil" @@ -287,6 +288,21 @@ func (msg *PGPMessage) GetHexEncryptionKeyIDs() ([]string, bool) { return getHexKeyIDs(msg.GetEncryptionKeyIDs()) } +// GetHexEncryptionKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +// If an error occurs it returns nil. +// Helper function for go-mobile clients. +func (msg *PGPMessage) GetHexEncryptionKeyIDsJson() []byte { + hexIds, ok := msg.GetHexEncryptionKeyIDs() + if !ok { + return nil + } + hexIdsJson, err := json.Marshal(hexIds) + if err != nil { + return nil + } + return hexIdsJson +} + // GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to. func (msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool) { return getSignatureKeyIDs(msg.Data) @@ -297,6 +313,20 @@ func (msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool) { return getHexKeyIDs(msg.GetSignatureKeyIDs()) } +// GetHexSignatureKeyIDsJson returns the key IDs of the keys to which the (readable) signature packets +// are encrypted to as a JSON array. Helper function for go-mobile clients. +func (msg *PGPMessage) GetHexSignatureKeyIDsJson() []byte { + sigHexSigIds, ok := msg.GetHexSignatureKeyIDs() + if !ok { + return nil + } + sigHexKeyIdsJSON, err := json.Marshal(sigHexSigIds) + if err != nil { + return nil + } + return sigHexKeyIdsJSON +} + // GetBinaryDataPacket returns the unarmored binary datapacket as a []byte. func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte { return msg.DataPacket @@ -324,6 +354,27 @@ func (msg *PGPSplitMessage) GetPGPMessage() *PGPMessage { return NewPGPMessage(append(msg.KeyPacket, msg.DataPacket...)) } +// GetNumberOfKeyPackets returns the number of keys packets in this message. +func (msg *PGPSplitMessage) GetNumberOfKeyPackets() (int, error) { + bytesReader := bytes.NewReader(msg.KeyPacket) + packets := packet.NewReader(bytesReader) + var keyPacketCount int + for { + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + break + } + if err != nil { + return 0, err + } + switch p.(type) { + case *packet.SymmetricKeyEncrypted, *packet.EncryptedKey: + keyPacketCount += 1 + } + } + return keyPacketCount, nil +} + // SplitMessage splits the message into key and data packet(s). // Parameters are for backwards compatibility and are unused. func (msg *PGPMessage) SplitMessage() (*PGPSplitMessage, error) { diff --git a/helper/message.go b/helper/message.go new file mode 100644 index 00000000..b4e58ff0 --- /dev/null +++ b/helper/message.go @@ -0,0 +1,22 @@ +package helper + +import "github.com/ProtonMail/gopenpgp/v2/crypto" + +// EncryptPGPMessageToAdditionalKey decrypts the session key of the input PGPSplitMessage with a private key in keyRing +// and encrypts it towards the additionalKeys by adding the additional key packets to the input PGPSplitMessage. +// If successful, new key packets are added to message. +// * messageToModify : The encrypted pgp message that should be modified +// * keyRing : The private keys to decrypt the session key in the messageToModify. +// * additionalKey : The public keys the message should be additionally encrypted to. +func EncryptPGPMessageToAdditionalKey(messageToModify *crypto.PGPSplitMessage, keyRing *crypto.KeyRing, additionalKey *crypto.KeyRing) error { + sessionKey, err := keyRing.DecryptSessionKey(messageToModify.KeyPacket) + if err != nil { + return err + } + additionalKeyPacket, err := additionalKey.EncryptSessionKey(sessionKey) + if err != nil { + return err + } + messageToModify.KeyPacket = append(messageToModify.KeyPacket, additionalKeyPacket...) + return nil +} diff --git a/helper/message_test.go b/helper/message_test.go new file mode 100644 index 00000000..9b140bff --- /dev/null +++ b/helper/message_test.go @@ -0,0 +1,56 @@ +package helper_test + +import ( + "testing" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v2/helper" + "github.com/stretchr/testify/assert" +) + +func TestEncryptPGPMessageToAdditionalKey(t *testing.T) { + keyA, err := crypto.GenerateKey("A", "a@a.a", "x25519", 0) + if err != nil { + t.Fatal("Expected no error when generating key, got:", err) + } + + keyB, err := crypto.GenerateKey("B", "b@b.b", "x25519", 0) + if err != nil { + t.Fatal("Expected no error when generating key, got:", err) + } + + keyRingA, err := crypto.NewKeyRing(keyA) + if err != nil { + t.Fatal("Expected no error when creating keyring, got:", err) + } + keyRingB, err := crypto.NewKeyRing(keyB) + if err != nil { + t.Fatal("Expected no error when creating keyring, got:", err) + } + + message := crypto.NewPlainMessageFromString("plain text") + // Encrypt towards A + ciphertext, err := keyRingA.Encrypt(message, nil) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + ciphertextSplit, err := ciphertext.SplitMessage() + if err != nil { + t.Fatal("Expected no error when splitting message, got:", err) + } + // Also encrypt the message towards B + if err := helper.EncryptPGPMessageToAdditionalKey(ciphertextSplit, keyRingA, keyRingB); err != nil { + t.Fatal("Expected no error when modifying the message, got:", err) + } + + // Test decrypt with B + decrypted, err := keyRingB.Decrypt( + ciphertextSplit.GetPGPMessage(), + nil, + 0, + ) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) +}