// Copyright (c) 2020 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ProtonMail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . package smtp import ( "errors" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/pkg/algo" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) const ( pgpInline = "pgp-inline" pgpMime = "pgp-mime" ) type SendingInfo struct { Encrypt bool Sign bool Scheme int MIMEType string PublicKey *crypto.KeyRing } func generateSendingInfo( eventListener listener.Listener, contactMeta *ContactMetadata, isInternal bool, composeMode string, apiKeys, contactKeys []*crypto.KeyRing, settingsSign bool, settingsPgpScheme int) (sendingInfo SendingInfo, err error) { contactKeys, err = crypto.FilterExpiredKeys(contactKeys) if err != nil { return } if isInternal { sendingInfo, err = generateInternalSendingInfo(eventListener, contactMeta, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme) } else { sendingInfo, err = generateExternalSendingInfo(contactMeta, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme) } if (sendingInfo.Scheme == pmapi.PGPInlinePackage || sendingInfo.Scheme == pmapi.PGPMIMEPackage) && sendingInfo.PublicKey == nil { return sendingInfo, errors.New("public key nil during attempt to encrypt") } return } func generateInternalSendingInfo( eventListener listener.Listener, contactMeta *ContactMetadata, composeMode string, apiKeys, contactKeys []*crypto.KeyRing, settingsSign bool, //nolint[unparam] settingsPgpScheme int) (sendingInfo SendingInfo, err error) { //nolint[unparam] // If sending internally, there should always be a public key; if not, there's an error. if len(apiKeys) == 0 { err = errors.New("no valid public keys found for contact") return } // The default settings, unless overridden by presence of a saved contact. sendingInfo = SendingInfo{ Encrypt: true, Sign: true, Scheme: pmapi.InternalPackage, MIMEType: composeMode, PublicKey: apiKeys[0], } // If there is no saved contact, our work here is done. if contactMeta == nil { return } // If contact has a pinned key, prefer that over the api key (if it's not expired). checkedContactKeys, err := checkContactKeysAgainstAPI(contactKeys, apiKeys) if err != nil { return } // If we find no matching keys with the api but the contact still has pinned keys // it means the pinned keys are out of date (e.g. the contact has since changed their protonmail // keys and so the keys returned via the api don't match the keys pinned in the contact). if len(checkedContactKeys) == 0 && len(contactKeys) != 0 { eventListener.Emit(events.NoActiveKeyForRecipientEvent, contactMeta.Email) return sendingInfo, errors.New("found no active key for recipient " + contactMeta.Email + ", please check contact settings") } if len(checkedContactKeys) > 0 { sendingInfo.PublicKey = checkedContactKeys[0] } // If contact has a saved mime type preference, prefer that over the default. if len(contactMeta.MIMEType) > 0 { sendingInfo.MIMEType = contactMeta.MIMEType } return sendingInfo, nil } func generateExternalSendingInfo( contactMeta *ContactMetadata, composeMode string, apiKeys, contactKeys []*crypto.KeyRing, settingsSign bool, settingsPgpScheme int) (sendingInfo SendingInfo, err error) { // The default settings, unless overridden by presence of a saved contact. sendingInfo = SendingInfo{ Encrypt: false, Sign: settingsSign, PublicKey: nil, } if contactMeta != nil && len(contactKeys) > 0 { // If the contact has a key, use it. And if the contact metadata says to encryt, do so. sendingInfo.PublicKey = contactKeys[0] sendingInfo.Encrypt = contactMeta.Encrypt } else if len(apiKeys) > 0 { // If the api returned a key (via WKD), use it. In this case we always encrypt. sendingInfo.PublicKey = apiKeys[0] sendingInfo.Encrypt = true } // - If we are encrypting, we always sign // - else if the contact has a preference, we follow that // - otherwise, we fall back to the mailbox default signing settings if sendingInfo.Encrypt { //nolint[gocritic] sendingInfo.Sign = true } else if contactMeta != nil && !contactMeta.SignMissing { sendingInfo.Sign = contactMeta.Sign } else { sendingInfo.Sign = settingsSign } sendingInfo.Scheme, sendingInfo.MIMEType, err = schemeAndMIME(contactMeta, settingsPgpScheme, composeMode, sendingInfo.Encrypt, sendingInfo.Sign) return sendingInfo, err } func schemeAndMIME(contact *ContactMetadata, settingsScheme int, settingsMIMEType string, encrypted, signed bool) (scheme int, mime string, err error) { if encrypted && signed { // Prefer contact settings. if contact != nil && contact.Scheme == pgpInline { return pmapi.PGPInlinePackage, pmapi.ContentTypePlainText, nil } else if contact != nil && contact.Scheme == pgpMime { return pmapi.PGPMIMEPackage, pmapi.ContentTypeMultipartMixed, nil } // If no contact settings, follow mailbox defaults. scheme = settingsScheme if scheme == pmapi.PGPMIMEPackage { return scheme, pmapi.ContentTypeMultipartMixed, nil } else if scheme == pmapi.PGPInlinePackage { return scheme, pmapi.ContentTypePlainText, nil } } if !encrypted && signed { // Prefer contact settings but send unencrypted (PGP-->Clear). if contact != nil && contact.Scheme == pgpMime { return pmapi.ClearMIMEPackage, pmapi.ContentTypeMultipartMixed, nil } else if contact != nil && contact.Scheme == pgpInline { return pmapi.ClearPackage, pmapi.ContentTypePlainText, nil } // If no contact settings, follow mailbox defaults but send unencrypted (PGP-->Clear). if settingsScheme == pmapi.PGPMIMEPackage { return pmapi.ClearMIMEPackage, pmapi.ContentTypeMultipartMixed, nil } else if settingsScheme == pmapi.PGPInlinePackage { return pmapi.ClearPackage, pmapi.ContentTypePlainText, nil } } if !encrypted && !signed { // Always send as clear package if we are neither encrypting nor signing. scheme = pmapi.ClearPackage // If the contact is nil, no further modifications can be made. if contact == nil { return scheme, settingsMIMEType, nil } // Prefer contact mime settings. if contact.Scheme == pgpMime { return scheme, pmapi.ContentTypeMultipartMixed, nil } else if contact.Scheme == pgpInline { return scheme, pmapi.ContentTypePlainText, nil } // If contact has a preferred mime type, use that, otherwise follow mailbox default. if len(contact.MIMEType) > 0 { return scheme, contact.MIMEType, nil } return scheme, settingsMIMEType, nil } // If we end up here, something went wrong. err = errors.New("could not determine correct PGP Scheme and MIME Type to use to send mail") return scheme, mime, err } // checkContactKeysAgainstAPI keeps only those contact keys which are up to date and have // an ID that matches an API key's ID. func checkContactKeysAgainstAPI(contactKeys, apiKeys []*crypto.KeyRing) (filteredKeys []*crypto.KeyRing, err error) { //nolint[unparam] keyIDsAreEqual := func(a, b interface{}) bool { aKey, bKey := a.(*crypto.KeyRing), b.(*crypto.KeyRing) aFirst, getKeyErr := aKey.GetKey(0) if getKeyErr != nil { err = errors.New("missing primary key") return false } bFirst, getKeyErr := bKey.GetKey(0) if getKeyErr != nil { err = errors.New("missing primary key") return false } return aFirst.GetKeyID() == bFirst.GetKeyID() } for _, v := range algo.SetIntersection(contactKeys, apiKeys, keyIDsAreEqual) { filteredKeys = append(filteredKeys, v.(*crypto.KeyRing)) } return }