diff --git a/Changelog.md b/Changelog.md index e3583188..7affa855 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,9 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-308 Better user error message when request is canceled. * GODT-312 Validate recipient emails in send before asking for their public keys. * GODT-368 Bump docker-credential-helpers version. +* GODT-280 Migrate to gopenpgp v2 + * `Unlock()` call on pmapi-client unlocks both User keys and Address keys + * Salt is available via `AuthSalt()` method ### Fixed * GODT-356 Fix crash when removing account while mail client is fetching messages (regression from GODT-204). diff --git a/go.mod b/go.mod index 581f8f08..fb56370d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 - github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed + github.com/ProtonMail/gopenpgp/v2 v2.0.1 github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc @@ -70,5 +70,5 @@ replace ( github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 - golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f + golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c ) diff --git a/go.sum b/go.sum index 571a68ba..80502cb8 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9Ww github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= -github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw= -github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU= +github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig= @@ -15,14 +15,14 @@ github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2Ksf github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= -github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM= -github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= +github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= +github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE= github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= -github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8= -github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g= +github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU= +github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= @@ -148,6 +148,7 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok= diff --git a/internal/bridge/credits.go b/internal/bridge/credits.go index 5202071c..417dcd4c 100644 --- a/internal/bridge/credits.go +++ b/internal/bridge/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Code generated by ./credits.sh at Thu 07 May 2020 08:24:48 PM CEST. DO NOT EDIT. +// Code generated by ./credits.sh at Fri 05 Jun 2020 09:09:44 AM CEST. DO NOT EDIT. package bridge -const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" +const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" diff --git a/internal/bridge/release_notes.go b/internal/bridge/release_notes.go index 7e128ddf..9c310be7 100644 --- a/internal/bridge/release_notes.go +++ b/internal/bridge/release_notes.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Code generated by ./release-notes.sh at Thu 07 May 2020 08:46:50 PM CEST. DO NOT EDIT. +// Code generated by ./release-notes.sh at Tue 09 Jun 2020 10:21:50 AM CEST. DO NOT EDIT. package bridge diff --git a/internal/imap/mailbox_message.go b/internal/imap/mailbox_message.go index 9e4646b0..48fdfe02 100644 --- a/internal/imap/mailbox_message.go +++ b/internal/imap/mailbox_message.go @@ -32,7 +32,7 @@ import ( "text/template" "time" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/imap/cache" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/pkg/message" @@ -82,7 +82,11 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L return errors.New("no available address for encryption") } m.AddressID = addr.ID - kr := addr.KeyRing() + + kr, err := im.user.client().KeyRingForAddressID(addr.ID) + if err != nil { + return err + } // Handle imported messages which have no "Sender" address. // This sometimes occurs with outlook which reports errors as imported emails or for drafts. @@ -184,7 +188,7 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq) } -func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *pmcrypto.KeyRing) (err error) { // nolint[funlen] +func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) { // nolint[funlen] b := &bytes.Buffer{} // Overwrite content for main header for import. @@ -240,7 +244,7 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr * } // Create encrypted writer. - pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil) + pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil) if err != nil { return err } @@ -722,7 +726,7 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt return structure, msgBody, err } -func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *pmcrypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen] +func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen] multipartType, err := im.setMessageContentType(m) if err != nil { return diff --git a/internal/imap/store.go b/internal/imap/store.go index 5cab6e9d..3fab3eb0 100644 --- a/internal/imap/store.go +++ b/internal/imap/store.go @@ -21,7 +21,7 @@ import ( "io" "net/mail" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/pkg/pmapi" @@ -35,7 +35,7 @@ type storeUserProvider interface { GetAddress(addressID string) (storeAddressProvider, error) CreateDraft( - kr *pmcrypto.KeyRing, + kr *crypto.KeyRing, message *pmapi.Message, attachmentReaders []io.Reader, attachedPublicKey, diff --git a/internal/smtp/repro_test.go b/internal/smtp/repro_test.go new file mode 100644 index 00000000..9f30cbf7 --- /dev/null +++ b/internal/smtp/repro_test.go @@ -0,0 +1,76 @@ +// 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 ( + "testing" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" +) + +func TestThing(t *testing.T) { + // Load the key. + key, err := crypto.NewKeyFromArmored(testPublicKey) + if err != nil { + panic(err) + } + + // Put it in a keyring. + keyRing, err := crypto.NewKeyRing(key) + if err != nil { + panic(err) + } + + // Filter out expired ones. + validKeyRings, err := crypto.FilterExpiredKeys([]*crypto.KeyRing{keyRing}) + if err != nil { + panic(err) + } + + // Filtering shouldn't make them unequal. + assert.True(t, isEqual(keyRing, validKeyRings[0])) +} + +func isEqual(a, b *crypto.KeyRing) bool { + if a == nil && b == nil { + return true + } + + if a == nil && b != nil || a != nil && b == nil { + return false + } + + aKeys, bKeys := a.GetKeys(), b.GetKeys() + + if len(aKeys) != len(bKeys) { + return false + } + + for i := range aKeys { + aFPs := aKeys[i].GetSHA256Fingerprints() + bFPs := bKeys[i].GetSHA256Fingerprints() + + if !cmp.Equal(aFPs, bFPs) { + return false + } + } + + return true +} diff --git a/internal/smtp/sending_info.go b/internal/smtp/sending_info.go index a2912609..122319e9 100644 --- a/internal/smtp/sending_info.go +++ b/internal/smtp/sending_info.go @@ -20,7 +20,7 @@ package smtp import ( "errors" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "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" @@ -37,7 +37,7 @@ type SendingInfo struct { Sign bool Scheme int MIMEType string - PublicKey *pmcrypto.KeyRing + PublicKey *crypto.KeyRing } func generateSendingInfo( @@ -46,10 +46,10 @@ func generateSendingInfo( isInternal bool, composeMode string, apiKeys, - contactKeys []*pmcrypto.KeyRing, + contactKeys []*crypto.KeyRing, settingsSign bool, settingsPgpScheme int) (sendingInfo SendingInfo, err error) { - contactKeys, err = pmcrypto.FilterExpiredKeys(contactKeys) + contactKeys, err = crypto.FilterExpiredKeys(contactKeys) if err != nil { return } @@ -72,7 +72,7 @@ func generateInternalSendingInfo( contactMeta *ContactMetadata, composeMode string, apiKeys, - contactKeys []*pmcrypto.KeyRing, + 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. @@ -125,7 +125,7 @@ func generateExternalSendingInfo( contactMeta *ContactMetadata, composeMode string, apiKeys, - contactKeys []*pmcrypto.KeyRing, + contactKeys []*crypto.KeyRing, settingsSign bool, settingsPgpScheme int) (sendingInfo SendingInfo, err error) { // The default settings, unless overridden by presence of a saved contact. @@ -230,14 +230,27 @@ func schemeAndMIME(contact *ContactMetadata, settingsScheme int, settingsMIMETyp // 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 []*pmcrypto.KeyRing) (filteredKeys []*pmcrypto.KeyRing, err error) { //nolint[unparam] +func checkContactKeysAgainstAPI(contactKeys, apiKeys []*crypto.KeyRing) (filteredKeys []*crypto.KeyRing, err error) { //nolint[unparam] keyIDsAreEqual := func(a, b interface{}) bool { - aKey, bKey := a.(*pmcrypto.KeyRing), b.(*pmcrypto.KeyRing) - return aKey.GetEntities()[0].PrimaryKey.KeyId == bKey.GetEntities()[0].PrimaryKey.KeyId + 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.(*pmcrypto.KeyRing)) + filteredKeys = append(filteredKeys, v.(*crypto.KeyRing)) } return diff --git a/internal/smtp/sending_info_test.go b/internal/smtp/sending_info_test.go index 4ef88c5e..1cbf86ac 100644 --- a/internal/smtp/sending_info_test.go +++ b/internal/smtp/sending_info_test.go @@ -18,15 +18,15 @@ package smtp import ( - "strings" "testing" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" ) @@ -46,8 +46,8 @@ func initMocks(t *testing.T) mocks { type args struct { eventListener listener.Listener contactMeta *ContactMetadata - apiKeys []*pmcrypto.KeyRing - contactKeys []*pmcrypto.KeyRing + apiKeys []*crypto.KeyRing + contactKeys []*crypto.KeyRing composeMode string settingsPgpScheme int settingsSign bool @@ -68,18 +68,61 @@ func (tt *testData) runTest(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, gotSendingInfo, tt.wantSendingInfo) + + assert.Equal(t, gotSendingInfo.Encrypt, tt.wantSendingInfo.Encrypt) + assert.Equal(t, gotSendingInfo.Sign, tt.wantSendingInfo.Sign) + assert.Equal(t, gotSendingInfo.Scheme, tt.wantSendingInfo.Scheme) + assert.Equal(t, gotSendingInfo.MIMEType, tt.wantSendingInfo.MIMEType) + assert.True(t, keyRingsAreEqual(gotSendingInfo.PublicKey, tt.wantSendingInfo.PublicKey)) } }) } +func keyRingFromKey(publicKey string) *crypto.KeyRing { + key, err := crypto.NewKeyFromArmored(publicKey) + if err != nil { + panic(err) + } + + kr, err := crypto.NewKeyRing(key) + if err != nil { + panic(err) + } + + return kr +} + +func keyRingsAreEqual(a, b *crypto.KeyRing) bool { + if a == nil && b == nil { + return true + } + + if a == nil && b != nil || a != nil && b == nil { + return false + } + + aKeys, bKeys := a.GetKeys(), b.GetKeys() + + if len(aKeys) != len(bKeys) { + return false + } + + for i := range aKeys { + aFPs := aKeys[i].GetSHA256Fingerprints() + bFPs := bKeys[i].GetSHA256Fingerprints() + + if !cmp.Equal(aFPs, bFPs) { + return false + } + } + + return true +} + func TestGenerateSendingInfo_WithoutContact(t *testing.T) { m := initMocks(t) - pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) - if err != nil { - panic(err) - } + pubKey := keyRingFromKey(testPublicKey) tests := []testData{ { @@ -88,8 +131,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { contactMeta: nil, isInternal: true, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -107,8 +150,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { contactMeta: nil, isInternal: true, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPInlinePackage, }, @@ -126,8 +169,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { contactMeta: nil, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -145,8 +188,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { contactMeta: nil, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPInlinePackage, }, @@ -164,8 +207,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { contactMeta: nil, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: false, settingsPgpScheme: pmapi.PGPInlinePackage, }, @@ -183,8 +226,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { eventListener: m.eventListener, contactMeta: nil, isInternal: true, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{pubKey}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{pubKey}, }, wantSendingInfo: SendingInfo{}, wantErr: true, @@ -195,8 +238,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { contactMeta: nil, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -217,20 +260,11 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) { func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { m := initMocks(t) - pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) - if err != nil { - panic(err) - } + pubKey := keyRingFromKey(testPublicKey) - preferredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) - if err != nil { - panic(err) - } + preferredPubKey := keyRingFromKey(testPublicKey) - differentPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testDifferentPublicKey)) - if err != nil { - panic(err) - } + differentPubKey := keyRingFromKey(testDifferentPublicKey) m.eventListener.EXPECT().Emit(events.NoActiveKeyForRecipientEvent, "badkey@email.com") @@ -241,8 +275,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, isInternal: true, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -260,8 +294,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, isInternal: true, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{pubKey}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{pubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -279,8 +313,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, isInternal: true, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{preferredPubKey}, - contactKeys: []*pmcrypto.KeyRing{pubKey}, + apiKeys: []*crypto.KeyRing{preferredPubKey}, + contactKeys: []*crypto.KeyRing{pubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -299,8 +333,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { contactMeta: &ContactMetadata{Email: "badkey@email.com", Encrypt: true, Scheme: "pgp-mime"}, isInternal: true, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{differentPubKey}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{differentPubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -313,8 +347,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -332,8 +366,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{pubKey}, - contactKeys: []*pmcrypto.KeyRing{differentPubKey}, + apiKeys: []*crypto.KeyRing{pubKey}, + contactKeys: []*crypto.KeyRing{differentPubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -352,15 +386,9 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { } func TestGenerateSendingInfo_Contact_External(t *testing.T) { - pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) - if err != nil { - panic(err) - } + pubKey := keyRingFromKey(testPublicKey) - expiredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testExpiredPublicKey)) - if err != nil { - panic(err) - } + expiredPubKey := keyRingFromKey(testExpiredPublicKey) tests := []testData{ { @@ -369,8 +397,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -388,8 +416,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{expiredPubKey}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{expiredPubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -407,8 +435,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{pubKey}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{pubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -426,8 +454,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{pubKey}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{pubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -445,8 +473,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{Encrypt: true}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{pubKey}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{pubKey}, settingsSign: true, settingsPgpScheme: pmapi.PGPMIMEPackage, }, @@ -464,8 +492,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPInlinePackage, }, @@ -483,8 +511,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPInlinePackage, }, @@ -502,8 +530,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) { contactMeta: &ContactMetadata{SignMissing: true}, isInternal: false, composeMode: pmapi.ContentTypeHTML, - apiKeys: []*pmcrypto.KeyRing{}, - contactKeys: []*pmcrypto.KeyRing{}, + apiKeys: []*crypto.KeyRing{}, + contactKeys: []*crypto.KeyRing{}, settingsSign: true, settingsPgpScheme: pmapi.PGPInlinePackage, }, diff --git a/internal/smtp/store.go b/internal/smtp/store.go index aeee30c6..875abe01 100644 --- a/internal/smtp/store.go +++ b/internal/smtp/store.go @@ -20,13 +20,13 @@ package smtp import ( "io" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) type storeUserProvider interface { CreateDraft( - kr *pmcrypto.KeyRing, + kr *crypto.KeyRing, message *pmapi.Message, attachmentReaders []io.Reader, attachedPublicKey, diff --git a/internal/smtp/user.go b/internal/smtp/user.go index 9d3e7d7a..8d431d66 100644 --- a/internal/smtp/user.go +++ b/internal/smtp/user.go @@ -20,7 +20,6 @@ package smtp import ( - "bytes" "encoding/base64" "fmt" "io" @@ -32,7 +31,7 @@ import ( "strings" "time" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/message" @@ -93,16 +92,26 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err err = errors.New("backend: invalid email address: not owned by user") return } - kr := addr.KeyRing() + + kr, err := su.client().KeyRingForAddressID(addr.ID) + if err != nil { + return + } var attachedPublicKey string var attachedPublicKeyName string if mailSettings.AttachPublicKey > 0 { - attachedPublicKey, err = kr.GetArmoredPublicKey() + firstKey, err := kr.GetKey(0) if err != nil { return err } - attachedPublicKeyName = "publickey - " + kr.Identities()[0].Name + + attachedPublicKey, err = firstKey.GetArmoredPublicKey() + if err != nil { + return err + } + + attachedPublicKeyName = "publickey - " + kr.GetIdentities()[0].Name } message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName) @@ -171,7 +180,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err atts = append(atts, message.Attachments...) // Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. - attkeys := make(map[string]*pmcrypto.SymmetricKey) + attkeys := make(map[string]*crypto.SessionKey) attkeysEncoded := make(map[string]pmapi.AlgoKey) for _, att := range atts { @@ -203,7 +212,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err // PMEL 3. composeMode := message.MIMEType - var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey + var plainKey, htmlKey, mimeKey *crypto.SessionKey var plainData, htmlData, mimeData []byte containsUnencryptedRecipients := false @@ -219,7 +228,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err return err } var contactMeta *ContactMetadata - var contactKeys []*pmcrypto.KeyRing + var contactKeyRings []*crypto.KeyRing for _, contactEmail := range contactEmails { if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_ continue @@ -236,12 +245,19 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err if err != nil { return err } + contactKeyRing, err := crypto.NewKeyRing(nil) + if err != nil { + return err + } for _, contactRawKey := range contactMeta.Keys { - contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey)) + contactKey, err := crypto.NewKeyFromArmored(contactRawKey) if err != nil { return err } - contactKeys = append(contactKeys, contactKey) + if err := contactKeyRing.AddKey(contactKey); err != nil { + return err + } + contactKeyRings = append(contactKeyRings, contactKeyRing) } break // We take the first hit where Defaults == 0, see "How to find the right contact" of PMEL @@ -254,16 +270,22 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err return err } - var apiKeys []*pmcrypto.KeyRing + var apiKeyRings []*crypto.KeyRing for _, apiRawKey := range apiRawKeyList { - var kr *pmcrypto.KeyRing - if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil { + key, err := crypto.NewKeyFromArmored(apiRawKey.PublicKey) + if err != nil { return err } - apiKeys = append(apiKeys, kr) + + kr, err := crypto.NewKeyRing(key) + if err != nil { + return err + } + + apiKeyRings = append(apiKeyRings, kr) } - sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme) + sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeyRings, contactKeyRings, settingsSign, settingsPgpScheme) if !sendingInfo.Encrypt { containsUnencryptedRecipients = true } @@ -284,7 +306,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err } } if sendingInfo.Scheme == pmapi.PGPMIMEPackage { - mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*pmcrypto.SymmetricKey{}) + mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*crypto.SessionKey{}) if err != nil { return err } diff --git a/internal/smtp/utils.go b/internal/smtp/utils.go index 0f3d38ba..1fa295db 100644 --- a/internal/smtp/utils.go +++ b/internal/smtp/utils.go @@ -21,7 +21,7 @@ import ( "encoding/base64" "regexp" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) @@ -37,9 +37,9 @@ func looksLikeEmail(e string) bool { } func createPackets( - pubkey *pmcrypto.KeyRing, - bodyKey *pmcrypto.SymmetricKey, - attkeys map[string]*pmcrypto.SymmetricKey, + pubkey *crypto.KeyRing, + bodyKey *crypto.SessionKey, + attkeys map[string]*crypto.SessionKey, ) (bodyPacket string, attachmentPackets map[string]string, err error) { // Encrypt message body keys. packetBytes, err := pubkey.EncryptSessionKey(bodyKey) @@ -61,24 +61,33 @@ func createPackets( } func encryptSymmetric( - kr *pmcrypto.KeyRing, + kr *crypto.KeyRing, textToEncrypt string, canonicalizeText bool, // nolint[unparam] -) (key *pmcrypto.SymmetricKey, symEncryptedData []byte, err error) { +) (key *crypto.SessionKey, symEncryptedData []byte, err error) { // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - pgpMessage, err := kr.FirstKey().Encrypt(pmcrypto.NewPlainMessageFromString(textToEncrypt), kr) + firstKey, err := kr.FirstKey() if err != nil { return } + + pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr) + if err != nil { + return + } + pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0) if err != nil { return } + key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket()) if err != nil { return } + symEncryptedData = pgpSplitMessage.GetBinaryDataPacket() + return } @@ -87,7 +96,7 @@ func buildPackage( sharedScheme int, mimeType string, bodyData []byte, - bodyKey *pmcrypto.SymmetricKey, + bodyKey *crypto.SessionKey, attKeys map[string]pmapi.AlgoKey, ) (pkg *pmapi.MessagePackage) { if len(addressMap) == 0 { diff --git a/internal/store/user_message.go b/internal/store/user_message.go index 4b58910b..7ea629e1 100644 --- a/internal/store/user_message.go +++ b/internal/store/user_message.go @@ -26,7 +26,7 @@ import ( "net/textproto" "strings" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -37,7 +37,7 @@ import ( // If `attachedPublicKey` is passed, it's added to attachments. // Both draft and attachments are encrypted with passed `kr` key. func (store *Store) CreateDraft( - kr *pmcrypto.KeyRing, + kr *crypto.KeyRing, message *pmapi.Message, attachmentReaders []io.Reader, attachedPublicKey, @@ -92,7 +92,7 @@ func (store *Store) getDraftAction(message *pmapi.Message) int { return pmapi.DraftActionReply } -func (store *Store) createAttachment(kr *pmcrypto.KeyRing, attachment *pmapi.Attachment, attachmentBody []byte) (*pmapi.Attachment, error) { +func (store *Store) createAttachment(kr *crypto.KeyRing, attachment *pmapi.Attachment, attachmentBody []byte) (*pmapi.Attachment, error) { r := bytes.NewReader(attachmentBody) sigReader, err := attachment.DetachedSign(kr, r) if err != nil { diff --git a/internal/users/user.go b/internal/users/user.go index 9e217ed7..69c0a4b9 100644 --- a/internal/users/user.go +++ b/internal/users/user.go @@ -53,9 +53,6 @@ type User struct { lock sync.RWMutex isAuthorized bool - - unlockingKeyringLock sync.Mutex - wasKeyringUnlocked bool } // newUser creates a new user. @@ -99,10 +96,6 @@ func (u *User) client() pmapi.Client { // if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if // something in the store changed). func (u *User) init(idleUpdates chan imapBackend.Update) (err error) { - u.unlockingKeyringLock.Lock() - u.wasKeyringUnlocked = false - u.unlockingKeyringLock.Unlock() - u.log.Info("Initialising user") // Reload the user's credentials (if they log out and back in we need the new @@ -197,22 +190,14 @@ func (u *User) authorizeIfNecessary(emitEvent bool) (err error) { // unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked. func (u *User) unlockIfNecessary() error { - u.unlockingKeyringLock.Lock() - defer u.unlockingKeyringLock.Unlock() - - if u.wasKeyringUnlocked { + if u.client().IsUnlocked() { return nil } - if _, err := u.client().Unlock(u.creds.MailboxPassword); err != nil { + if err := u.client().Unlock([]byte(u.creds.MailboxPassword)); err != nil { return errors.Wrap(err, "failed to unlock user") } - if err := u.client().UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil { - return errors.Wrap(err, "failed to unlock user addresses") - } - - u.wasKeyringUnlocked = true return nil } @@ -228,14 +213,10 @@ func (u *User) authorizeAndUnlock() (err error) { return errors.Wrap(err, "failed to refresh API auth") } - if _, err = u.client().Unlock(u.creds.MailboxPassword); err != nil { + if err := u.client().Unlock([]byte(u.creds.MailboxPassword)); err != nil { return errors.Wrap(err, "failed to unlock user") } - if err = u.client().UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil { - return errors.Wrap(err, "failed to unlock user addresses") - } - return nil } @@ -432,12 +413,8 @@ func (u *User) UpdateUser() error { return err } - if _, err = u.client().Unlock(u.creds.MailboxPassword); err != nil { - return err - } - - if err := u.client().UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil { - return err + if err = u.client().Unlock([]byte(u.creds.MailboxPassword)); err != nil { + return errors.Wrap(err, "failed to unlock user") } emails := u.client().Addresses().ActiveEmails() @@ -514,10 +491,6 @@ func (u *User) Logout() (err error) { return } - u.unlockingKeyringLock.Lock() - u.wasKeyringUnlocked = false - u.unlockingKeyringLock.Unlock() - u.client().Logout() if err = u.credStorer.Logout(u.userID); err != nil { diff --git a/internal/users/user_credentials_test.go b/internal/users/user_credentials_test.go index 9521078b..23dc08cc 100644 --- a/internal/users/user_credentials_test.go +++ b/internal/users/user_credentials_test.go @@ -35,12 +35,11 @@ func TestUpdateUser(t *testing.T) { defer cleanUpUserData(user) gomock.InOrder( - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), + m.pmapiClient.EXPECT().IsUnlocked().Return(false), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil), - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}), @@ -155,8 +154,8 @@ func TestCheckBridgeLoginOK(t *testing.T) { defer cleanUpUserData(user) gomock.InOrder( - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), + m.pmapiClient.EXPECT().IsUnlocked().Return(false), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), ) err := user.CheckBridgeLogin(testCredentials.BridgePassword) @@ -166,6 +165,28 @@ func TestCheckBridgeLoginOK(t *testing.T) { assert.NoError(t, err) } +func TestCheckBridgeLoginTwiceOK(t *testing.T) { + m := initMocks(t) + defer m.ctrl.Finish() + + user := testNewUser(m) + defer cleanUpUserData(user) + + gomock.InOrder( + m.pmapiClient.EXPECT().IsUnlocked().Return(false), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), + m.pmapiClient.EXPECT().IsUnlocked().Return(true), + ) + + err := user.CheckBridgeLogin(testCredentials.BridgePassword) + waitForEvents() + assert.NoError(t, err) + + err = user.CheckBridgeLogin(testCredentials.BridgePassword) + waitForEvents() + assert.NoError(t, err) +} + func TestCheckBridgeLoginUpgradeApplication(t *testing.T) { m := initMocks(t) defer m.ctrl.Finish() @@ -220,8 +241,8 @@ func TestCheckBridgeLoginBadPassword(t *testing.T) { defer cleanUpUserData(user) gomock.InOrder( - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), + m.pmapiClient.EXPECT().IsUnlocked().Return(false), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), ) err := user.CheckBridgeLogin("wrong!") diff --git a/internal/users/user_new_test.go b/internal/users/user_new_test.go index c70e0f8f..7bac94f5 100644 --- a/internal/users/user_new_test.go +++ b/internal/users/user_new_test.go @@ -114,33 +114,7 @@ func TestNewUserUnlockFails(t *testing.T) { m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password")), - m.credentialsStore.EXPECT().Logout("user").Return(nil), - m.pmapiClient.EXPECT().Logout(), - m.credentialsStore.EXPECT().Logout("user").Return(nil), - m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil), - ) - - checkNewUserHasCredentials(testCredentialsDisconnected, m) -} - -func TestNewUserUnlockAddressesFails(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) - - m.eventListener.EXPECT().Emit(events.LogoutEvent, "user") - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") - - gomock.InOrder( - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), - m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), - - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password")), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(errors.New("bad password")), m.credentialsStore.EXPECT().Logout("user").Return(nil), m.pmapiClient.EXPECT().Logout(), m.credentialsStore.EXPECT().Logout("user").Return(nil), diff --git a/internal/users/users.go b/internal/users/users.go index d2679494..ee8c839e 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -182,17 +182,8 @@ func (u *Users) closeAllConnections() { } } -// Login authenticates a user. -// The login flow: -// * Authenticate user: -// client, auth, err := users.Authenticate(username, password) -// -// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user. -// auth2FA, err := client.Auth2FA(twoFactorCode) -// -// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password` -// and then finish the login procedure. -// user, err := users.FinishLogin(client, auth, mailboxPassword) +// Login authenticates a user by username/password, returning an authorised client and an auth object. +// The authorisation scope may not yet be full if the user has 2FA enabled. func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) { u.crashBandicoot(username) @@ -214,7 +205,6 @@ func (u *Users) Login(username, password string) (authClient pmapi.Client, auth } // FinishLogin finishes the login procedure and adds the user into the credentials store. -// See `Login` for more details of the login flow. func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen] defer func() { if err == pmapi.ErrUpgradeApplication { @@ -230,20 +220,22 @@ func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPasswor authClient.Logout() }() - apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword) + apiUser, passphrase, err := getAPIUser(authClient, mbPassword) if err != nil { log.WithError(err).Error("Failed to get API user") return } + log.Info("Got API user") + var ok bool if user, ok = u.hasUser(apiUser.ID); ok { - if err = u.connectExistingUser(user, auth, hashedPassword); err != nil { + if err = u.connectExistingUser(user, auth, passphrase); err != nil { log.WithError(err).Error("Failed to connect existing user") return } } else { - if err = u.addNewUser(apiUser, auth, hashedPassword); err != nil { + if err = u.addNewUser(apiUser, auth, passphrase); err != nil { log.WithError(err).Error("Failed to add new user") return } @@ -255,13 +247,15 @@ func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPasswor } // connectExistingUser connects an existing user. -func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword string) (err error) { +func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, passphrase string) (err error) { if user.IsConnected() { return errors.New("user is already connected") } + log.Info("Connecting existing user") + // Update the user's password in the cred store in case they changed it. - if err = u.credStorer.UpdatePassword(user.ID(), hashedPassword); err != nil { + if err = u.credStorer.UpdatePassword(user.ID(), passphrase); err != nil { return errors.Wrap(err, "failed to update password of user in credentials store") } @@ -283,7 +277,7 @@ func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword } // addNewUser adds a new user. -func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword string) (err error) { +func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, passphrase string) (err error) { u.lock.Lock() defer u.lock.Unlock() @@ -299,7 +293,7 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword activeEmails := client.Addresses().ActiveEmails() - if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassword, activeEmails); err != nil { + if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), passphrase, activeEmails); err != nil { return errors.Wrap(err, "failed to add user to credentials store") } @@ -321,21 +315,27 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword return err } -func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) { - hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt) +func getAPIUser(client pmapi.Client, mbPassword string) (user *pmapi.User, passphrase string, err error) { + salt, err := client.AuthSalt() + if err != nil { + log.WithError(err).Error("Could not get salt") + return + } + + passphrase, err = pmapi.HashMailboxPassword(mbPassword, salt) if err != nil { log.WithError(err).Error("Could not hash mailbox password") return } // We unlock the user's PGP key here to detect if the user's mailbox password is wrong. - if _, err = client.Unlock(hashedPassword); err != nil { + if err = client.Unlock([]byte(passphrase)); err != nil { log.WithError(err).Error("Wrong mailbox password") return } if user, err = client.CurrentUser(); err != nil { - log.WithError(err).Error("Could not load API user") + log.WithError(err).Error("Could not load user data") return } diff --git a/internal/users/users_login_test.go b/internal/users/users_login_test.go index b16d50d1..a976ac5b 100644 --- a/internal/users/users_login_test.go +++ b/internal/users/users_login_test.go @@ -39,7 +39,8 @@ func TestUsersFinishLoginBadMailboxPassword(t *testing.T) { m.credentialsStore.EXPECT().List().Return([]string{}, nil), // Set up mocks for FinishLogin. - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, err), + m.pmapiClient.EXPECT().AuthSalt().Return("", nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(err), m.pmapiClient.EXPECT().DeleteAuth(), m.pmapiClient.EXPECT().Logout(), ) @@ -57,7 +58,8 @@ func TestUsersFinishLoginUpgradeApplication(t *testing.T) { m.credentialsStore.EXPECT().List().Return([]string{}, nil), // Set up mocks for FinishLogin. - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, pmapi.ErrUpgradeApplication), + m.pmapiClient.EXPECT().AuthSalt().Return("", nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(pmapi.ErrUpgradeApplication), m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, ""), m.pmapiClient.EXPECT().DeleteAuth().Return(err), @@ -70,7 +72,6 @@ func TestUsersFinishLoginUpgradeApplication(t *testing.T) { func refreshWithToken(token string) *pmapi.Auth { return &pmapi.Auth{ RefreshToken: token, - KeySalt: "", // No salting in tests. } } @@ -93,7 +94,8 @@ func TestUsersFinishLoginNewUser(t *testing.T) { m.credentialsStore.EXPECT().List().Return([]string{}, nil), // getAPIUser() loads user info from API (e.g. userID). - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), + m.pmapiClient.EXPECT().AuthSalt().Return("", nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil), // addNewUser() @@ -106,8 +108,7 @@ func TestUsersFinishLoginNewUser(t *testing.T) { // user.init() in addNewUser m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil), m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil), - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), // store.New() in user.init m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), @@ -158,7 +159,8 @@ func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) { m.pmapiClient.EXPECT().Addresses().Return(nil), // getAPIUser() loads user info from API (e.g. userID). - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), + m.pmapiClient.EXPECT().AuthSalt().Return("", nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil), // connectExistingUser() @@ -169,8 +171,7 @@ func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) { // user.init() in connectExistingUser m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil), m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil), - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), // store.New() in user.init m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), @@ -206,7 +207,8 @@ func TestUsersFinishLoginConnectedUser(t *testing.T) { // Then, try to log in again... gomock.InOrder( - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), + m.pmapiClient.EXPECT().AuthSalt().Return("", nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil), m.pmapiClient.EXPECT().DeleteAuth(), m.pmapiClient.EXPECT().Logout(), diff --git a/internal/users/users_new_test.go b/internal/users/users_new_test.go index f151800f..6b085c8a 100644 --- a/internal/users/users_new_test.go +++ b/internal/users/users_new_test.go @@ -93,8 +93,7 @@ func mockConnectedUser(m mocks) { m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), - m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil), // Set up mocks for store initialisation for the authorized user. m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), diff --git a/internal/users/users_test.go b/internal/users/users_test.go index 10110a42..997a700f 100644 --- a/internal/users/users_test.go +++ b/internal/users/users_test.go @@ -49,11 +49,9 @@ func TestMain(m *testing.M) { var ( testAuth = &pmapi.Auth{ //nolint[gochecknoglobals] RefreshToken: "tok", - KeySalt: "", // No salting in tests. } testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals] RefreshToken: "reftok", - KeySalt: "", // No salting in tests. } testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals] @@ -209,8 +207,7 @@ func testNewUsersWithUsers(t *testing.T, m mocks) *Users { m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), @@ -219,8 +216,7 @@ func testNewUsersWithUsers(t *testing.T, m mocks) *Users { m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil), m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), - m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), - m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), + m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses), diff --git a/pkg/message/body.go b/pkg/message/body.go index f16c569c..283fc7f3 100644 --- a/pkg/message/body.go +++ b/pkg/message/body.go @@ -23,13 +23,13 @@ import ( "io" "mime/quotedprintable" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/emersion/go-textwrapper" openpgperrors "golang.org/x/crypto/openpgp/errors" ) -func WriteBody(w io.Writer, kr *pmcrypto.KeyRing, m *pmapi.Message) error { +func WriteBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message) error { // Decrypt body. if err := m.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired { return err @@ -46,7 +46,7 @@ func WriteBody(w io.Writer, kr *pmcrypto.KeyRing, m *pmapi.Message) error { return err } -func WriteAttachmentBody(w io.Writer, kr *pmcrypto.KeyRing, m *pmapi.Message, att *pmapi.Attachment, r io.Reader) (err error) { +func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att *pmapi.Attachment, r io.Reader) (err error) { // Decrypt it var dr io.Reader dr, err = att.Decrypt(r, kr) diff --git a/pkg/message/testdata/text_plain_bad_sender.eml b/pkg/message/testdata/text_plain_bad_sender.eml new file mode 100644 index 00000000..5e65e444 --- /dev/null +++ b/pkg/message/testdata/text_plain_bad_sender.eml @@ -0,0 +1,4 @@ +From: ì¹èø¾ýáíé +To: Receiver + +body \ No newline at end of file diff --git a/pkg/message/testdata/text_plain_bad_subject.eml b/pkg/message/testdata/text_plain_bad_subject.eml new file mode 100644 index 00000000..326963dd --- /dev/null +++ b/pkg/message/testdata/text_plain_bad_subject.eml @@ -0,0 +1,5 @@ +From: Sender +To: Receiver +Subject: ì¹èø¾ýáíé + +body \ No newline at end of file diff --git a/pkg/message/testdata/text_plain_plain_attachment_latin1.eml b/pkg/message/testdata/text_plain_plain_attachment_latin1.eml new file mode 100644 index 00000000..111d0c14 --- /dev/null +++ b/pkg/message/testdata/text_plain_plain_attachment_latin1.eml @@ -0,0 +1,12 @@ +From: Sender +To: Receiver +Content-Type: multipart/mixed; boundary=longrandomstring + +--longrandomstring + +body +--longrandomstring +Content-Disposition: attachment + +Aurélien is a latin1 name. +--longrandomstring-- \ No newline at end of file diff --git a/pkg/message/testdata/text_plain_plain_attachment_latin2.eml b/pkg/message/testdata/text_plain_plain_attachment_latin2.eml new file mode 100644 index 00000000..eb1430c5 --- /dev/null +++ b/pkg/message/testdata/text_plain_plain_attachment_latin2.eml @@ -0,0 +1,12 @@ +From: Sender +To: Receiver +Content-Type: multipart/mixed; boundary=longrandomstring + +--longrandomstring + +body +--longrandomstring +Content-Disposition: attachment + +Aurélien is a latin1 name but this document is latin2. +--longrandomstring-- \ No newline at end of file diff --git a/pkg/pmapi/addresses.go b/pkg/pmapi/addresses.go index 8042db08..0573407a 100644 --- a/pkg/pmapi/addresses.go +++ b/pkg/pmapi/addresses.go @@ -19,10 +19,9 @@ package pmapi import ( "errors" - "fmt" "strings" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) // Address statuses. @@ -86,11 +85,6 @@ type AddressesRes struct { Addresses AddressList } -// KeyRing returns the (possibly unlocked) PMKeys KeyRing. -func (a *Address) KeyRing() *pmcrypto.KeyRing { - return a.Keys.KeyRing -} - // ByID returns an address by id. Returns nil if no address is found. func (l AddressList) ByID(id string) *Address { for _, addr := range l { @@ -202,31 +196,33 @@ func (c *client) Addresses() AddressList { return c.addresses } -// UnlockAddresses unlocks all keys for all addresses of current user. -func (c *client) UnlockAddresses(passphrase []byte) (err error) { +// unlockAddresses unlocks all keys for all addresses of current user. +func (c *client) unlockAddresses(passphrase []byte) (err error) { for _, a := range c.addresses { if a.HasKeys == MissingKeys { continue } - // Unlock the address token using the UserKey, use the unlocked token to unlock the keyring. - if err = a.Keys.unlockKeyRing(c.kr, passphrase, c.keyLocker); err != nil { - err = fmt.Errorf("pmapi: cannot unlock private key of address %v: %v", a.Email, err) + if c.addrKeyRing[a.ID] != nil { + continue + } + + var kr *crypto.KeyRing + + if kr, err = a.Keys.UnlockAll(passphrase, c.userKeyRing); err != nil { return } + + c.addrKeyRing[a.ID] = kr } return } -func (c *client) KeyRingForAddressID(addrID string) (*pmcrypto.KeyRing, error) { - if addr := c.addresses.ByID(addrID); addr != nil { - return addr.KeyRing(), nil +func (c *client) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) { + if kr, ok := c.addrKeyRing[addrID]; ok { + return kr, nil } - if addr := c.addresses.Main(); addr != nil { - return addr.KeyRing(), nil - } - - return nil, errors.New("no such address ID") + return nil, errors.New("no keyring available") } diff --git a/pkg/pmapi/addresses_test.go b/pkg/pmapi/addresses_test.go index 41863e3d..19ca4d9a 100644 --- a/pkg/pmapi/addresses_test.go +++ b/pkg/pmapi/addresses_test.go @@ -52,12 +52,6 @@ func routeGetAddresses(tb testing.TB, w http.ResponseWriter, r *http.Request) st return "addresses/get_response.json" } -func routeGetSalts(tb testing.TB, w http.ResponseWriter, r *http.Request) string { - Ok(tb, checkMethodAndPath(r, "GET", "/keys/salts")) - Ok(tb, isAuthReq(r, testUID, testAccessToken)) - return "keys/salts/get_response.json" -} - func TestAddressList(t *testing.T) { input := "1" addr := testAddressList.ByID(input) diff --git a/pkg/pmapi/attachments.go b/pkg/pmapi/attachments.go index 8daefb97..bbf44f53 100644 --- a/pkg/pmapi/attachments.go +++ b/pkg/pmapi/attachments.go @@ -26,7 +26,7 @@ import ( "mime/multipart" "net/textproto" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) type header textproto.MIMEHeader @@ -114,7 +114,7 @@ func (a *Attachment) UnmarshalJSON(b []byte) error { } // Decrypt decrypts this attachment's data from r using the keys from kr. -func (a *Attachment) Decrypt(r io.Reader, kr *pmcrypto.KeyRing) (decrypted io.Reader, err error) { +func (a *Attachment) Decrypt(r io.Reader, kr *crypto.KeyRing) (decrypted io.Reader, err error) { keyPackets, err := base64.StdEncoding.DecodeString(a.KeyPackets) if err != nil { return @@ -123,11 +123,11 @@ func (a *Attachment) Decrypt(r io.Reader, kr *pmcrypto.KeyRing) (decrypted io.Re } // Encrypt encrypts an attachment. -func (a *Attachment) Encrypt(kr *pmcrypto.KeyRing, att io.Reader) (encrypted io.Reader, err error) { +func (a *Attachment) Encrypt(kr *crypto.KeyRing, att io.Reader) (encrypted io.Reader, err error) { return encryptAttachment(kr, att, a.Name) } -func (a *Attachment) DetachedSign(kr *pmcrypto.KeyRing, att io.Reader) (signed io.Reader, err error) { +func (a *Attachment) DetachedSign(kr *crypto.KeyRing, att io.Reader) (signed io.Reader, err error) { return signAttachment(kr, att) } diff --git a/pkg/pmapi/auth.go b/pkg/pmapi/auth.go index fb550c25..58b81da6 100644 --- a/pkg/pmapi/auth.go +++ b/pkg/pmapi/auth.go @@ -25,7 +25,6 @@ import ( "net/http" "strings" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/proton-bridge/pkg/srp" ) @@ -112,7 +111,6 @@ type Auth struct { Scope string uid string // Read from AuthRes. RefreshToken string - KeySalt string EventID string PasswordMode int TwoFA *TwoFactorInfo `json:"2FA,omitempty"` @@ -145,10 +143,6 @@ func (s *Auth) HasMailboxPassword() bool { return s.PasswordMode == 2 } -func (s *Auth) hasFullScope() bool { - return strings.Contains(s.Scope, "full") -} - type AuthRes struct { Res Auth @@ -327,15 +321,6 @@ func (c *client) Auth(username, password string, info *AuthInfo) (auth *Auth, er auth = authRes.getAuth() c.sendAuth(auth) - // Auth has to be fully unlocked to get key salt. During `Auth` it can happen - // only to accounts without 2FA. For 2FA accounts, it's done in `Auth2FA`. - if auth.hasFullScope() { - err = c.setKeySaltToAuth(auth) - if err != nil { - return nil, err - } - } - return auth, err } @@ -367,55 +352,9 @@ func (c *client) Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error) { } } - if err := c.setKeySaltToAuth(auth); err != nil { - return nil, err - } - return auth2FARes.getAuth2FA(), nil } -func (c *client) setKeySaltToAuth(auth *Auth) error { - // KeySalt already set up, no need to do it again. - if auth.KeySalt != "" { - return nil - } - - user, err := c.CurrentUser() - if err != nil { - return err - } - salts, err := c.GetKeySalts() - if err != nil { - return err - } - for _, s := range salts { - if s.ID == user.KeyRing().FirstKeyID { - auth.KeySalt = s.KeySalt - break - } - } - return nil -} - -// Unlock decrypts the key ring. -// If the password is invalid, IsUnlockError(err) will return true. -func (c *client) Unlock(password string) (kr *pmcrypto.KeyRing, err error) { - if _, err = c.CurrentUser(); err != nil { - return - } - - c.keyLocker.Lock() - defer c.keyLocker.Unlock() - - kr = c.user.KeyRing() - if err = unlockKeyRingNoErrorWhenAlreadyUnlocked(kr, []byte(password)); err != nil { - return - } - - c.kr = kr - return kr, err -} - // AuthRefresh will refresh an expired access token. func (c *client) AuthRefresh(uidAndRefreshToken string) (auth *Auth, err error) { // If we don't yet have a saved access token, save this one in case the refresh fails! @@ -466,6 +405,25 @@ func (c *client) AuthRefresh(uidAndRefreshToken string) (auth *Auth, err error) return auth, err } +func (c *client) AuthSalt() (string, error) { + salts, err := c.GetKeySalts() + if err != nil { + return "", err + } + + if _, err := c.CurrentUser(); err != nil { + return "", err + } + + for _, s := range salts { + if s.ID == c.user.Keys[0].ID { + return s.KeySalt, nil + } + } + + return "", errors.New("no matching salt found") +} + // Logout instructs the client manager to log this client out. func (c *client) Logout() { c.cm.LogoutClient(c.userID) @@ -499,7 +457,18 @@ func (c *client) IsConnected() bool { func (c *client) ClearData() { c.uid = "" c.accessToken = "" - c.kr = nil c.addresses = nil c.user = nil + + if c.userKeyRing != nil { + c.userKeyRing.ClearPrivateParams() + c.userKeyRing = nil + } + + for addrID, addr := range c.addrKeyRing { + if addr != nil { + addr.ClearPrivateParams() + delete(c.addrKeyRing, addrID) + } + } } diff --git a/pkg/pmapi/auth_test.go b/pkg/pmapi/auth_test.go index 7a602cec..a6ff8b0f 100644 --- a/pkg/pmapi/auth_test.go +++ b/pkg/pmapi/auth_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/srp" "github.com/sirupsen/logrus" @@ -33,7 +33,7 @@ import ( r "github.com/stretchr/testify/require" ) -var testIdentity = &pmcrypto.Identity{ +var testIdentity = &crypto.Identity{ Name: "UserID", Email: "", } @@ -131,22 +131,16 @@ func TestClient_Auth(t *testing.T) { return "/auth/post_response.json" }, - routeGetUsers, - routeGetAddresses, - routeGetSalts, ) defer finish() auth, err := c.Auth(testUsername, testAPIPassword, testAuthInfo) r.Nil(t, err) - r.True(t, c.user.KeyRing().FirstKeyID != "", "Parsing First key ID issue") - exp := &Auth{} *exp = *testAuth exp.accessToken = testAccessToken exp.RefreshToken = testRefreshToken - exp.KeySalt = "abc" a.Equal(t, exp, auth) } @@ -161,9 +155,6 @@ func TestClient_Auth2FA(t *testing.T) { return "/auth/2fa/post_response.json" }, - routeGetUsers, - routeGetAddresses, - routeGetSalts, ) defer finish() @@ -224,16 +215,16 @@ func TestClient_Unlock(t *testing.T) { c.uid = testUID c.accessToken = testAccessToken - _, err := c.Unlock("wrong") - a.True(t, IsUnlockError(err), "expected error, pasword is wrong") + err := c.Unlock([]byte("wrong")) + a.Error(t, err, "expected error, pasword is wrong") - _, err = c.Unlock(testMailboxPassword) + err = c.Unlock([]byte(testMailboxPassword)) a.Nil(t, err) a.Equal(t, testUID, c.uid) a.Equal(t, testAccessToken, c.accessToken) // second try should not fail because there is an unlocked key already - _, err = c.Unlock("wrong") + err = c.Unlock([]byte("wrong")) a.Nil(t, err) } @@ -246,7 +237,7 @@ func TestClient_Unlock_EncPrivKey(t *testing.T) { c.uid = testUID c.accessToken = testAccessToken - _, err := c.Unlock(testMailboxPassword) + err := c.Unlock([]byte(testMailboxPassword)) Ok(t, err) Equals(t, testUID, c.uid) Equals(t, testAccessToken, c.accessToken) @@ -280,7 +271,6 @@ func TestClient_AuthRefresh(t *testing.T) { *exp = *testAuth exp.uid = testUID // AuthRefresh will not return UID (only Auth returns the UID) we should set testUID to be able to generate token, see `GetToken` exp.accessToken = testAccessToken - exp.KeySalt = "" exp.EventID = "" exp.ExpiresIn = 360000 exp.RefreshToken = testRefreshTokenNew @@ -313,7 +303,6 @@ func TestClient_AuthRefresh_HasUID(t *testing.T) { exp := &Auth{} *exp = *testAuth exp.accessToken = testAccessToken - exp.KeySalt = "" exp.EventID = "" exp.ExpiresIn = 360000 exp.RefreshToken = testRefreshTokenNew @@ -336,7 +325,7 @@ func TestClient_Logout(t *testing.T) { c.Logout() r.Eventually(t, func() bool { - return c.IsConnected() == false && c.kr == nil && c.addresses == nil && c.user == nil + return c.IsConnected() == false && c.userKeyRing == nil && c.addresses == nil && c.user == nil }, 10*time.Second, 10*time.Millisecond) } diff --git a/pkg/pmapi/client.go b/pkg/pmapi/client.go index 0095a982..00b70e3f 100644 --- a/pkg/pmapi/client.go +++ b/pkg/pmapi/client.go @@ -32,7 +32,7 @@ import ( "sync" "time" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/jaytaylor/html2text" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -104,63 +104,6 @@ type ClientConfig struct { MinBytesPerSecond int64 } -// Client defines the interface of a PMAPI client. -type Client interface { - Auth(username, password string, info *AuthInfo) (*Auth, error) - AuthInfo(username string) (*AuthInfo, error) - AuthRefresh(token string) (*Auth, error) - Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error) - Logout() - DeleteAuth() error - IsConnected() bool - ClearData() - - CurrentUser() (*User, error) - UpdateUser() (*User, error) - Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error) - UnlockAddresses(passphrase []byte) error - - GetAddresses() (addresses AddressList, err error) - Addresses() AddressList - ReorderAddresses(addressIDs []string) error - - GetEvent(eventID string) (*Event, error) - - SendMessage(string, *SendMessageReq) (sent, parent *Message, err error) - CreateDraft(m *Message, parent string, action int) (created *Message, err error) - Import([]*ImportMsgReq) ([]*ImportMsgRes, error) - - CountMessages(addressID string) ([]*MessagesCount, error) - ListMessages(filter *MessagesFilter) ([]*Message, int, error) - GetMessage(apiID string) (*Message, error) - DeleteMessages(apiIDs []string) error - LabelMessages(apiIDs []string, labelID string) error - UnlabelMessages(apiIDs []string, labelID string) error - MarkMessagesRead(apiIDs []string) error - MarkMessagesUnread(apiIDs []string) error - - ListLabels() ([]*Label, error) - CreateLabel(label *Label) (*Label, error) - UpdateLabel(label *Label) (*Label, error) - DeleteLabel(labelID string) error - EmptyFolder(labelID string, addressID string) error - - ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error - SendSimpleMetric(category, action, label string) error - - GetMailSettings() (MailSettings, error) - GetContactEmailByEmail(string, int, int) ([]ContactEmail, error) - GetContactByID(string) (Contact, error) - DecryptAndVerifyCards([]Card) ([]Card, error) - - GetAttachment(id string) (att io.ReadCloser, err error) - CreateAttachment(att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error) - DeleteAttachment(attID string) (err error) - - KeyRingForAddressID(string) (kr *pmcrypto.KeyRing, err error) - GetPublicKeysForEmail(string) ([]PublicKey, bool, error) -} - // client is a client of the protonmail API. It implements the Client interface. type client struct { cm *ClientManager @@ -170,11 +113,12 @@ type client struct { accessToken string userID string requestLocker sync.Locker - keyLocker sync.Locker - user *User - addresses AddressList - kr *pmcrypto.KeyRing + user *User + addresses AddressList + userKeyRing *crypto.KeyRing + addrKeyRing map[string]*crypto.KeyRing + keyRingLock sync.Locker log *logrus.Entry } @@ -186,7 +130,8 @@ func newClient(cm *ClientManager, userID string) *client { hc: getHTTPClient(cm.config, cm.roundTripper), userID: userID, requestLocker: &sync.Mutex{}, - keyLocker: &sync.Mutex{}, + keyRingLock: &sync.Mutex{}, + addrKeyRing: make(map[string]*crypto.KeyRing), log: logrus.WithField("pkg", "pmapi").WithField("userID", userID), } } @@ -199,6 +144,39 @@ func getHTTPClient(cfg *ClientConfig, rt http.RoundTripper) (hc *http.Client) { } } +func (c *client) IsUnlocked() bool { + return c.userKeyRing != nil +} + +// Unlock unlocks all the user and address keys using the given passphrase. +func (c *client) Unlock(passphrase []byte) (err error) { + c.keyRingLock.Lock() + defer c.keyRingLock.Unlock() + + // If the user already has a keyring, we already unlocked, so no need to try again. + if c.userKeyRing != nil { + return + } + + if _, err = c.CurrentUser(); err != nil { + return + } + + if c.user == nil || c.addresses == nil { + return errors.New("user data is not loaded") + } + + if err = c.unlockUser(passphrase); err != nil { + return errors.Wrap(err, "failed to unlock user") + } + + if err = c.unlockAddresses(passphrase); err != nil { + return errors.Wrap(err, "failed to unlock addresses") + } + + return +} + // Do makes an API request. It does not check for HTTP status code errors. func (c *client) Do(req *http.Request, retryUnauthorized bool) (res *http.Response, err error) { // Copy the request body in case we need to retry it. @@ -258,7 +236,7 @@ func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthori resDate := res.Header.Get("Date") if resDate != "" { if serverTime, err := http.ParseTime(resDate); err == nil { - pmcrypto.GetGopenPGP().UpdateTime(serverTime.Unix()) + crypto.UpdateTime(serverTime.Unix()) } } diff --git a/pkg/pmapi/client_types.go b/pkg/pmapi/client_types.go new file mode 100644 index 00000000..9c8c9806 --- /dev/null +++ b/pkg/pmapi/client_types.go @@ -0,0 +1,82 @@ +// 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 pmapi + +import ( + "io" + + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +// Client defines the interface of a PMAPI client. +type Client interface { + Auth(username, password string, info *AuthInfo) (*Auth, error) + AuthInfo(username string) (*AuthInfo, error) + AuthRefresh(token string) (*Auth, error) + Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error) + AuthSalt() (salt string, err error) + Logout() + DeleteAuth() error + IsConnected() bool + ClearData() + + CurrentUser() (*User, error) + UpdateUser() (*User, error) + Unlock(passphrase []byte) (err error) + IsUnlocked() bool + + GetAddresses() (addresses AddressList, err error) + Addresses() AddressList + ReorderAddresses(addressIDs []string) error + + GetEvent(eventID string) (*Event, error) + + SendMessage(string, *SendMessageReq) (sent, parent *Message, err error) + CreateDraft(m *Message, parent string, action int) (created *Message, err error) + Import([]*ImportMsgReq) ([]*ImportMsgRes, error) + + CountMessages(addressID string) ([]*MessagesCount, error) + ListMessages(filter *MessagesFilter) ([]*Message, int, error) + GetMessage(apiID string) (*Message, error) + DeleteMessages(apiIDs []string) error + LabelMessages(apiIDs []string, labelID string) error + UnlabelMessages(apiIDs []string, labelID string) error + MarkMessagesRead(apiIDs []string) error + MarkMessagesUnread(apiIDs []string) error + + ListLabels() ([]*Label, error) + CreateLabel(label *Label) (*Label, error) + UpdateLabel(label *Label) (*Label, error) + DeleteLabel(labelID string) error + EmptyFolder(labelID string, addressID string) error + + ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error + SendSimpleMetric(category, action, label string) error + + GetMailSettings() (MailSettings, error) + GetContactEmailByEmail(string, int, int) ([]ContactEmail, error) + GetContactByID(string) (Contact, error) + DecryptAndVerifyCards([]Card) ([]Card, error) + + GetAttachment(id string) (att io.ReadCloser, err error) + CreateAttachment(att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error) + DeleteAttachment(attID string) (err error) + + KeyRingForAddressID(string) (kr *crypto.KeyRing, err error) + GetPublicKeysForEmail(string) ([]PublicKey, bool, error) +} diff --git a/pkg/pmapi/contacts_test.go b/pkg/pmapi/contacts_test.go index 887103f1..c2f73e9c 100644 --- a/pkg/pmapi/contacts_test.go +++ b/pkg/pmapi/contacts_test.go @@ -655,7 +655,7 @@ var testCardsCleartext = []Card{ func TestClient_Encrypt(t *testing.T) { c := newTestClient(newTestClientManager(testClientConfig)) - c.kr = testPrivateKeyRing + c.userKeyRing = testPrivateKeyRing cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext) assert.Nil(t, err) @@ -669,7 +669,7 @@ func TestClient_Encrypt(t *testing.T) { func TestClient_Decrypt(t *testing.T) { c := newTestClient(newTestClientManager(testClientConfig)) - c.kr = testPrivateKeyRing + c.userKeyRing = testPrivateKeyRing cardCleartext, err := c.DecryptAndVerifyCards(testCardsEncrypted) assert.Nil(t, err) diff --git a/pkg/pmapi/key.go b/pkg/pmapi/key.go index 656be0f4..00428079 100644 --- a/pkg/pmapi/key.go +++ b/pkg/pmapi/key.go @@ -21,9 +21,8 @@ import ( "fmt" "net/http" "net/url" - "strings" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) // Flags @@ -46,12 +45,13 @@ type PublicKey struct { } // PublicKeys returns the public keys of the given email addresses. -func (c *client) PublicKeys(emails []string) (keys map[string]*pmcrypto.KeyRing, err error) { +func (c *client) PublicKeys(emails []string) (keys map[string]*crypto.Key, err error) { if len(emails) == 0 { err = fmt.Errorf("pmapi: cannot get public keys: no email address provided") return } - keys = map[string]*pmcrypto.KeyRing{} + + keys = make(map[string]*crypto.Key) for _, email := range emails { email = url.QueryEscape(email) @@ -66,13 +66,15 @@ func (c *client) PublicKeys(emails []string) (keys map[string]*pmcrypto.KeyRing, return } - for _, key := range res.Keys { - if key.Flags&UseToEncryptFlag == UseToEncryptFlag { - var kr *pmcrypto.KeyRing - if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(key.PublicKey)); err != nil { + for _, rawKey := range res.Keys { + if rawKey.Flags&UseToEncryptFlag == UseToEncryptFlag { + var key *crypto.Key + + if key, err = crypto.NewKeyFromArmored(rawKey.PublicKey); err != nil { return } - keys[email] = kr + + keys[email] = key } } } diff --git a/pkg/pmapi/keyring.go b/pkg/pmapi/keyring.go index 0014c27c..3cad0e0a 100644 --- a/pkg/pmapi/keyring.go +++ b/pkg/pmapi/keyring.go @@ -20,183 +20,152 @@ package pmapi import ( "bytes" "encoding/json" - "errors" - "fmt" "io" "io/ioutil" - "sync" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) -// clearableKey is a region of memory intended to hold a private key and which can be securely -// cleared by calling clear(). -type clearableKey []byte - -// UnmarshalJSON Removes quotation and unescapes CR, LF. -func (pk *clearableKey) UnmarshalJSON(b []byte) (err error) { - b = bytes.Trim(b, "\"") - b = bytes.ReplaceAll(b, []byte("\\n"), []byte("\n")) - b = bytes.ReplaceAll(b, []byte("\\r"), []byte("\r")) - *pk = b - return -} - -// clear irreversibly destroys the full range of `clearableKey` by filling it with zeros to ensure -// nobody can see what was in there (e.g. while waiting for the garbage collector to clean it up). -func (pk *clearableKey) clear() { - for b := range *pk { - (*pk)[b] = 0 - } -} - type PMKey struct { ID string Version int Flags int Fingerprint string + PrivateKey *crypto.Key Primary int Token *string `json:",omitempty"` Signature *string `json:",omitempty"` } -type PMKeys struct { - Keys []PMKey - KeyRing *pmcrypto.KeyRing -} +func (key *PMKey) UnmarshalJSON(b []byte) (err error) { + type _PMKey PMKey -func (k *PMKeys) UnmarshalJSON(b []byte) (err error) { - var rawKeys []struct { - PMKey - PrivateKey clearableKey - } - if err = json.Unmarshal(b, &rawKeys); err != nil { + rawKey := struct { + _PMKey + PrivateKey string + }{} + + if err = json.Unmarshal(b, &rawKey); err != nil { return } - k.KeyRing = &pmcrypto.KeyRing{} - for _, rawKey := range rawKeys { - err = k.KeyRing.ReadFrom(bytes.NewReader(rawKey.PrivateKey), true) - rawKey.PrivateKey.clear() - if err != nil { - return - } - k.Keys = append(k.Keys, rawKey.PMKey) - } - if len(k.Keys) > 0 { - k.KeyRing.FirstKeyID = k.Keys[0].ID + *key = PMKey(rawKey._PMKey) + + if key.PrivateKey, err = crypto.NewKeyFromArmored(rawKey.PrivateKey); err != nil { + return errors.Wrap(err, "failed to create crypto key from armored private key") } + return } -// unlockKeyRing tries to unlock them with the provided keyRing using the token -// and if the token is not available it will use passphrase. It will not fail -// if keyring contains at least one unlocked private key. -func (k *PMKeys) unlockKeyRing(userKeyring *pmcrypto.KeyRing, passphrase []byte, locker sync.Locker) (err error) { - locker.Lock() - defer locker.Unlock() +func (key PMKey) getPassphraseFromToken(kr *crypto.KeyRing) (passphrase []byte, err error) { + if kr == nil { + return nil, errors.New("no user key was provided") + } - if k == nil { - err = errors.New("keys is a nil object") + msg, err := crypto.NewPGPMessageFromArmored(*key.Token) + if err != nil { return } - for _, key := range k.Keys { + sig, err := crypto.NewPGPSignatureFromArmored(*key.Signature) + if err != nil { + return + } + + token, err := kr.Decrypt(msg, nil, 0) + if err != nil { + return + } + + if err = kr.VerifyDetached(token, sig, 0); err != nil { + return + } + + return token.GetBinary(), nil +} + +func (key PMKey) unlock(passphrase []byte) (unlockedKey *crypto.Key, err error) { + if unlockedKey, err = key.PrivateKey.Unlock(passphrase); err != nil { + return + } + + ok, err := unlockedKey.Check() + if err != nil { + return + } + if !ok { + err = errors.New("private and public keys do not match") + return + } + + return +} + +type PMKeys []PMKey + +// UnlockAll goes through each key and unlocks it, returning a keyring containing all unlocked keys, +// or an error if at least one could not be unlocked. +// The passphrase is used to unlock the key unless the key's token and signature are both non-nil, +// in which case the given userkey is used to deduce the passphrase. +func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *crypto.KeyRing, err error) { + if kr, err = crypto.NewKeyRing(nil); err != nil { + return + } + + for _, key := range *keys { + var secret []byte + if key.Token == nil || key.Signature == nil { - if err = unlockKeyRingNoErrorWhenAlreadyUnlocked(k.KeyRing, passphrase); err != nil { - return - } + secret = passphrase + } else if secret, err = key.getPassphraseFromToken(userKey); err != nil { + return + } + + var k *crypto.Key + + if k, err = key.unlock(secret); err != nil { + logrus.WithError(err).Warn("Failed to unlock key") continue } - message, err := pmcrypto.NewPGPMessageFromArmored(*key.Token) - if err != nil { - return err - } - - signature, err := pmcrypto.NewPGPSignatureFromArmored(*key.Signature) - if err != nil { - return err - } - - if userKeyring == nil { - return errors.New("userkey required to decrypt tokens but wasn't provided") - } - token, err := userKeyring.Decrypt(message, nil, 0) - if err != nil { - return err - } - - err = userKeyring.VerifyDetached(token, signature, 0) - if err != nil { - return err - } - - err = unlockKeyRingNoErrorWhenAlreadyUnlocked(k.KeyRing, token.GetBinary()) - if err != nil { - return fmt.Errorf("wrong token: %v", err) + if err = kr.AddKey(k); err != nil { + logrus.WithError(err).Warn("Failed to add key to keyring") + continue } } - return nil -} - -type unlockError struct { - error -} - -func (err *unlockError) Error() string { - return "Invalid mailbox password (" + err.error.Error() + ")" -} - -// IsUnlockError checks whether the error is due to failure to unlock (which is represented by an unexported type). -func IsUnlockError(err error) bool { - _, ok := err.(*unlockError) - return ok -} - -func unlockKeyRingNoErrorWhenAlreadyUnlocked(kr *pmcrypto.KeyRing, passphrase []byte) (err error) { - if err = kr.Unlock(passphrase); err != nil { - // Do not fail if it has already unlocked keys. - hasUnlockedKey := false - for _, e := range kr.GetEntities() { - if e.PrivateKey != nil && !e.PrivateKey.Encrypted { - hasUnlockedKey = true - break - } - for _, se := range e.Subkeys { - if se.PrivateKey != nil && (!se.Sig.FlagsValid || se.Sig.FlagEncryptStorage || se.Sig.FlagEncryptCommunications) && !e.PrivateKey.Encrypted { - hasUnlockedKey = true - break - } - } - if hasUnlockedKey { - break - } - } - if !hasUnlockedKey { - err = &unlockError{err} - return - } - err = nil + if kr.CountEntities() == 0 { + err = errors.New("no keys could be unlocked") + return } - return + + return kr, err } // ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities. var ErrNoKeyringAvailable = errors.New("no keyring available") -func (c *client) encrypt(plain string, signer *pmcrypto.KeyRing) (armored string, err error) { - return encrypt(c.kr, plain, signer) +func (c *client) encrypt(plain string, signer *crypto.KeyRing) (armored string, err error) { + return encrypt(c.userKeyRing, plain, signer) } -func encrypt(encrypter *pmcrypto.KeyRing, plain string, signer *pmcrypto.KeyRing) (armored string, err error) { - if encrypter == nil || encrypter.FirstKey() == nil { +func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (armored string, err error) { + if encrypter == nil { return "", ErrNoKeyringAvailable } - plainMessage := pmcrypto.NewPlainMessageFromString(plain) + + firstKey, err := encrypter.FirstKey() + if err != nil { + return "", err + } + + plainMessage := crypto.NewPlainMessageFromString(plain) + // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - pgpMessage, err := encrypter.FirstKey().Encrypt(plainMessage, signer) + pgpMessage, err := firstKey.Encrypt(plainMessage, signer) if err != nil { return } @@ -204,14 +173,14 @@ func encrypt(encrypter *pmcrypto.KeyRing, plain string, signer *pmcrypto.KeyRing } func (c *client) decrypt(armored string) (plain string, err error) { - return decrypt(c.kr, armored) + return decrypt(c.userKeyRing, armored) } -func decrypt(decrypter *pmcrypto.KeyRing, armored string) (plainBody string, err error) { +func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody string, err error) { if decrypter == nil { return "", ErrNoKeyringAvailable } - pgpMessage, err := pmcrypto.NewPGPMessageFromArmored(armored) + pgpMessage, err := crypto.NewPGPMessageFromArmored(armored) if err != nil { return } @@ -223,11 +192,11 @@ func decrypt(decrypter *pmcrypto.KeyRing, armored string) (plainBody string, err } func (c *client) sign(plain string) (armoredSignature string, err error) { - if c.kr == nil { + if c.userKeyRing == nil { return "", ErrNoKeyringAvailable } - plainMessage := pmcrypto.NewPlainMessageFromString(plain) - pgpSignature, err := c.kr.SignDetached(plainMessage) + plainMessage := crypto.NewPlainMessageFromString(plain) + pgpSignature, err := c.userKeyRing.SignDetached(plainMessage) if err != nil { return } @@ -235,34 +204,44 @@ func (c *client) sign(plain string) (armoredSignature string, err error) { } func (c *client) verify(plain, amroredSignature string) (err error) { - plainMessage := pmcrypto.NewPlainMessageFromString(plain) - pgpSignature, err := pmcrypto.NewPGPSignatureFromArmored(amroredSignature) + plainMessage := crypto.NewPlainMessageFromString(plain) + pgpSignature, err := crypto.NewPGPSignatureFromArmored(amroredSignature) if err != nil { return } verifyTime := int64(0) // By default it will use current timestamp. - return c.kr.VerifyDetached(plainMessage, pgpSignature, verifyTime) + return c.userKeyRing.VerifyDetached(plainMessage, pgpSignature, verifyTime) } -func encryptAttachment(kr *pmcrypto.KeyRing, data io.Reader, filename string) (encrypted io.Reader, err error) { - if kr == nil || kr.FirstKey() == nil { +func encryptAttachment(kr *crypto.KeyRing, data io.Reader, filename string) (encrypted io.Reader, err error) { + if kr == nil { return nil, ErrNoKeyringAvailable } + + firstKey, err := kr.FirstKey() + if err != nil { + return nil, err + } + dataBytes, err := ioutil.ReadAll(data) if err != nil { return } - plainMessage := pmcrypto.NewPlainMessage(dataBytes) + + plainMessage := crypto.NewPlainMessage(dataBytes) + // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - pgpSplitMessage, err := kr.FirstKey().EncryptAttachment(plainMessage, filename) + pgpSplitMessage, err := firstKey.EncryptAttachment(plainMessage, filename) if err != nil { return } + packets := append(pgpSplitMessage.KeyPacket, pgpSplitMessage.DataPacket...) + return bytes.NewReader(packets), nil } -func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader) (decrypted io.Reader, err error) { +func decryptAttachment(kr *crypto.KeyRing, keyPackets []byte, data io.Reader) (decrypted io.Reader, err error) { if kr == nil { return nil, ErrNoKeyringAvailable } @@ -270,7 +249,7 @@ func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader) if err != nil { return } - pgpSplitMessage := pmcrypto.NewPGPSplitMessage(keyPackets, dataBytes) + pgpSplitMessage := crypto.NewPGPSplitMessage(keyPackets, dataBytes) plainMessage, err := kr.DecryptAttachment(pgpSplitMessage) if err != nil { return @@ -278,7 +257,7 @@ func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader) return plainMessage.NewReader(), nil } -func signAttachment(encrypter *pmcrypto.KeyRing, data io.Reader) (signature io.Reader, err error) { +func signAttachment(encrypter *crypto.KeyRing, data io.Reader) (signature io.Reader, err error) { if encrypter == nil { return nil, ErrNoKeyringAvailable } @@ -286,7 +265,7 @@ func signAttachment(encrypter *pmcrypto.KeyRing, data io.Reader) (signature io.R if err != nil { return } - plainMessage := pmcrypto.NewPlainMessage(dataBytes) + plainMessage := crypto.NewPlainMessage(dataBytes) sig, err := encrypter.SignDetached(plainMessage) if err != nil { return diff --git a/pkg/pmapi/keyring_test.go b/pkg/pmapi/keyring_test.go index 1ec07ad0..4777ccf3 100644 --- a/pkg/pmapi/keyring_test.go +++ b/pkg/pmapi/keyring_test.go @@ -19,11 +19,9 @@ package pmapi import ( "encoding/json" - "strings" - "sync" "testing" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/stretchr/testify/assert" ) @@ -38,11 +36,16 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { addrKeysPrimaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysPrimaryHasToken_JSON", false)) addrKeysSecondaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysSecondaryHasToken_JSON", false)) - userKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_userKey", false))) + key, err := crypto.NewKeyFromArmored(readTestFile("keyring_userKey", false)) + if err != nil { + panic(err) + } + + userKey, err := crypto.NewKeyRing(key) assert.NoError(t, err, "Expected not to receive an error unlocking user key") type args struct { - userKeyring *pmcrypto.KeyRing + userKeyring *crypto.KeyRing passphrase []byte } tests := []struct { @@ -73,21 +76,26 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tempLocker := &sync.Mutex{} - - err := tt.keys.unlockKeyRing(tt.args.userKeyring, tt.args.passphrase, tempLocker) // nolint[scopelint] + kr, err := tt.keys.UnlockAll(tt.args.passphrase, tt.args.userKeyring) // nolint[scopelint] if !assert.NoError(t, err) { return } // assert at least one key has been decrypted atLeastOneDecrypted := false - for _, e := range tt.keys.KeyRing.GetEntities() { // nolint[scopelint] - if !e.PrivateKey.Encrypted { + + for _, k := range kr.GetKeys() { // nolint[scopelint] + ok, err := k.IsUnlocked() + if err != nil { + panic(err) + } + + if ok { atLeastOneDecrypted = true break } } + assert.True(t, atLeastOneDecrypted) }) } diff --git a/pkg/pmapi/messages.go b/pkg/pmapi/messages.go index b97d3e02..82c6c13a 100644 --- a/pkg/pmapi/messages.go +++ b/pkg/pmapi/messages.go @@ -31,7 +31,7 @@ import ( "strconv" "strings" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "golang.org/x/crypto/openpgp/packet" ) @@ -250,7 +250,7 @@ func (m *Message) IsLegacyMessage() bool { strings.Contains(m.Body, MessageTail) } -func (m *Message) Decrypt(kr *pmcrypto.KeyRing) (err error) { +func (m *Message) Decrypt(kr *crypto.KeyRing) (err error) { if m.IsLegacyMessage() { return m.DecryptLegacy(kr) } @@ -269,7 +269,7 @@ func (m *Message) Decrypt(kr *pmcrypto.KeyRing) (err error) { return } -func (m *Message) DecryptLegacy(kr *pmcrypto.KeyRing) (err error) { +func (m *Message) DecryptLegacy(kr *crypto.KeyRing) (err error) { randomKeyStart := strings.Index(m.Body, RandomKeyHeader) + len(RandomKeyHeader) randomKeyEnd := strings.Index(m.Body, RandomKeyTail) randomKey := m.Body[randomKeyStart:randomKeyEnd] @@ -341,7 +341,7 @@ func decodeBase64UTF8(input string) (output []byte, err error) { return } -func (m *Message) Encrypt(encrypter, signer *pmcrypto.KeyRing) (err error) { +func (m *Message) Encrypt(encrypter, signer *crypto.KeyRing) (err error) { if m.IsBodyEncrypted() { err = errors.New("pmapi: trying to encrypt an already encrypted message") return diff --git a/pkg/pmapi/messages_test.go b/pkg/pmapi/messages_test.go index e7772ecc..3d53ab17 100644 --- a/pkg/pmapi/messages_test.go +++ b/pkg/pmapi/messages_test.go @@ -20,10 +20,9 @@ package pmapi import ( "fmt" "net/http" - "strings" "testing" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/stretchr/testify/assert" ) @@ -142,10 +141,15 @@ func TestMessage_Decrypt(t *testing.T) { func TestMessage_Decrypt_Legacy(t *testing.T) { testPrivateKeyLegacy := readTestFile("testPrivateKeyLegacy", false) - testPrivateKeyRingLegacy, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPrivateKeyLegacy)) + + key, err := crypto.NewKeyFromArmored(testPrivateKeyLegacy) Ok(t, err) - Ok(t, testPrivateKeyRingLegacy.Unlock([]byte(testMailboxPasswordLegacy))) + unlockedKey, err := key.Unlock([]byte(testMailboxPasswordLegacy)) + Ok(t, err) + + testPrivateKeyRingLegacy, err := crypto.NewKeyRing(unlockedKey) + Ok(t, err) msg := &Message{Body: testMessageEncryptedLegacy} @@ -163,7 +167,10 @@ func TestMessage_Decrypt_signed(t *testing.T) { } func TestMessage_Encrypt(t *testing.T) { - signer, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testMessageSigner)) + key, err := crypto.NewKeyFromArmored(testMessageSigner) + Ok(t, err) + + signer, err := crypto.NewKeyRing(key) Ok(t, err) msg := &Message{Body: testMessageCleartext} @@ -173,7 +180,7 @@ func TestMessage_Encrypt(t *testing.T) { Ok(t, err) Equals(t, testMessageCleartext, msg.Body) - Equals(t, testIdentity, signer.Identities()[0]) + Equals(t, testIdentity, signer.GetIdentities()[0]) } func routeLabelMessages(tb testing.TB, w http.ResponseWriter, r *http.Request) string { diff --git a/pkg/pmapi/mocks/mocks.go b/pkg/pmapi/mocks/mocks.go index e6af7c32..18807126 100644 --- a/pkg/pmapi/mocks/mocks.go +++ b/pkg/pmapi/mocks/mocks.go @@ -8,7 +8,7 @@ import ( io "io" reflect "reflect" - crypto "github.com/ProtonMail/gopenpgp/crypto" + crypto "github.com/ProtonMail/gopenpgp/v2/crypto" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" gomock "github.com/golang/mock/gomock" ) @@ -110,6 +110,21 @@ func (mr *MockClientMockRecorder) AuthRefresh(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRefresh", reflect.TypeOf((*MockClient)(nil).AuthRefresh), arg0) } +// AuthSalt mocks base method +func (m *MockClient) AuthSalt() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthSalt") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AuthSalt indicates an expected call of AuthSalt +func (mr *MockClientMockRecorder) AuthSalt() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthSalt", reflect.TypeOf((*MockClient)(nil).AuthSalt)) +} + // ClearData mocks base method func (m *MockClient) ClearData() { m.ctrl.T.Helper() @@ -432,6 +447,20 @@ func (mr *MockClientMockRecorder) IsConnected() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockClient)(nil).IsConnected)) } +// IsUnlocked mocks base method +func (m *MockClient) IsUnlocked() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsUnlocked") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsUnlocked indicates an expected call of IsUnlocked +func (mr *MockClientMockRecorder) IsUnlocked() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUnlocked", reflect.TypeOf((*MockClient)(nil).IsUnlocked)) +} + // KeyRingForAddressID mocks base method func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) { m.ctrl.T.Helper() @@ -605,12 +634,11 @@ func (mr *MockClientMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomoc } // Unlock mocks base method -func (m *MockClient) Unlock(arg0 string) (*crypto.KeyRing, error) { +func (m *MockClient) Unlock(arg0 []byte) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Unlock", arg0) - ret0, _ := ret[0].(*crypto.KeyRing) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // Unlock indicates an expected call of Unlock @@ -619,20 +647,6 @@ func (mr *MockClientMockRecorder) Unlock(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockClient)(nil).Unlock), arg0) } -// UnlockAddresses mocks base method -func (m *MockClient) UnlockAddresses(arg0 []byte) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UnlockAddresses", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UnlockAddresses indicates an expected call of UnlockAddresses -func (mr *MockClientMockRecorder) UnlockAddresses(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlockAddresses", reflect.TypeOf((*MockClient)(nil).UnlockAddresses), arg0) -} - // UpdateLabel mocks base method func (m *MockClient) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) { m.ctrl.T.Helper() diff --git a/pkg/pmapi/passwords.go b/pkg/pmapi/passwords.go index 73410091..9a2f12c0 100644 --- a/pkg/pmapi/passwords.go +++ b/pkg/pmapi/passwords.go @@ -24,12 +24,12 @@ import ( "github.com/jameskeane/bcrypt" ) -func HashMailboxPassword(password, keySalt string) (hashedPassword string, err error) { - if keySalt == "" { +func HashMailboxPassword(password, salt string) (hashedPassword string, err error) { + if salt == "" { hashedPassword = password return } - decodedSalt, err := base64.StdEncoding.DecodeString(keySalt) + decodedSalt, err := base64.StdEncoding.DecodeString(salt) if err != nil { return } diff --git a/pkg/pmapi/pmapi_test.go b/pkg/pmapi/pmapi_test.go index 17248cb9..6fcfcbd8 100644 --- a/pkg/pmapi/pmapi_test.go +++ b/pkg/pmapi/pmapi_test.go @@ -21,15 +21,15 @@ import ( "io/ioutil" "strings" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) const testMailboxPassword = "apple" const testMailboxPasswordLegacy = "123" var ( - testPrivateKeyRing *pmcrypto.KeyRing - testPublicKeyRing *pmcrypto.KeyRing + testPrivateKeyRing *crypto.KeyRing + testPublicKeyRing *crypto.KeyRing ) func init() { @@ -37,15 +37,27 @@ func init() { testPublicKey := readTestFile("testPublicKey", false) var err error - if testPrivateKeyRing, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPrivateKey)); err != nil { + + privKey, err := crypto.NewKeyFromArmored(testPrivateKey) + if err != nil { panic(err) } - if testPublicKeyRing, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)); err != nil { + privKeyUnlocked, err := privKey.Unlock([]byte(testMailboxPassword)) + if err != nil { panic(err) } - if err := testPrivateKeyRing.Unlock([]byte(testMailboxPassword)); err != nil { + pubKey, err := crypto.NewKeyFromArmored(testPublicKey) + if err != nil { + panic(err) + } + + if testPrivateKeyRing, err = crypto.NewKeyRing(privKeyUnlocked); err != nil { + panic(err) + } + + if testPublicKeyRing, err = crypto.NewKeyRing(pubKey); err != nil { panic(err) } } diff --git a/pkg/pmapi/users.go b/pkg/pmapi/users.go index 3e20c63d..92b6cd70 100644 --- a/pkg/pmapi/users.go +++ b/pkg/pmapi/users.go @@ -18,8 +18,8 @@ package pmapi import ( - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/getsentry/raven-go" + "github.com/pkg/errors" ) // Role values. @@ -68,15 +68,17 @@ type User struct { Private int Subscribed int Services int - VPN struct { + Deliquent int + + Keys PMKeys + + VPN struct { Status int ExpirationTime int PlanName string MaxConnect int MaxTier int } - Deliquent int - Keys PMKeys } // UserRes holds structure of JSON response. @@ -86,9 +88,17 @@ type UserRes struct { User *User } -// KeyRing returns the (possibly unlocked) PMKeys KeyRing. -func (u *User) KeyRing() *pmcrypto.KeyRing { - return u.Keys.KeyRing +// unlockUser unlocks all the client's user keys using the given passphrase. +func (c *client) unlockUser(passphrase []byte) (err error) { + if c.userKeyRing != nil { + return + } + + if c.userKeyRing, err = c.user.Keys.UnlockAll(passphrase, nil); err != nil { + return errors.Wrap(err, "failed to unlock user keys") + } + + return } // UpdateUser retrieves details about user and loads its addresses. diff --git a/pkg/pmapi/users_test.go b/pkg/pmapi/users_test.go index 9671f3ac..814c7559 100644 --- a/pkg/pmapi/users_test.go +++ b/pkg/pmapi/users_test.go @@ -23,7 +23,7 @@ import ( "net/url" "testing" - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" r "github.com/stretchr/testify/require" @@ -72,9 +72,9 @@ func TestClient_CurrentUser(t *testing.T) { r.Nil(t, err) // Ignore KeyRings during the check because they have unexported fields and cannot be compared - r.True(t, cmp.Equal(user, testCurrentUser, cmpopts.IgnoreTypes(&pmcrypto.KeyRing{}))) + r.True(t, cmp.Equal(user, testCurrentUser, cmpopts.IgnoreTypes(&crypto.Key{}))) - r.Nil(t, c.UnlockAddresses([]byte(testMailboxPassword))) + r.Nil(t, c.Unlock([]byte(testMailboxPassword))) } func TestClient_PublicKeys(t *testing.T) { diff --git a/test/accounts/account.go b/test/accounts/account.go index 94c4e368..35f566e1 100644 --- a/test/accounts/account.go +++ b/test/accounts/account.go @@ -29,9 +29,8 @@ import ( ) const ( - testUserKey = "user_key.json" - testAddressKey = "address_key.json" - testKeyPassphrase = "testpassphrase" + testUserKey = "user_key.json" + testAddressKey = "address_key.json" ) type TestAccount struct { @@ -78,14 +77,9 @@ func newTestAccount( } func (a *TestAccount) initKeys() { - if a.user.Keys.Keys != nil { - return - } userKeys := loadPMKeys(readTestFile(testUserKey)) - _ = userKeys.KeyRing.Unlock([]byte(testKeyPassphrase)) addressKeys := loadPMKeys(readTestFile(testAddressKey)) - _ = addressKeys.KeyRing.Unlock([]byte(testKeyPassphrase)) a.user.Keys = *userKeys for _, addressEmail := range a.Addresses().ActiveEmails() { diff --git a/test/accounts/fake.json b/test/accounts/fake.json index f93bab39..60bea18a 100644 --- a/test/accounts/fake.json +++ b/test/accounts/fake.json @@ -27,7 +27,8 @@ "ID": "userAddress", "Email": "user@pm.me", "Order": 1, - "Receive": 1 + "Receive": 1, + "HasKeys": 1 } }, "user2fa": { @@ -35,7 +36,8 @@ "ID": "user2faAddress", "Email": "user@pm.me", "Order": 1, - "Receive": 1 + "Receive": 1, + "HasKeys": 1 } }, "userAddressWithCapitalLetter": { @@ -43,7 +45,8 @@ "ID": "userAddressWithCapitalLetterAddress", "Email": "uSeR@pm.me", "Order": 1, - "Receive": 1 + "Receive": 1, + "HasKeys": 1 } }, "userMoreAddresses": { @@ -51,13 +54,15 @@ "ID": "primary", "Email": "primaryaddress@pm.me", "Order": 1, - "Receive": 1 + "Receive": 1, + "HasKeys": 1 }, "secondary": { "ID": "secondary", "Email": "secondaryaddress@pm.me", "Order": 2, - "Receive": 1 + "Receive": 1, + "HasKeys": 1 }, "disabled": { "ID": "disabled", @@ -75,9 +80,10 @@ }, "secondary": { "ID": "secondary", - "Email": "user@pm.me", + "Email": "secondaryaddress@pm.me", "Order": 2, - "Receive": 1 + "Receive": 1, + "HasKeys": 1 } } }, @@ -89,11 +95,11 @@ "userDisabledPrimaryAddress": "password" }, "mailboxPasswords": { - "user": "password", - "user2fa": "password", - "userAddressWithCapitalLetter": "password", - "userMoreAddresses": "password", - "userDisabledPrimaryAddress": "password" + "user": "testpassphrase", + "user2fa": "testpassphrase", + "userAddressWithCapitalLetter": "testpassphrase", + "userMoreAddresses": "testpassphrase", + "userDisabledPrimaryAddress": "testpassphrase" }, "twoFAs": { "user": false, @@ -102,4 +108,4 @@ "userMoreAddresses": false, "userDisabledPrimaryAddress": false } -} \ No newline at end of file +} diff --git a/test/fakeapi/auth.go b/test/fakeapi/auth.go index 4cd74951..ce25495a 100644 --- a/test/fakeapi/auth.go +++ b/test/fakeapi/auth.go @@ -147,6 +147,14 @@ func (api *FakePMAPI) AuthRefresh(token string) (*pmapi.Auth, error) { return auth, nil } +func (api *FakePMAPI) AuthSalt() (string, error) { + if err := api.checkInternetAndRecordCall(GET, "/keys/salts", nil); err != nil { + return "", err + } + + return "", nil +} + func (api *FakePMAPI) Logout() { api.controller.clientManager.LogoutClient(api.userID) } @@ -164,5 +172,17 @@ func (api *FakePMAPI) DeleteAuth() error { } func (api *FakePMAPI) ClearData() { + if api.userKeyRing != nil { + api.userKeyRing.ClearPrivateParams() + api.userKeyRing = nil + } + + for addrID, addr := range api.addrKeyRing { + if addr != nil { + addr.ClearPrivateParams() + delete(api.addrKeyRing, addrID) + } + } + api.unsetUser() } diff --git a/test/fakeapi/fakeapi.go b/test/fakeapi/fakeapi.go index f37a2646..7031091c 100644 --- a/test/fakeapi/fakeapi.go +++ b/test/fakeapi/fakeapi.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/sirupsen/logrus" ) @@ -33,12 +34,14 @@ type FakePMAPI struct { controller *Controller eventIDGenerator idGenerator - auths chan<- *pmapi.Auth - user *pmapi.User - addresses *pmapi.AddressList - labels []*pmapi.Label - messages []*pmapi.Message - events []*pmapi.Event + auths chan<- *pmapi.Auth + user *pmapi.User + userKeyRing *crypto.KeyRing + addresses *pmapi.AddressList + addrKeyRing map[string]*crypto.KeyRing + labels []*pmapi.Label + messages []*pmapi.Message + events []*pmapi.Event // uid represents the API UID. It is the unique session ID. uid, lastToken string @@ -48,9 +51,10 @@ type FakePMAPI struct { func New(controller *Controller, userID string) *FakePMAPI { fakePMAPI := &FakePMAPI{ - controller: controller, - log: logrus.WithField("pkg", "fakeapi"), - userID: userID, + controller: controller, + log: logrus.WithField("pkg", "fakeapi"), + userID: userID, + addrKeyRing: make(map[string]*crypto.KeyRing), } fakePMAPI.addEvent(&pmapi.Event{ diff --git a/test/fakeapi/user.go b/test/fakeapi/user.go index 0ad105b1..9f6e69fa 100644 --- a/test/fakeapi/user.go +++ b/test/fakeapi/user.go @@ -18,7 +18,7 @@ package fakeapi import ( - pmcrypto "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) @@ -29,13 +29,37 @@ func (api *FakePMAPI) GetMailSettings() (pmapi.MailSettings, error) { return pmapi.MailSettings{}, nil } -func (api *FakePMAPI) Unlock(mailboxPassword string) (*pmcrypto.KeyRing, error) { - return &pmcrypto.KeyRing{ - FirstKeyID: "key", - }, nil +func (api *FakePMAPI) IsUnlocked() bool { + return api.userKeyRing != nil } -func (api *FakePMAPI) UnlockAddresses(password []byte) error { +func (api *FakePMAPI) Unlock(passphrase []byte) (err error) { + if api.userKeyRing != nil { + return + } + + if api.userKeyRing, err = api.user.Keys.UnlockAll(passphrase, nil); err != nil { + return + } + + for _, a := range *api.addresses { + if a.HasKeys == pmapi.MissingKeys { + continue + } + + if api.addrKeyRing[a.ID] != nil { + continue + } + + var kr *crypto.KeyRing + + if kr, err = a.Keys.UnlockAll(passphrase, api.userKeyRing); err != nil { + return + } + + api.addrKeyRing[a.ID] = kr + } + return nil } @@ -47,6 +71,7 @@ func (api *FakePMAPI) UpdateUser() (*pmapi.User, error) { if err := api.checkAndRecordCall(GET, "/users", nil); err != nil { return nil, err } + return api.user, nil } @@ -84,8 +109,6 @@ func (api *FakePMAPI) Addresses() pmapi.AddressList { return *api.addresses } -func (api *FakePMAPI) KeyRingForAddressID(addrID string) (*pmcrypto.KeyRing, error) { - return &pmcrypto.KeyRing{ - FirstKeyID: "key", - }, nil +func (api *FakePMAPI) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) { + return api.addrKeyRing[addrID], nil } diff --git a/test/features/imap/auth.feature b/test/features/imap/auth.feature index 94ebdc2f..3c3976a5 100644 --- a/test/features/imap/auth.feature +++ b/test/features/imap/auth.feature @@ -71,7 +71,7 @@ Feature: IMAP auth @ignore-live Scenario: Authenticates with disabled primary address Given there is connected user "userDisabledPrimaryAddress" - When IMAP client authenticates "userDisabledPrimaryAddress" with address "primary" + When IMAP client authenticates "userDisabledPrimaryAddress" with address "disabled" Then IMAP response is "OK" Scenario: Authenticates two users diff --git a/test/features/smtp/auth.feature b/test/features/smtp/auth.feature index 3933b4c1..f5e125ef 100644 --- a/test/features/smtp/auth.feature +++ b/test/features/smtp/auth.feature @@ -48,7 +48,7 @@ Feature: SMTP auth @ignore-live Scenario: Authenticates with disabled primary address Given there is connected user "userDisabledPrimaryAddress" - When SMTP client authenticates "userDisabledPrimaryAddress" with address "primary" + When SMTP client authenticates "userDisabledPrimaryAddress" with address "secondary" Then SMTP response is "OK" Scenario: Authenticates two users diff --git a/test/liveapi/messages.go b/test/liveapi/messages.go index c7bdab80..f7232095 100644 --- a/test/liveapi/messages.go +++ b/test/liveapi/messages.go @@ -80,11 +80,10 @@ func buildMessage(client pmapi.Client, message *pmapi.Message) (*bytes.Buffer, e } func encryptMessage(client pmapi.Client, message *pmapi.Message) error { - addresses, err := client.GetAddresses() + kr, err := client.KeyRingForAddressID(message.AddressID) if err != nil { - return errors.Wrap(err, "failed to get address") + return errors.Wrap(err, "failed to get keyring for address") } - kr := addresses.ByID(message.AddressID).KeyRing() if err = message.Encrypt(kr, nil); err != nil { return errors.Wrap(err, "failed to encrypt message body") diff --git a/test/liveapi/users.go b/test/liveapi/users.go index 3d8c77e8..44b273dc 100644 --- a/test/liveapi/users.go +++ b/test/liveapi/users.go @@ -34,21 +34,25 @@ func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, p if err != nil { return errors.Wrap(err, "failed to get auth info") } - auth, err := client.Auth(user.Name, password, authInfo) + + _, err = client.Auth(user.Name, password, authInfo) if err != nil { return errors.Wrap(err, "failed to auth user") } - mailboxPassword, err := pmapi.HashMailboxPassword(password, auth.KeySalt) + salt, err := client.AuthSalt() + if err != nil { + return errors.Wrap(err, "failed to get salt") + } + + mailboxPassword, err := pmapi.HashMailboxPassword(password, salt) if err != nil { return errors.Wrap(err, "failed to hash mailbox password") } - if _, err := client.Unlock(mailboxPassword); err != nil { + + if err := client.Unlock([]byte(mailboxPassword)); err != nil { return errors.Wrap(err, "failed to unlock user") } - if err := client.UnlockAddresses([]byte(mailboxPassword)); err != nil { - return errors.Wrap(err, "failed to unlock addresses") - } if err := cleanup(client, addresses); err != nil { return errors.Wrap(err, "failed to clean user") diff --git a/test/testdata/user_key.json b/test/testdata/user_key.json index 730222a0..8cc2928f 100644 --- a/test/testdata/user_key.json +++ b/test/testdata/user_key.json @@ -2,7 +2,7 @@ { "ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==", "Version": 3, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: OpenPGP.js v0.7.1\r\nComment: http://openpgpjs.org\r\n\r\n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\nqqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==\r\n=2wIY\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n", + "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n\r\n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\r\na7RmMPBR30nWAMrvteun5z2n8rizgEZHcmE=\r\n=2e5K\r\n-----END PGP PRIVATE KEY BLOCK-----", "Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353", "Activation": null, "Primary": 1