diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e00a1aa0..08648a98 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main, v3 ] + branches: [ main, Proton ] permissions: {} @@ -23,7 +23,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v6 with: - go-version: ^1.16 + go-version: '^1.16' id: go - name: Install NDK diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 02c7adfa..8e21681b 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main, v3 ] + branches: [ main, Proton ] permissions: {} @@ -23,7 +23,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v6 with: - go-version: ^1.16 + go-version: '^1.22' id: go - name: Checkout diff --git a/.github/workflows/sop-test-suite.yml b/.github/workflows/sop-test-suite.yml index 52a849cb..bae5cff1 100644 --- a/.github/workflows/sop-test-suite.yml +++ b/.github/workflows/sop-test-suite.yml @@ -2,7 +2,7 @@ name: SOP interoperability test suite on: pull_request: - branches: [ main, v3 ] + branches: [ main, Proton ] permissions: {} diff --git a/.golangci.yml b/.golangci.yml index c0c62fc6..c25e2bbd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -25,6 +25,8 @@ issues: - param max has same name as predeclared identifier - G115 - the methods of "signatureCollector" + - SA1019 + - commentFormatting exclude-rules: - path: crypto/key_clear.go text: "SA1019" diff --git a/crypto/base_test.go b/crypto/base_test.go index c4429c76..4ba144b5 100644 --- a/crypto/base_test.go +++ b/crypto/base_test.go @@ -35,8 +35,8 @@ func readTestFile(name string, trimNewlines bool) string { func init() { testPGP = PGP() testPGP.defaultTime = NewConstantClock(testTime) // 2019-05-13T13:37:07+00:00 - testProfiles = []*profile.Custom{profile.Default(), profile.RFC4880(), profile.RFC9580()} - testProfileNames = []string{"Default", "RFC4880", "RFC9580"} + testProfiles = []*profile.Custom{profile.Default(), profile.RFC4880(), profile.RFC9580(), profile.Symmetric(), profile.PQC()} + testProfileNames = []string{"Default", "RFC4880", "RFC9580", "Symmetric", "PQC"} initEncDecTest() initGenerateKeys() initArmoredKeys() diff --git a/crypto/key.go b/crypto/key.go index 1388ab7c..aac781ee 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -397,6 +397,31 @@ func (key *Key) GetSHA256Fingerprint() (fingerprint string) { return hex.EncodeToString(getSHA256FingerprintBytes(key.entity.PrimaryKey)) } +// IsForwardingKey checks if the given key is a Proton forwarding key. +func (key *Key) IsForwardingKey() bool { + decryptionKeys := key.entity.DecryptionKeys(0, time.Time{}, nil) + allForwarding := len(decryptionKeys) > 0 + for _, key := range decryptionKeys { + if !isForwardingKey(key) { + allForwarding = false + break + } + } + return allForwarding +} + +// isForwardingKey determines if the given openpgp.Key is a forwarding key. +func isForwardingKey(key openpgp.Key) bool { + curve, err := key.PublicKey.Curve() + keyCheck := key.PublicKey.IsSubkey && + key.PublicKey.Version == 4 && + key.PublicKey.PubKeyAlgo == packet.PubKeyAlgoECDH && + err == nil && curve == packet.Curve25519 + hasForwardFlag := key.SelfSignature != nil && + key.SelfSignature.FlagForward + return keyCheck && hasForwardFlag +} + // GetSHA256Fingerprints computes the SHA256 fingerprints of the key and subkeys. func (key *Key) GetSHA256Fingerprints() (fingerprints []string) { fingerprints = append(fingerprints, key.GetSHA256Fingerprint()) diff --git a/crypto/key_clear.go b/crypto/key_clear.go index b19aae86..587d67b8 100644 --- a/crypto/key_clear.go +++ b/crypto/key_clear.go @@ -3,7 +3,6 @@ package crypto import ( "crypto/dsa" "crypto/rsa" - "errors" "math/big" "github.com/ProtonMail/go-crypto/openpgp/ecdh" @@ -76,7 +75,7 @@ func clearPrivateKey(privateKey interface{}) error { case *ed448.PrivateKey: return clearEd448PrivateKey(priv) default: - return errors.New("gopenpgp: unknown private key") + return nil } } diff --git a/crypto/keyring.go b/crypto/keyring.go index 80a46fea..a8350e57 100644 --- a/crypto/keyring.go +++ b/crypto/keyring.go @@ -285,6 +285,19 @@ func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err err return filteredKeys, nil } +// WithoutForwardingKeys returns a new keyring containing only non-forwarding keys. +func (keyRing *KeyRing) WithoutForwardingKeys() (*KeyRing, error) { + filtered := &KeyRing{} + for _, key := range keyRing.GetKeys() { + if !key.IsForwardingKey() { + if err := filtered.AddKey(key); err != nil { + return nil, fmt.Errorf("gopenpgp: failed to add key to filtered keyring: %w", err) + } + } + } + return filtered, nil +} + // FirstKey returns a KeyRing with only the first key of the original one. func (keyRing *KeyRing) FirstKey() (*KeyRing, error) { if len(keyRing.entities) == 0 { diff --git a/crypto/proton_test.go b/crypto/proton_test.go new file mode 100644 index 00000000..c0a76dca --- /dev/null +++ b/crypto/proton_test.go @@ -0,0 +1,119 @@ +package crypto + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestForwardeeDecryption(t *testing.T) { + //pgp.latestServerTime = 1679044110 + + forwardeeKey, err := NewKeyFromArmored(readTestFile("key_forwardee", false)) + if err != nil { + t.Fatal("Expected no error while unarmoring private keyring, got:", err) + } + + forwardeeKeyRing, err := NewKeyRing(forwardeeKey) + if err != nil { + t.Fatal("Expected no error while building private keyring, got:", err) + } + + pgpMessage := readTestFile("message_forwardee", false) + decryptor, err := PGP().Decryption(). + DecryptionKeys(forwardeeKeyRing). + VerifyTime(1679044110). + New() + if err != nil { + t.Fatal(err) + } + plainMessage, err := decryptor.Decrypt([]byte(pgpMessage), Armor) + if err != nil { + t.Fatal("Expected no error while decrypting/verifying, got:", err) + } + + assert.Exactly(t, "Message for Bob", plainMessage.String()) +} + +func TestFowardingKeyCheck(t *testing.T) { + forwardingKey, err := NewKeyFromArmored(readTestFile("key_forwardee", false)) + if err != nil { + t.Fatal("Expected no error while unarmoring private keyring, got:", err) + } + + nonForwardingKey, err := NewKeyFromArmored(readTestFile("keyring_userKey", false)) + if err != nil { + t.Fatal("Expected no error while unarmoring private keyring, got:", err) + } + + if !forwardingKey.IsForwardingKey() { + t.Fatal("Expected a forwarding key") + } + + if nonForwardingKey.IsForwardingKey() { + t.Fatal("Expected non-forwarding key") + } + + kr, err := NewKeyRing(forwardingKey) + if err != nil { + t.Fatal(err) + } + + if err := kr.AddKey(nonForwardingKey); err != nil { + t.Fatal(err) + } + + krWithoutForwarding, err := kr.WithoutForwardingKeys() + if err != nil { + t.Fatal(err) + } + + assert.Exactly(t, 1, krWithoutForwarding.CountEntities()) + + key, err := krWithoutForwarding.GetKey(0) + if err != nil { + t.Fatal(err) + } + assert.False(t, key.IsForwardingKey()) +} + +func TestSymmetricKeys(t *testing.T) { + symmetricKey, err := NewKeyFromArmored(readTestFile("key_symmetric", false)) + if err != nil { + t.Fatal("Expected no error while unarmoring private keyring, got:", err) + } + + symmetricKeyRing, err := NewKeyRing(symmetricKey) + if err != nil { + t.Fatal("Expected no error while building private keyring, got:", err) + } + + binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") + pgp := PGP() + encryptor, err := pgp.Encryption(). + Recipients(symmetricKeyRing). + SignTime(1679044110). + New() + if err != nil { + t.Fatal(err) + } + + ciphertext, err := encryptor.Encrypt(binData) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + decryptor, err := pgp.Decryption(). + DecryptionKeys(symmetricKeyRing). + VerifyTime(1679044110). + New() + if err != nil { + t.Fatal(err) + } + decrypted, err := decryptor.Decrypt(ciphertext.Bytes(), Bytes) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, binData, decrypted.Bytes()) +} diff --git a/crypto/testdata/key_forwardee b/crypto/testdata/key_forwardee new file mode 100644 index 00000000..adf82ad1 --- /dev/null +++ b/crypto/testdata/key_forwardee @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj +-----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/crypto/testdata/key_symmetric b/crypto/testdata/key_symmetric new file mode 100644 index 00000000..98f702f2 --- /dev/null +++ b/crypto/testdata/key_symmetric @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH +-----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/crypto/testdata/message_forwardee b/crypto/testdata/message_forwardee new file mode 100644 index 00000000..cd44ecf1 --- /dev/null +++ b/crypto/testdata/message_forwardee @@ -0,0 +1,8 @@ +-----BEGIN PGP MESSAGE----- + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa +-----END PGP MESSAGE----- \ No newline at end of file diff --git a/go.mod b/go.mod index 894ca148..f519c71d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ProtonMail/gopenpgp/v3 go 1.23.0 require ( - github.com/ProtonMail/go-crypto v1.4.1 + github.com/ProtonMail/go-crypto v1.4.1-proton github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f github.com/stretchr/testify v1.10.0 ) diff --git a/go.sum b/go.sum index ddfe87ce..fe2cac96 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= -github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/ProtonMail/go-crypto v1.4.1-proton h1:I4nanwGUmEeu7bTP9pkVWBbebGaxGyeMPnPRKmL++DY= +github.com/ProtonMail/go-crypto v1.4.1-proton/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= 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/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= diff --git a/profile/preset.go b/profile/preset.go index 01ce53a1..7998bb4e 100644 --- a/profile/preset.go +++ b/profile/preset.go @@ -11,24 +11,7 @@ import ( // Default returns a custom profile that support features // that are widely implemented. func Default() *Custom { - setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { - cfg.Algorithm = packet.PubKeyAlgoEdDSA - switch securityLevel { - case constants.HighSecurity: - cfg.Curve = packet.Curve25519 - default: - cfg.Curve = packet.Curve25519 - } - } - return &Custom{ - SetKeyAlgorithm: setKeyAlgorithm, - Hash: crypto.SHA256, - CipherEncryption: packet.CipherAES256, - CompressionAlgorithm: packet.CompressionZLIB, - CompressionConfiguration: &packet.CompressionConfig{ - Level: 6, - }, - } + return ProtonV1() } // RFC4880 returns a custom profile for this library @@ -80,3 +63,116 @@ func RFC9580() *Custom { V6: true, } } + +func PQC() *Custom { + setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { + switch securityLevel { + case constants.HighSecurity: + cfg.Algorithm = packet.PubKeyAlgoMldsa87Ed448 + default: + cfg.Algorithm = packet.PubKeyAlgoMldsa65Ed25519 + } + } + return &Custom{ + SetKeyAlgorithm: setKeyAlgorithm, + Hash: crypto.SHA512, + CipherEncryption: packet.CipherAES256, + CipherKeyEncryption: packet.CipherAES256, + CompressionAlgorithm: packet.CompressionZLIB, + AeadKeyEncryption: &packet.AEADConfig{ + DefaultMode: packet.AEADModeGCM, + }, + AeadEncryption: &packet.AEADConfig{ + DefaultMode: packet.AEADModeGCM, + }, + CompressionConfiguration: &packet.CompressionConfig{ + Level: 6, + }, + S2kKeyEncryption: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + Argon2Config: &s2k.Argon2Config{}, + }, + S2kEncryption: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + Argon2Config: &s2k.Argon2Config{}, + }, + V6: true, + } +} + +func Symmetric() *Custom { + setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { + cfg.Algorithm = packet.ExperimentalPubKeyAlgoHMAC + } + return &Custom{ + SetKeyAlgorithm: setKeyAlgorithm, + Hash: crypto.SHA512, + CipherEncryption: packet.CipherAES256, + CipherKeyEncryption: packet.CipherAES256, + CompressionAlgorithm: packet.CompressionZLIB, + AeadKeyEncryption: &packet.AEADConfig{ + DefaultMode: packet.AEADModeGCM, + }, + AeadEncryption: &packet.AEADConfig{ + DefaultMode: packet.AEADModeGCM, + }, + CompressionConfiguration: &packet.CompressionConfig{ + Level: 6, + }, + S2kKeyEncryption: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + Argon2Config: &s2k.Argon2Config{}, + }, + S2kEncryption: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + Argon2Config: &s2k.Argon2Config{}, + }, + V6: true, + } +} + +// ProtonV1 is the version 1 profile used in proton clients. +func ProtonV1() *Custom { + const maxDecompressedMessageSize = 50 * (int64(1) << 20) + setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { + cfg.Algorithm = packet.PubKeyAlgoEdDSA + switch securityLevel { + case constants.HighSecurity: + cfg.Curve = packet.Curve25519 + default: + cfg.Curve = packet.Curve25519 + } + } + s2kConfig := s2k.Config{ + S2KMode: s2k.IteratedSaltedS2K, + Hash: crypto.SHA256, + S2KCount: 65536, + } + return &Custom{ + SetKeyAlgorithm: setKeyAlgorithm, + Hash: crypto.SHA512, + CipherEncryption: packet.CipherAES256, + CipherKeyEncryption: packet.CipherAES256, + CompressionAlgorithm: packet.CompressionZLIB, + CompressionConfiguration: &packet.CompressionConfig{ + Level: 6, + }, + S2kKeyEncryption: &s2kConfig, + S2kEncryption: &s2kConfig, + DisableIntendedRecipients: true, + AllowAllPublicKeyAlgorithms: true, + InsecureAllowWeakRSA: true, + InsecureAllowDecryptionWithSigningKeys: true, + MaxDecompressedMessageSize: maxDecompressedMessageSize, + } +} + +// ProtonAeadV1 is the Proton profile but with AEAD enabled. +// Use this profile with care as it is not backward compatible. +func ProtonAeadV1() *Custom { + profile := ProtonV1() + profile.AeadEncryption = &packet.AEADConfig{ + DefaultMode: packet.AEADModeGCM, + } + return profile +} diff --git a/profile/profile.go b/profile/profile.go index 6b42ac76..56dd4c1b 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -24,7 +24,12 @@ type Custom struct { // S2kKeyEncryption defines the s2k algorithm for key encryption. S2kKeyEncryption *s2k.Config // AeadEncryption defines the aead encryption algorithm for pgp encryption. + // If nil, aead is disabled even if the key supports it. AeadEncryption *packet.AEADConfig + // KeyGenAeadEncryption defines if the output key in key generation + // advertises SEIPDv2 and aead algorithms in its key preferences. + // If nil, uses AeadEncryption as key preferences. + KeyGenAeadEncryption *packet.AEADConfig // S2kEncryption defines the s2k algorithm for pgp encryption. S2kEncryption *s2k.Config // CompressionConfiguration defines the compression configuration to be used if any. @@ -64,10 +69,14 @@ type Custom struct { // KeyGenerationProfile, KeyEncryptionProfile, EncryptionProfile, and SignProfile func (p *Custom) KeyGenerationConfig(securityLevel int8) *packet.Config { + aeadConfig := p.AeadEncryption + if p.KeyGenAeadEncryption != nil { + aeadConfig = p.KeyGenAeadEncryption + } cfg := &packet.Config{ DefaultHash: p.Hash, DefaultCipher: p.CipherEncryption, - AEADConfig: p.AeadEncryption, + AEADConfig: aeadConfig, DefaultCompressionAlgo: p.CompressionAlgorithm, CompressionConfig: p.CompressionConfiguration, V6Keys: p.V6,