Files
proton-bridge/internal/services/smtp/smtp_prefs.go
Leander Beernaert 4a01c46aed fix(GODT-3048): WKD Policy behavior
Ensure Bridge respects the no encrypt setting on a contact which has a
WKD key.
2023-11-08 14:23:36 +01:00

599 lines
16 KiB
Go

// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package smtp
import (
"fmt"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
pgpInline = "pgp-inline"
pgpMIME = "pgp-mime"
pmInternal = "internal" // A mix between pgpInline and pgpMime used by PM.
)
type contactSettings struct {
Email string
Keys []string
Scheme string
Sign bool
SignIsSet bool
Encrypt bool
EncryptUntrusted bool
MIMEType rfc822.MIMEType
}
// newContactSettings converts the API settings into our local settings.
// This is due to the legacy send preferences code.
func newContactSettings(settings proton.ContactSettings) *contactSettings {
metadata := &contactSettings{}
if settings.MIMEType != nil {
metadata.MIMEType = *settings.MIMEType
}
if settings.Sign != nil {
metadata.Sign = *settings.Sign
metadata.SignIsSet = true
}
if settings.Encrypt != nil {
metadata.Encrypt = *settings.Encrypt
}
if settings.EncryptUntrusted != nil {
metadata.EncryptUntrusted = *settings.EncryptUntrusted
} else {
metadata.EncryptUntrusted = true
}
if settings.Scheme != nil {
switch *settings.Scheme { // nolint:exhaustive
case proton.PGPMIMEScheme:
metadata.Scheme = pgpMIME
case proton.PGPInlineScheme:
metadata.Scheme = pgpInline
default:
panic("unknown scheme")
}
}
if settings.Keys != nil {
for _, key := range settings.Keys {
b, err := key.Serialize()
if err != nil {
panic(err)
}
metadata.Keys = append(metadata.Keys, string(b))
}
}
return metadata
}
func buildSendPrefs(
contactSettings proton.ContactSettings,
mailSettings proton.MailSettings,
pubKeys []proton.PublicKey,
mimeType rfc822.MIMEType,
isInternal bool,
) (proton.SendPreferences, error) {
builder := &sendPrefsBuilder{}
if err := builder.setPGPSettings(newContactSettings(contactSettings), pubKeys, isInternal); err != nil {
return proton.SendPreferences{}, fmt.Errorf("failed to set PGP settings: %w", err)
}
builder.setEncryptionPreferences(mailSettings)
builder.setMIMEPreferences(string(mimeType))
return builder.build(), nil
}
type sendPrefsBuilder struct {
internal bool
encrypt *bool
sign *bool
scheme *string
mimeType *rfc822.MIMEType
publicKey *crypto.KeyRing
}
func (b *sendPrefsBuilder) withInternal() {
b.internal = true
}
func (b *sendPrefsBuilder) isInternal() bool {
return b.internal
}
func (b *sendPrefsBuilder) withEncrypt(v bool) {
b.encrypt = &v
}
func (b *sendPrefsBuilder) withEncryptDefault(v bool) {
if b.encrypt == nil {
b.encrypt = &v
}
}
func (b *sendPrefsBuilder) shouldEncrypt() bool {
if b.encrypt != nil {
return *b.encrypt
}
return false
}
func (b *sendPrefsBuilder) withSign(sign bool) {
b.sign = &sign
}
func (b *sendPrefsBuilder) withSignDefault() {
v := true
if b.sign == nil {
b.sign = &v
}
}
func (b *sendPrefsBuilder) shouldSign() bool {
if b.sign != nil {
return *b.sign
}
return false
}
func (b *sendPrefsBuilder) withScheme(v string) {
b.scheme = &v
}
func (b *sendPrefsBuilder) withSchemeDefault(v string) {
if b.scheme == nil {
b.scheme = &v
}
}
func (b *sendPrefsBuilder) getScheme() string {
if b.scheme != nil {
return *b.scheme
}
return ""
}
func (b *sendPrefsBuilder) withMIMEType(v rfc822.MIMEType) {
b.mimeType = &v
}
func (b *sendPrefsBuilder) withMIMETypeDefault(v rfc822.MIMEType) {
if b.mimeType == nil {
b.mimeType = &v
}
}
func (b *sendPrefsBuilder) removeMIMEType() {
b.mimeType = nil
}
func (b *sendPrefsBuilder) getMIMEType() rfc822.MIMEType {
if b.mimeType != nil {
return *b.mimeType
}
return ""
}
func (b *sendPrefsBuilder) withPublicKey(v *crypto.KeyRing) {
b.publicKey = v
}
// Build converts the PGP scheme with a string value into a number value, and
// we may override some of the other encryption preferences with the composer
// preferences. Notice that the composer allows to select a sign preference,
// an email format preference and an encrypt-to-outside preference. The
// object we extract has the following possible value types:
//
// {
// encrypt: true | false,
// sign: true | false,
// pgpScheme: 1 (protonmail custom scheme)
// | 2 (Protonmail scheme for encrypted-to-outside email)
// | 4 (no cryptographic scheme)
// | 8 (PGP/INLINE)
// | 16 (PGP/MIME),
// mimeType: 'text/html' | 'text/plain' | 'multipart/mixed',
// publicKey: OpenPGPKey | undefined/null
// }.
func (b *sendPrefsBuilder) build() (p proton.SendPreferences) {
p.Encrypt = b.shouldEncrypt()
p.MIMEType = b.getMIMEType()
p.PubKey = b.publicKey
if b.shouldSign() {
p.SignatureType = proton.DetachedSignature
} else {
p.SignatureType = proton.NoSignature
}
switch {
case b.isInternal():
p.EncryptionScheme = proton.InternalScheme
case b.shouldSign() && b.shouldEncrypt():
if b.getScheme() == pgpInline {
p.EncryptionScheme = proton.PGPInlineScheme
} else {
p.EncryptionScheme = proton.PGPMIMEScheme
}
case b.shouldSign() && !b.shouldEncrypt():
if b.getScheme() == pgpInline {
p.EncryptionScheme = proton.ClearScheme
} else {
p.EncryptionScheme = proton.ClearMIMEScheme
}
default:
p.EncryptionScheme = proton.ClearScheme
}
return p
}
// setPGPSettings returns a SendPreferences with the following possible values:
//
// {
// encrypt: true | false | undefined/null/'',
// sign: true | false | undefined/null/'',
// pgpScheme: 'pgp-mime' | 'pgp-inline' | undefined/null/'',
// mimeType: 'text/html' | 'text/plain' | undefined/null/'',
// publicKey: OpenPGPKey | undefined/null
// }
//
// These settings are simply a reflection of the vCard content plus the public
// key info retrieved from the API via the GET KEYS route.
func (b *sendPrefsBuilder) setPGPSettings(
vCardData *contactSettings,
apiKeys []proton.PublicKey,
isInternal bool,
) (err error) {
// If there is no contact metadata, we can just use a default constructed one.
if vCardData == nil {
vCardData = &contactSettings{}
}
// Sending internal.
// We are guaranteed to always receive API keys.
if isInternal {
b.withInternal()
return b.setInternalPGPSettings(vCardData, apiKeys)
}
// Sending external but with keys supplied by WKD.
// Treated pretty much same as internal.
if len(apiKeys) > 0 {
return b.setExternalPGPSettingsWithWKDKeys(vCardData, apiKeys)
}
// Sending external without any WKD keys.
// If we have a contact saved, we can use its settings.
return b.setExternalPGPSettingsWithoutWKDKeys(vCardData)
}
// setInternalPGPSettings returns SendPreferences for internal messages.
// An internal address can be either an obvious one: abc@protonmail.com,
// abc@protonmail.ch or abc@pm.me, or one belonging to a custom domain
// registered with proton.
func (b *sendPrefsBuilder) setInternalPGPSettings(
vCardData *contactSettings,
apiKeys []proton.PublicKey,
) error {
// We're guaranteed to get at least one valid (i.e. not expired, revoked or
// marked as verification-only) public key from the server.
if len(apiKeys) == 0 {
return errors.New("an API key is necessary but wasn't provided")
}
// We always encrypt and sign internal mail.
b.withEncrypt(true)
b.withSign(true)
// We use a custom scheme for internal messages.
b.withScheme(pmInternal)
// If user has overridden the MIMEType for a contact, we use that.
// Otherwise, we take the MIMEType from the composer.
if vCardData.MIMEType != "" {
b.withMIMEType(vCardData.MIMEType)
}
sendingKey, err := pickSendingKey(vCardData, apiKeys)
if err != nil {
return err
}
b.withPublicKey(sendingKey)
return nil
}
// pickSendingKey tries to determine which key to use to encrypt outgoing mail.
// It returns a keyring containing the chosen key or an error.
//
// 1. If there are pinned keys in the vCard, those should be given preference
// (assuming the fingerprint matches one of the keys served by the API).
// 2. If there are pinned keys in the vCard but no matching keys were served
// by the API, we use one of the API keys but first show a modal to the
// user to ask them to confirm that they trust the API key.
// (Use case: user doesn't trust server, pins the only keys they trust to
// the contact, rogue server sends unknown keys, user should have option
// to say they don't recognise these keys and abort the mail send.)
// 3. If there are no pinned keys, then the client should encrypt with the
// first valid key served by the API (in principle the server already
// validates the keys and the first one provided should be valid).
func pickSendingKey(vCardData *contactSettings, rawAPIKeys []proton.PublicKey) (*crypto.KeyRing, error) {
contactKeys := make([]*crypto.Key, len(vCardData.Keys))
apiKeys := make([]*crypto.Key, len(rawAPIKeys))
for i, key := range vCardData.Keys {
var ck *crypto.Key
// Contact keys are not armored.
var err error
if ck, err = crypto.NewKey([]byte(key)); err != nil {
return nil, err
}
contactKeys[i] = ck
}
for i, key := range rawAPIKeys {
var ck *crypto.Key
// API keys are armored.
var err error
if ck, err = crypto.NewKeyFromArmored(key.PublicKey); err != nil {
return nil, err
}
apiKeys[i] = ck
}
matchedKeys := matchFingerprints(contactKeys, apiKeys)
var sendingKey *crypto.Key
switch {
// Case 1.
case len(matchedKeys) > 0:
sendingKey = matchedKeys[0]
// Case 2.
case len(matchedKeys) == 0 && len(contactKeys) > 0:
// NOTE: Here we should ask for trust confirmation.
sendingKey = apiKeys[0]
// Case 3.
default:
sendingKey = apiKeys[0]
}
return crypto.NewKeyRing(sendingKey)
}
func matchFingerprints(a, b []*crypto.Key) (res []*crypto.Key) {
aMap := make(map[string]*crypto.Key)
for _, el := range a {
aMap[el.GetFingerprint()] = el
}
for _, el := range b {
if _, inA := aMap[el.GetFingerprint()]; inA {
res = append(res, el)
}
}
return
}
func (b *sendPrefsBuilder) setExternalPGPSettingsWithWKDKeys(
vCardData *contactSettings,
apiKeys []proton.PublicKey,
) error {
// We're guaranteed to get at least one valid (i.e. not expired, revoked or
// marked as verification-only) public key from the server.
if len(apiKeys) == 0 {
return errors.New("an API key is necessary but wasn't provided")
}
b.withEncrypt(vCardData.EncryptUntrusted)
if vCardData.EncryptUntrusted {
b.withSign(true)
} else if vCardData.SignIsSet {
b.withSign(vCardData.Sign)
}
// If the contact has a specific Scheme preference, we set it (otherwise we
// leave it unset to allow it to be filled in with the default value later).
if vCardData.Scheme != "" {
b.withScheme(vCardData.Scheme)
}
// Because the email is signed, the cryptographic scheme determines the email
// format. A PGP/INLINE scheme forces to use plain text. A PGP/MIME scheme
// forces the automatic format.
switch vCardData.Scheme {
case pgpMIME:
b.removeMIMEType()
case pgpInline:
b.withMIMEType("text/plain")
}
sendingKey, err := pickSendingKey(vCardData, apiKeys)
if err != nil {
return err
}
b.withPublicKey(sendingKey)
return nil
}
func (b *sendPrefsBuilder) setExternalPGPSettingsWithoutWKDKeys(
vCardData *contactSettings,
) error {
b.withEncrypt(vCardData.Encrypt)
if vCardData.SignIsSet {
b.withSign(vCardData.Sign)
}
// Sign must be enabled whenever encrypt is.
if vCardData.Encrypt {
b.withSign(true)
}
// If the contact has a specific Scheme preference, we set it (otherwise we
// leave it unset to allow it to be filled in with the default value later).
if vCardData.Scheme != "" {
b.withScheme(vCardData.Scheme)
}
// If we are signing the message, the PGP scheme overrides the MIMEType.
// Otherwise, we read the MIMEType from the vCard, if set.
if vCardData.Sign {
switch vCardData.Scheme {
case pgpMIME:
b.removeMIMEType()
case pgpInline:
b.withMIMEType("text/plain")
}
} else if vCardData.MIMEType != "" {
b.withMIMEType(vCardData.MIMEType)
}
if len(vCardData.Keys) > 0 {
var (
key *crypto.Key
err error
)
// Contact keys are not armored.
if key, err = crypto.NewKey([]byte(vCardData.Keys[0])); err != nil {
return err
}
var kr *crypto.KeyRing
if kr, err = crypto.NewKeyRing(key); err != nil {
return err
}
b.withPublicKey(kr)
}
return nil
}
// setEncryptionPreferences sets the undefined values in the SendPreferences
// determined thus far using using the (global) user mail settings.
// The object we extract has the following possible value types:
//
// {
// encrypt: true | false,
// sign: true | false,
// pgpScheme: 'pgp-mime' | 'pgp-inline',
// mimeType: 'text/html' | 'text/plain',
// publicKey: OpenPGPKey | undefined/null
// }
//
// The public key can still be undefined as we do not need it if the outgoing
// email is not encrypted.
func (b *sendPrefsBuilder) setEncryptionPreferences(mailSettings proton.MailSettings) {
// For internal addresses or external ones with WKD keys, this flag should
// always be true. For external ones, an undefined flag defaults to false.
b.withEncryptDefault(false)
// For internal addresses or external ones with WKD keys, this flag should
// always be true. For external ones, an undefined flag defaults to the user
// mail setting "Sign External messages". Otherwise we keep the defined value
// unless it conflicts with the encrypt flag (we do not allow to send
// encrypted but not signed).
if mailSettings.Sign > 0 {
b.withSignDefault()
}
if b.shouldEncrypt() {
b.withSign(true)
}
// If undefined, default to the user mail setting "Default PGP scheme".
// Otherwise keep the defined value.
switch mailSettings.PGPScheme {
case proton.PGPInlineScheme:
logrus.WithFields(logrus.Fields{"service": "smtp", "settings": "account"}).Warn("PGPInline scheme used. Planed to be deprecated.")
b.withSchemeDefault(pgpInline)
case proton.PGPMIMEScheme:
b.withSchemeDefault(pgpMIME)
case proton.ClearMIMEScheme, proton.ClearScheme, proton.EncryptedOutsideScheme, proton.InternalScheme:
// nothing to set
}
// Its value is constrained by the sign flag and the PGP scheme:
// - Sign flag = true → For a PGP/Inline scheme, the MIME type must be
// 'plain/text'. Otherwise we default to the user mail setting "Composer mode"
// - Sign flag = false → If undefined, default to the user mail setting
// "Composer mode". Otherwise keep the defined value.
if b.shouldSign() && b.getScheme() == pgpInline {
b.withMIMEType("text/plain")
} else {
b.withMIMETypeDefault(mailSettings.DraftMIMEType)
}
}
func (b *sendPrefsBuilder) setMIMEPreferences(composerMIMEType string) {
// If the sign flag (that we just determined above) is true, then the MIME
// type is determined by the PGP scheme (also determined above): we should
// use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise.
// Otherwise we use the MIME type from the encryption preferences, unless
// the plain text option has been selecting in the composer, which should
// enforce 'text/plain' and override the encryption preference.
if !b.isInternal() && b.shouldSign() {
switch b.getScheme() {
case pgpInline:
b.withMIMEType("text/plain")
default:
b.withMIMEType("multipart/mixed")
}
} else if composerMIMEType == "text/plain" {
b.withMIMEType("text/plain")
}
}