feat: migrate to gopenpgp v2

This commit is contained in:
James Houlahan
2020-06-05 09:33:37 +02:00
parent de16f6f2d1
commit c19bb0fa97
54 changed files with 928 additions and 684 deletions

View File

@ -15,6 +15,9 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-308 Better user error message when request is canceled. * GODT-308 Better user error message when request is canceled.
* GODT-312 Validate recipient emails in send before asking for their public keys. * GODT-312 Validate recipient emails in send before asking for their public keys.
* GODT-368 Bump docker-credential-helpers version. * 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 ### Fixed
* GODT-356 Fix crash when removing account while mail client is fetching messages (regression from GODT-204). * GODT-356 Fix crash when removing account while mail client is fetching messages (regression from GODT-204).

4
go.mod
View File

@ -18,7 +18,7 @@ require (
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a 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-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 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/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc 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/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/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 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
) )

13
go.sum
View File

@ -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/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 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= 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-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig= 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-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 h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= 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-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
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/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 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI= 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 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= 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/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
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/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= 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/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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= github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// 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 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;"

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// 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 package bridge

View File

@ -32,7 +32,7 @@ import (
"text/template" "text/template"
"time" "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/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message" "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") return errors.New("no available address for encryption")
} }
m.AddressID = addr.ID 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. // Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts. // 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) 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{} b := &bytes.Buffer{}
// Overwrite content for main header for import. // 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. // Create encrypted writer.
pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil) pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
if err != nil { if err != nil {
return err return err
} }
@ -722,7 +726,7 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
return structure, msgBody, err 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) multipartType, err := im.setMessageContentType(m)
if err != nil { if err != nil {
return return

View File

@ -21,7 +21,7 @@ import (
"io" "io"
"net/mail" "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/imap/uidplus"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -35,7 +35,7 @@ type storeUserProvider interface {
GetAddress(addressID string) (storeAddressProvider, error) GetAddress(addressID string) (storeAddressProvider, error)
CreateDraft( CreateDraft(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
message *pmapi.Message, message *pmapi.Message,
attachmentReaders []io.Reader, attachmentReaders []io.Reader,
attachedPublicKey, attachedPublicKey,

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -20,7 +20,7 @@ package smtp
import ( import (
"errors" "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/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/algo" "github.com/ProtonMail/proton-bridge/pkg/algo"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
@ -37,7 +37,7 @@ type SendingInfo struct {
Sign bool Sign bool
Scheme int Scheme int
MIMEType string MIMEType string
PublicKey *pmcrypto.KeyRing PublicKey *crypto.KeyRing
} }
func generateSendingInfo( func generateSendingInfo(
@ -46,10 +46,10 @@ func generateSendingInfo(
isInternal bool, isInternal bool,
composeMode string, composeMode string,
apiKeys, apiKeys,
contactKeys []*pmcrypto.KeyRing, contactKeys []*crypto.KeyRing,
settingsSign bool, settingsSign bool,
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
contactKeys, err = pmcrypto.FilterExpiredKeys(contactKeys) contactKeys, err = crypto.FilterExpiredKeys(contactKeys)
if err != nil { if err != nil {
return return
} }
@ -72,7 +72,7 @@ func generateInternalSendingInfo(
contactMeta *ContactMetadata, contactMeta *ContactMetadata,
composeMode string, composeMode string,
apiKeys, apiKeys,
contactKeys []*pmcrypto.KeyRing, contactKeys []*crypto.KeyRing,
settingsSign bool, //nolint[unparam] settingsSign bool, //nolint[unparam]
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { //nolint[unparam] settingsPgpScheme int) (sendingInfo SendingInfo, err error) { //nolint[unparam]
// If sending internally, there should always be a public key; if not, there's an error. // If sending internally, there should always be a public key; if not, there's an error.
@ -125,7 +125,7 @@ func generateExternalSendingInfo(
contactMeta *ContactMetadata, contactMeta *ContactMetadata,
composeMode string, composeMode string,
apiKeys, apiKeys,
contactKeys []*pmcrypto.KeyRing, contactKeys []*crypto.KeyRing,
settingsSign bool, settingsSign bool,
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
// The default settings, unless overridden by presence of a saved contact. // 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 // checkContactKeysAgainstAPI keeps only those contact keys which are up to date and have
// an ID that matches an API key's ID. // 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 { keyIDsAreEqual := func(a, b interface{}) bool {
aKey, bKey := a.(*pmcrypto.KeyRing), b.(*pmcrypto.KeyRing) aKey, bKey := a.(*crypto.KeyRing), b.(*crypto.KeyRing)
return aKey.GetEntities()[0].PrimaryKey.KeyId == bKey.GetEntities()[0].PrimaryKey.KeyId
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) { for _, v := range algo.SetIntersection(contactKeys, apiKeys, keyIDsAreEqual) {
filteredKeys = append(filteredKeys, v.(*pmcrypto.KeyRing)) filteredKeys = append(filteredKeys, v.(*crypto.KeyRing))
} }
return return

View File

@ -18,15 +18,15 @@
package smtp package smtp
import ( import (
"strings"
"testing" "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/events"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -46,8 +46,8 @@ func initMocks(t *testing.T) mocks {
type args struct { type args struct {
eventListener listener.Listener eventListener listener.Listener
contactMeta *ContactMetadata contactMeta *ContactMetadata
apiKeys []*pmcrypto.KeyRing apiKeys []*crypto.KeyRing
contactKeys []*pmcrypto.KeyRing contactKeys []*crypto.KeyRing
composeMode string composeMode string
settingsPgpScheme int settingsPgpScheme int
settingsSign bool settingsSign bool
@ -68,18 +68,61 @@ func (tt *testData) runTest(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
assert.NoError(t, err) 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) { func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
m := initMocks(t) m := initMocks(t)
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) pubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
tests := []testData{ tests := []testData{
{ {
@ -88,8 +131,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -107,8 +150,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -126,8 +169,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -145,8 +188,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -164,8 +207,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: false, settingsSign: false,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -183,8 +226,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
eventListener: m.eventListener, eventListener: m.eventListener,
contactMeta: nil, contactMeta: nil,
isInternal: true, isInternal: true,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
}, },
wantSendingInfo: SendingInfo{}, wantSendingInfo: SendingInfo{},
wantErr: true, wantErr: true,
@ -195,8 +238,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -217,20 +260,11 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
m := initMocks(t) m := initMocks(t)
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) pubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
preferredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) preferredPubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
differentPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testDifferentPublicKey)) differentPubKey := keyRingFromKey(testDifferentPublicKey)
if err != nil {
panic(err)
}
m.eventListener.EXPECT().Emit(events.NoActiveKeyForRecipientEvent, "badkey@email.com") 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"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -260,8 +294,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -279,8 +313,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{preferredPubKey}, apiKeys: []*crypto.KeyRing{preferredPubKey},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, 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"}, contactMeta: &ContactMetadata{Email: "badkey@email.com", Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{differentPubKey}, contactKeys: []*crypto.KeyRing{differentPubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -313,8 +347,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -332,8 +366,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{differentPubKey}, contactKeys: []*crypto.KeyRing{differentPubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -352,15 +386,9 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
} }
func TestGenerateSendingInfo_Contact_External(t *testing.T) { func TestGenerateSendingInfo_Contact_External(t *testing.T) {
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) pubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
expiredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testExpiredPublicKey)) expiredPubKey := keyRingFromKey(testExpiredPublicKey)
if err != nil {
panic(err)
}
tests := []testData{ tests := []testData{
{ {
@ -369,8 +397,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -388,8 +416,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{expiredPubKey}, contactKeys: []*crypto.KeyRing{expiredPubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -407,8 +435,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -426,8 +454,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -445,8 +473,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true}, contactMeta: &ContactMetadata{Encrypt: true},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -464,8 +492,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -483,8 +511,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText}, contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -502,8 +530,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{SignMissing: true}, contactMeta: &ContactMetadata{SignMissing: true},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },

View File

@ -20,13 +20,13 @@ package smtp
import ( import (
"io" "io"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
type storeUserProvider interface { type storeUserProvider interface {
CreateDraft( CreateDraft(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
message *pmapi.Message, message *pmapi.Message,
attachmentReaders []io.Reader, attachmentReaders []io.Reader,
attachedPublicKey, attachedPublicKey,

View File

@ -20,7 +20,6 @@
package smtp package smtp
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
@ -32,7 +31,7 @@ import (
"strings" "strings"
"time" "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/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/message" "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") err = errors.New("backend: invalid email address: not owned by user")
return return
} }
kr := addr.KeyRing()
kr, err := su.client().KeyRingForAddressID(addr.ID)
if err != nil {
return
}
var attachedPublicKey string var attachedPublicKey string
var attachedPublicKeyName string var attachedPublicKeyName string
if mailSettings.AttachPublicKey > 0 { if mailSettings.AttachPublicKey > 0 {
attachedPublicKey, err = kr.GetArmoredPublicKey() firstKey, err := kr.GetKey(0)
if err != nil { if err != nil {
return err 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) 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...) atts = append(atts, message.Attachments...)
// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. // 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) attkeysEncoded := make(map[string]pmapi.AlgoKey)
for _, att := range atts { for _, att := range atts {
@ -203,7 +212,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
// PMEL 3. // PMEL 3.
composeMode := message.MIMEType composeMode := message.MIMEType
var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey var plainKey, htmlKey, mimeKey *crypto.SessionKey
var plainData, htmlData, mimeData []byte var plainData, htmlData, mimeData []byte
containsUnencryptedRecipients := false containsUnencryptedRecipients := false
@ -219,7 +228,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
return err return err
} }
var contactMeta *ContactMetadata var contactMeta *ContactMetadata
var contactKeys []*pmcrypto.KeyRing var contactKeyRings []*crypto.KeyRing
for _, contactEmail := range contactEmails { for _, contactEmail := range contactEmails {
if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_ if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_
continue continue
@ -236,12 +245,19 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
if err != nil { if err != nil {
return err return err
} }
for _, contactRawKey := range contactMeta.Keys { contactKeyRing, err := crypto.NewKeyRing(nil)
contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey))
if err != nil { if err != nil {
return err return err
} }
contactKeys = append(contactKeys, contactKey) for _, contactRawKey := range contactMeta.Keys {
contactKey, err := crypto.NewKeyFromArmored(contactRawKey)
if err != nil {
return err
}
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 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 return err
} }
var apiKeys []*pmcrypto.KeyRing var apiKeyRings []*crypto.KeyRing
for _, apiRawKey := range apiRawKeyList { for _, apiRawKey := range apiRawKeyList {
var kr *pmcrypto.KeyRing key, err := crypto.NewKeyFromArmored(apiRawKey.PublicKey)
if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil { if err != nil {
return err return err
} }
apiKeys = append(apiKeys, kr)
kr, err := crypto.NewKeyRing(key)
if err != nil {
return err
} }
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme) apiKeyRings = append(apiKeyRings, kr)
}
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeyRings, contactKeyRings, settingsSign, settingsPgpScheme)
if !sendingInfo.Encrypt { if !sendingInfo.Encrypt {
containsUnencryptedRecipients = true containsUnencryptedRecipients = true
} }
@ -284,7 +306,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
} }
} }
if sendingInfo.Scheme == pmapi.PGPMIMEPackage { 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 { if err != nil {
return err return err
} }

View File

@ -21,7 +21,7 @@ import (
"encoding/base64" "encoding/base64"
"regexp" "regexp"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
@ -37,9 +37,9 @@ func looksLikeEmail(e string) bool {
} }
func createPackets( func createPackets(
pubkey *pmcrypto.KeyRing, pubkey *crypto.KeyRing,
bodyKey *pmcrypto.SymmetricKey, bodyKey *crypto.SessionKey,
attkeys map[string]*pmcrypto.SymmetricKey, attkeys map[string]*crypto.SessionKey,
) (bodyPacket string, attachmentPackets map[string]string, err error) { ) (bodyPacket string, attachmentPackets map[string]string, err error) {
// Encrypt message body keys. // Encrypt message body keys.
packetBytes, err := pubkey.EncryptSessionKey(bodyKey) packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
@ -61,24 +61,33 @@ func createPackets(
} }
func encryptSymmetric( func encryptSymmetric(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
textToEncrypt string, textToEncrypt string,
canonicalizeText bool, // nolint[unparam] 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). // 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 { if err != nil {
return return
} }
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
if err != nil {
return
}
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0) pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
if err != nil { if err != nil {
return return
} }
key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket()) key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
if err != nil { if err != nil {
return return
} }
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket() symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
return return
} }
@ -87,7 +96,7 @@ func buildPackage(
sharedScheme int, sharedScheme int,
mimeType string, mimeType string,
bodyData []byte, bodyData []byte,
bodyKey *pmcrypto.SymmetricKey, bodyKey *crypto.SessionKey,
attKeys map[string]pmapi.AlgoKey, attKeys map[string]pmapi.AlgoKey,
) (pkg *pmapi.MessagePackage) { ) (pkg *pmapi.MessagePackage) {
if len(addressMap) == 0 { if len(addressMap) == 0 {

View File

@ -26,7 +26,7 @@ import (
"net/textproto" "net/textproto"
"strings" "strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -37,7 +37,7 @@ import (
// If `attachedPublicKey` is passed, it's added to attachments. // If `attachedPublicKey` is passed, it's added to attachments.
// Both draft and attachments are encrypted with passed `kr` key. // Both draft and attachments are encrypted with passed `kr` key.
func (store *Store) CreateDraft( func (store *Store) CreateDraft(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
message *pmapi.Message, message *pmapi.Message,
attachmentReaders []io.Reader, attachmentReaders []io.Reader,
attachedPublicKey, attachedPublicKey,
@ -92,7 +92,7 @@ func (store *Store) getDraftAction(message *pmapi.Message) int {
return pmapi.DraftActionReply 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) r := bytes.NewReader(attachmentBody)
sigReader, err := attachment.DetachedSign(kr, r) sigReader, err := attachment.DetachedSign(kr, r)
if err != nil { if err != nil {

View File

@ -53,9 +53,6 @@ type User struct {
lock sync.RWMutex lock sync.RWMutex
isAuthorized bool isAuthorized bool
unlockingKeyringLock sync.Mutex
wasKeyringUnlocked bool
} }
// newUser creates a new user. // 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 // 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). // something in the store changed).
func (u *User) init(idleUpdates chan imapBackend.Update) (err error) { func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
u.log.Info("Initialising user") u.log.Info("Initialising user")
// Reload the user's credentials (if they log out and back in we need the new // 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. // unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked.
func (u *User) unlockIfNecessary() error { func (u *User) unlockIfNecessary() error {
u.unlockingKeyringLock.Lock() if u.client().IsUnlocked() {
defer u.unlockingKeyringLock.Unlock()
if u.wasKeyringUnlocked {
return nil 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") 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 return nil
} }
@ -228,14 +213,10 @@ func (u *User) authorizeAndUnlock() (err error) {
return errors.Wrap(err, "failed to refresh API auth") 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") 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 return nil
} }
@ -432,12 +413,8 @@ func (u *User) UpdateUser() error {
return err return err
} }
if _, err = u.client().Unlock(u.creds.MailboxPassword); err != nil { if err = u.client().Unlock([]byte(u.creds.MailboxPassword)); err != nil {
return err return errors.Wrap(err, "failed to unlock user")
}
if err := u.client().UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return err
} }
emails := u.client().Addresses().ActiveEmails() emails := u.client().Addresses().ActiveEmails()
@ -514,10 +491,6 @@ func (u *User) Logout() (err error) {
return return
} }
u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
u.client().Logout() u.client().Logout()
if err = u.credStorer.Logout(u.userID); err != nil { if err = u.credStorer.Logout(u.userID); err != nil {

View File

@ -35,12 +35,11 @@ func TestUpdateUser(t *testing.T) {
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( gomock.InOrder(
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), m.pmapiClient.EXPECT().IsUnlocked().Return(false),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil), m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}), m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}),
@ -155,8 +154,8 @@ func TestCheckBridgeLoginOK(t *testing.T) {
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( gomock.InOrder(
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), m.pmapiClient.EXPECT().IsUnlocked().Return(false),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
) )
err := user.CheckBridgeLogin(testCredentials.BridgePassword) err := user.CheckBridgeLogin(testCredentials.BridgePassword)
@ -166,6 +165,28 @@ func TestCheckBridgeLoginOK(t *testing.T) {
assert.NoError(t, err) 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) { func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -220,8 +241,8 @@ func TestCheckBridgeLoginBadPassword(t *testing.T) {
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( gomock.InOrder(
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), m.pmapiClient.EXPECT().IsUnlocked().Return(false),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
) )
err := user.CheckBridgeLogin("wrong!") err := user.CheckBridgeLogin("wrong!")

View File

@ -114,33 +114,7 @@ func TestNewUserUnlockFails(t *testing.T) {
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().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, 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),
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.credentialsStore.EXPECT().Logout("user").Return(nil), m.credentialsStore.EXPECT().Logout("user").Return(nil),
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),
m.credentialsStore.EXPECT().Logout("user").Return(nil), m.credentialsStore.EXPECT().Logout("user").Return(nil),

View File

@ -182,17 +182,8 @@ func (u *Users) closeAllConnections() {
} }
} }
// Login authenticates a user. // Login authenticates a user by username/password, returning an authorised client and an auth object.
// The login flow: // The authorisation scope may not yet be full if the user has 2FA enabled.
// * 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)
func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) { func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
u.crashBandicoot(username) 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. // 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] func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
defer func() { defer func() {
if err == pmapi.ErrUpgradeApplication { if err == pmapi.ErrUpgradeApplication {
@ -230,20 +220,22 @@ func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPasswor
authClient.Logout() authClient.Logout()
}() }()
apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword) apiUser, passphrase, err := getAPIUser(authClient, mbPassword)
if err != nil { if err != nil {
log.WithError(err).Error("Failed to get API user") log.WithError(err).Error("Failed to get API user")
return return
} }
log.Info("Got API user")
var ok bool var ok bool
if user, ok = u.hasUser(apiUser.ID); ok { 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") log.WithError(err).Error("Failed to connect existing user")
return return
} }
} else { } 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") log.WithError(err).Error("Failed to add new user")
return return
} }
@ -255,13 +247,15 @@ func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPasswor
} }
// connectExistingUser connects an existing user. // 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() { if user.IsConnected() {
return errors.New("user is already connected") 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. // 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") 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. // 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() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
@ -299,7 +293,7 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword
activeEmails := client.Addresses().ActiveEmails() 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") 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 return err
} }
func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) { func getAPIUser(client pmapi.Client, mbPassword string) (user *pmapi.User, passphrase string, err error) {
hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt) 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 { if err != nil {
log.WithError(err).Error("Could not hash mailbox password") log.WithError(err).Error("Could not hash mailbox password")
return return
} }
// We unlock the user's PGP key here to detect if the user's mailbox password is wrong. // 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") log.WithError(err).Error("Wrong mailbox password")
return return
} }
if user, err = client.CurrentUser(); err != nil { 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 return
} }

View File

@ -39,7 +39,8 @@ func TestUsersFinishLoginBadMailboxPassword(t *testing.T) {
m.credentialsStore.EXPECT().List().Return([]string{}, nil), m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// Set up mocks for FinishLogin. // 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().DeleteAuth(),
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),
) )
@ -57,7 +58,8 @@ func TestUsersFinishLoginUpgradeApplication(t *testing.T) {
m.credentialsStore.EXPECT().List().Return([]string{}, nil), m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// Set up mocks for FinishLogin. // 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.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, ""),
m.pmapiClient.EXPECT().DeleteAuth().Return(err), m.pmapiClient.EXPECT().DeleteAuth().Return(err),
@ -70,7 +72,6 @@ func TestUsersFinishLoginUpgradeApplication(t *testing.T) {
func refreshWithToken(token string) *pmapi.Auth { func refreshWithToken(token string) *pmapi.Auth {
return &pmapi.Auth{ return &pmapi.Auth{
RefreshToken: token, RefreshToken: token,
KeySalt: "", // No salting in tests.
} }
} }
@ -93,7 +94,8 @@ func TestUsersFinishLoginNewUser(t *testing.T) {
m.credentialsStore.EXPECT().List().Return([]string{}, nil), m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// getAPIUser() loads user info from API (e.g. userID). // 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), m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil),
// addNewUser() // addNewUser()
@ -106,8 +108,7 @@ func TestUsersFinishLoginNewUser(t *testing.T) {
// user.init() in addNewUser // user.init() in addNewUser
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil), m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil),
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil), m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil),
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil),
// store.New() in user.init // store.New() in user.init
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
@ -158,7 +159,8 @@ func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) {
m.pmapiClient.EXPECT().Addresses().Return(nil), m.pmapiClient.EXPECT().Addresses().Return(nil),
// getAPIUser() loads user info from API (e.g. userID). // 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), m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil),
// connectExistingUser() // connectExistingUser()
@ -169,8 +171,7 @@ func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) {
// user.init() in connectExistingUser // user.init() in connectExistingUser
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil), m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil),
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil), m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil),
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil),
// store.New() in user.init // store.New() in user.init
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
@ -206,7 +207,8 @@ func TestUsersFinishLoginConnectedUser(t *testing.T) {
// Then, try to log in again... // Then, try to log in again...
gomock.InOrder( 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().CurrentUser().Return(testPMAPIUser, nil),
m.pmapiClient.EXPECT().DeleteAuth(), m.pmapiClient.EXPECT().DeleteAuth(),
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),

View File

@ -93,8 +93,7 @@ func mockConnectedUser(m mocks) {
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().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil), m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil),
// Set up mocks for store initialisation for the authorized user. // Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),

View File

@ -49,11 +49,9 @@ func TestMain(m *testing.M) {
var ( var (
testAuth = &pmapi.Auth{ //nolint[gochecknoglobals] testAuth = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "tok", RefreshToken: "tok",
KeySalt: "", // No salting in tests.
} }
testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals] testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "reftok", RefreshToken: "reftok",
KeySalt: "", // No salting in tests.
} }
testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals] 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.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().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), 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.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().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses), m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses),

View File

@ -23,13 +23,13 @@ import (
"io" "io"
"mime/quotedprintable" "mime/quotedprintable"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-textwrapper" "github.com/emersion/go-textwrapper"
openpgperrors "golang.org/x/crypto/openpgp/errors" 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. // Decrypt body.
if err := m.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired { if err := m.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
return err return err
@ -46,7 +46,7 @@ func WriteBody(w io.Writer, kr *pmcrypto.KeyRing, m *pmapi.Message) error {
return err 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 // Decrypt it
var dr io.Reader var dr io.Reader
dr, err = att.Decrypt(r, kr) dr, err = att.Decrypt(r, kr)

View File

@ -0,0 +1,4 @@
From: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <sender@pm.me>
To: Receiver <receiver@pm.me>
body

View File

@ -0,0 +1,5 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Subject: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
body

View File

@ -0,0 +1,12 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: multipart/mixed; boundary=longrandomstring
--longrandomstring
body
--longrandomstring
Content-Disposition: attachment
Aur<EFBFBD>lien is a latin1 name.
--longrandomstring--

View File

@ -0,0 +1,12 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: multipart/mixed; boundary=longrandomstring
--longrandomstring
body
--longrandomstring
Content-Disposition: attachment
Aur<EFBFBD>lien is a latin1 name but this document is latin2.
--longrandomstring--

View File

@ -19,10 +19,9 @@ package pmapi
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
) )
// Address statuses. // Address statuses.
@ -86,11 +85,6 @@ type AddressesRes struct {
Addresses AddressList 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. // ByID returns an address by id. Returns nil if no address is found.
func (l AddressList) ByID(id string) *Address { func (l AddressList) ByID(id string) *Address {
for _, addr := range l { for _, addr := range l {
@ -202,31 +196,33 @@ func (c *client) Addresses() AddressList {
return c.addresses return c.addresses
} }
// UnlockAddresses unlocks all keys for all addresses of current user. // unlockAddresses unlocks all keys for all addresses of current user.
func (c *client) UnlockAddresses(passphrase []byte) (err error) { func (c *client) unlockAddresses(passphrase []byte) (err error) {
for _, a := range c.addresses { for _, a := range c.addresses {
if a.HasKeys == MissingKeys { if a.HasKeys == MissingKeys {
continue continue
} }
// Unlock the address token using the UserKey, use the unlocked token to unlock the keyring. if c.addrKeyRing[a.ID] != nil {
if err = a.Keys.unlockKeyRing(c.kr, passphrase, c.keyLocker); err != nil { continue
err = fmt.Errorf("pmapi: cannot unlock private key of address %v: %v", a.Email, err) }
var kr *crypto.KeyRing
if kr, err = a.Keys.UnlockAll(passphrase, c.userKeyRing); err != nil {
return return
} }
c.addrKeyRing[a.ID] = kr
} }
return return
} }
func (c *client) KeyRingForAddressID(addrID string) (*pmcrypto.KeyRing, error) { func (c *client) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) {
if addr := c.addresses.ByID(addrID); addr != nil { if kr, ok := c.addrKeyRing[addrID]; ok {
return addr.KeyRing(), nil return kr, nil
} }
if addr := c.addresses.Main(); addr != nil { return nil, errors.New("no keyring available")
return addr.KeyRing(), nil
}
return nil, errors.New("no such address ID")
} }

View File

@ -52,12 +52,6 @@ func routeGetAddresses(tb testing.TB, w http.ResponseWriter, r *http.Request) st
return "addresses/get_response.json" 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) { func TestAddressList(t *testing.T) {
input := "1" input := "1"
addr := testAddressList.ByID(input) addr := testAddressList.ByID(input)

View File

@ -26,7 +26,7 @@ import (
"mime/multipart" "mime/multipart"
"net/textproto" "net/textproto"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
) )
type header textproto.MIMEHeader 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. // 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) keyPackets, err := base64.StdEncoding.DecodeString(a.KeyPackets)
if err != nil { if err != nil {
return return
@ -123,11 +123,11 @@ func (a *Attachment) Decrypt(r io.Reader, kr *pmcrypto.KeyRing) (decrypted io.Re
} }
// Encrypt encrypts an attachment. // 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) 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) return signAttachment(kr, att)
} }

View File

@ -25,7 +25,6 @@ import (
"net/http" "net/http"
"strings" "strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/pkg/srp" "github.com/ProtonMail/proton-bridge/pkg/srp"
) )
@ -112,7 +111,6 @@ type Auth struct {
Scope string Scope string
uid string // Read from AuthRes. uid string // Read from AuthRes.
RefreshToken string RefreshToken string
KeySalt string
EventID string EventID string
PasswordMode int PasswordMode int
TwoFA *TwoFactorInfo `json:"2FA,omitempty"` TwoFA *TwoFactorInfo `json:"2FA,omitempty"`
@ -145,10 +143,6 @@ func (s *Auth) HasMailboxPassword() bool {
return s.PasswordMode == 2 return s.PasswordMode == 2
} }
func (s *Auth) hasFullScope() bool {
return strings.Contains(s.Scope, "full")
}
type AuthRes struct { type AuthRes struct {
Res Res
Auth Auth
@ -327,15 +321,6 @@ func (c *client) Auth(username, password string, info *AuthInfo) (auth *Auth, er
auth = authRes.getAuth() auth = authRes.getAuth()
c.sendAuth(auth) 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 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 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. // AuthRefresh will refresh an expired access token.
func (c *client) AuthRefresh(uidAndRefreshToken string) (auth *Auth, err error) { 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! // 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 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. // Logout instructs the client manager to log this client out.
func (c *client) Logout() { func (c *client) Logout() {
c.cm.LogoutClient(c.userID) c.cm.LogoutClient(c.userID)
@ -499,7 +457,18 @@ func (c *client) IsConnected() bool {
func (c *client) ClearData() { func (c *client) ClearData() {
c.uid = "" c.uid = ""
c.accessToken = "" c.accessToken = ""
c.kr = nil
c.addresses = nil c.addresses = nil
c.user = 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)
}
}
} }

View File

@ -24,7 +24,7 @@ import (
"testing" "testing"
"time" "time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/srp" "github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -33,7 +33,7 @@ import (
r "github.com/stretchr/testify/require" r "github.com/stretchr/testify/require"
) )
var testIdentity = &pmcrypto.Identity{ var testIdentity = &crypto.Identity{
Name: "UserID", Name: "UserID",
Email: "", Email: "",
} }
@ -131,22 +131,16 @@ func TestClient_Auth(t *testing.T) {
return "/auth/post_response.json" return "/auth/post_response.json"
}, },
routeGetUsers,
routeGetAddresses,
routeGetSalts,
) )
defer finish() defer finish()
auth, err := c.Auth(testUsername, testAPIPassword, testAuthInfo) auth, err := c.Auth(testUsername, testAPIPassword, testAuthInfo)
r.Nil(t, err) r.Nil(t, err)
r.True(t, c.user.KeyRing().FirstKeyID != "", "Parsing First key ID issue")
exp := &Auth{} exp := &Auth{}
*exp = *testAuth *exp = *testAuth
exp.accessToken = testAccessToken exp.accessToken = testAccessToken
exp.RefreshToken = testRefreshToken exp.RefreshToken = testRefreshToken
exp.KeySalt = "abc"
a.Equal(t, exp, auth) a.Equal(t, exp, auth)
} }
@ -161,9 +155,6 @@ func TestClient_Auth2FA(t *testing.T) {
return "/auth/2fa/post_response.json" return "/auth/2fa/post_response.json"
}, },
routeGetUsers,
routeGetAddresses,
routeGetSalts,
) )
defer finish() defer finish()
@ -224,16 +215,16 @@ func TestClient_Unlock(t *testing.T) {
c.uid = testUID c.uid = testUID
c.accessToken = testAccessToken c.accessToken = testAccessToken
_, err := c.Unlock("wrong") err := c.Unlock([]byte("wrong"))
a.True(t, IsUnlockError(err), "expected error, pasword is wrong") a.Error(t, err, "expected error, pasword is wrong")
_, err = c.Unlock(testMailboxPassword) err = c.Unlock([]byte(testMailboxPassword))
a.Nil(t, err) a.Nil(t, err)
a.Equal(t, testUID, c.uid) a.Equal(t, testUID, c.uid)
a.Equal(t, testAccessToken, c.accessToken) a.Equal(t, testAccessToken, c.accessToken)
// second try should not fail because there is an unlocked key already // 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) a.Nil(t, err)
} }
@ -246,7 +237,7 @@ func TestClient_Unlock_EncPrivKey(t *testing.T) {
c.uid = testUID c.uid = testUID
c.accessToken = testAccessToken c.accessToken = testAccessToken
_, err := c.Unlock(testMailboxPassword) err := c.Unlock([]byte(testMailboxPassword))
Ok(t, err) Ok(t, err)
Equals(t, testUID, c.uid) Equals(t, testUID, c.uid)
Equals(t, testAccessToken, c.accessToken) Equals(t, testAccessToken, c.accessToken)
@ -280,7 +271,6 @@ func TestClient_AuthRefresh(t *testing.T) {
*exp = *testAuth *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.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.accessToken = testAccessToken
exp.KeySalt = ""
exp.EventID = "" exp.EventID = ""
exp.ExpiresIn = 360000 exp.ExpiresIn = 360000
exp.RefreshToken = testRefreshTokenNew exp.RefreshToken = testRefreshTokenNew
@ -313,7 +303,6 @@ func TestClient_AuthRefresh_HasUID(t *testing.T) {
exp := &Auth{} exp := &Auth{}
*exp = *testAuth *exp = *testAuth
exp.accessToken = testAccessToken exp.accessToken = testAccessToken
exp.KeySalt = ""
exp.EventID = "" exp.EventID = ""
exp.ExpiresIn = 360000 exp.ExpiresIn = 360000
exp.RefreshToken = testRefreshTokenNew exp.RefreshToken = testRefreshTokenNew
@ -336,7 +325,7 @@ func TestClient_Logout(t *testing.T) {
c.Logout() c.Logout()
r.Eventually(t, func() bool { 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) }, 10*time.Second, 10*time.Millisecond)
} }

View File

@ -32,7 +32,7 @@ import (
"sync" "sync"
"time" "time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/jaytaylor/html2text" "github.com/jaytaylor/html2text"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -104,63 +104,6 @@ type ClientConfig struct {
MinBytesPerSecond int64 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. // client is a client of the protonmail API. It implements the Client interface.
type client struct { type client struct {
cm *ClientManager cm *ClientManager
@ -170,11 +113,12 @@ type client struct {
accessToken string accessToken string
userID string userID string
requestLocker sync.Locker requestLocker sync.Locker
keyLocker sync.Locker
user *User user *User
addresses AddressList addresses AddressList
kr *pmcrypto.KeyRing userKeyRing *crypto.KeyRing
addrKeyRing map[string]*crypto.KeyRing
keyRingLock sync.Locker
log *logrus.Entry log *logrus.Entry
} }
@ -186,7 +130,8 @@ func newClient(cm *ClientManager, userID string) *client {
hc: getHTTPClient(cm.config, cm.roundTripper), hc: getHTTPClient(cm.config, cm.roundTripper),
userID: userID, userID: userID,
requestLocker: &sync.Mutex{}, requestLocker: &sync.Mutex{},
keyLocker: &sync.Mutex{}, keyRingLock: &sync.Mutex{},
addrKeyRing: make(map[string]*crypto.KeyRing),
log: logrus.WithField("pkg", "pmapi").WithField("userID", userID), 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. // 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) { 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. // 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") resDate := res.Header.Get("Date")
if resDate != "" { if resDate != "" {
if serverTime, err := http.ParseTime(resDate); err == nil { if serverTime, err := http.ParseTime(resDate); err == nil {
pmcrypto.GetGopenPGP().UpdateTime(serverTime.Unix()) crypto.UpdateTime(serverTime.Unix())
} }
} }

82
pkg/pmapi/client_types.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -655,7 +655,7 @@ var testCardsCleartext = []Card{
func TestClient_Encrypt(t *testing.T) { func TestClient_Encrypt(t *testing.T) {
c := newTestClient(newTestClientManager(testClientConfig)) c := newTestClient(newTestClientManager(testClientConfig))
c.kr = testPrivateKeyRing c.userKeyRing = testPrivateKeyRing
cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext) cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext)
assert.Nil(t, err) assert.Nil(t, err)
@ -669,7 +669,7 @@ func TestClient_Encrypt(t *testing.T) {
func TestClient_Decrypt(t *testing.T) { func TestClient_Decrypt(t *testing.T) {
c := newTestClient(newTestClientManager(testClientConfig)) c := newTestClient(newTestClientManager(testClientConfig))
c.kr = testPrivateKeyRing c.userKeyRing = testPrivateKeyRing
cardCleartext, err := c.DecryptAndVerifyCards(testCardsEncrypted) cardCleartext, err := c.DecryptAndVerifyCards(testCardsEncrypted)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -21,9 +21,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
) )
// Flags // Flags
@ -46,12 +45,13 @@ type PublicKey struct {
} }
// PublicKeys returns the public keys of the given email addresses. // 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 { if len(emails) == 0 {
err = fmt.Errorf("pmapi: cannot get public keys: no email address provided") err = fmt.Errorf("pmapi: cannot get public keys: no email address provided")
return return
} }
keys = map[string]*pmcrypto.KeyRing{}
keys = make(map[string]*crypto.Key)
for _, email := range emails { for _, email := range emails {
email = url.QueryEscape(email) email = url.QueryEscape(email)
@ -66,13 +66,15 @@ func (c *client) PublicKeys(emails []string) (keys map[string]*pmcrypto.KeyRing,
return return
} }
for _, key := range res.Keys { for _, rawKey := range res.Keys {
if key.Flags&UseToEncryptFlag == UseToEncryptFlag { if rawKey.Flags&UseToEncryptFlag == UseToEncryptFlag {
var kr *pmcrypto.KeyRing var key *crypto.Key
if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(key.PublicKey)); err != nil {
if key, err = crypto.NewKeyFromArmored(rawKey.PublicKey); err != nil {
return return
} }
keys[email] = kr
keys[email] = key
} }
} }
} }

View File

@ -20,183 +20,152 @@ package pmapi
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io" "io"
"io/ioutil" "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 { type PMKey struct {
ID string ID string
Version int Version int
Flags int Flags int
Fingerprint string Fingerprint string
PrivateKey *crypto.Key
Primary int Primary int
Token *string `json:",omitempty"` Token *string `json:",omitempty"`
Signature *string `json:",omitempty"` Signature *string `json:",omitempty"`
} }
type PMKeys struct { func (key *PMKey) UnmarshalJSON(b []byte) (err error) {
Keys []PMKey type _PMKey PMKey
KeyRing *pmcrypto.KeyRing
}
func (k *PMKeys) UnmarshalJSON(b []byte) (err error) { rawKey := struct {
var rawKeys []struct { _PMKey
PMKey PrivateKey string
PrivateKey clearableKey }{}
}
if err = json.Unmarshal(b, &rawKeys); err != nil { if err = json.Unmarshal(b, &rawKey); err != nil {
return return
} }
k.KeyRing = &pmcrypto.KeyRing{} *key = PMKey(rawKey._PMKey)
for _, rawKey := range rawKeys {
err = k.KeyRing.ReadFrom(bytes.NewReader(rawKey.PrivateKey), true) if key.PrivateKey, err = crypto.NewKeyFromArmored(rawKey.PrivateKey); err != nil {
rawKey.PrivateKey.clear() return errors.Wrap(err, "failed to create crypto key from armored private key")
}
return
}
func (key PMKey) getPassphraseFromToken(kr *crypto.KeyRing) (passphrase []byte, err error) {
if kr == nil {
return nil, errors.New("no user key was provided")
}
msg, err := crypto.NewPGPMessageFromArmored(*key.Token)
if err != nil { if err != nil {
return return
} }
k.Keys = append(k.Keys, rawKey.PMKey)
sig, err := crypto.NewPGPSignatureFromArmored(*key.Signature)
if err != nil {
return
} }
if len(k.Keys) > 0 {
k.KeyRing.FirstKeyID = k.Keys[0].ID 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 return
} }
// unlockKeyRing tries to unlock them with the provided keyRing using the token type PMKeys []PMKey
// 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()
if k == nil { // UnlockAll goes through each key and unlocks it, returning a keyring containing all unlocked keys,
err = errors.New("keys is a nil object") // 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 return
} }
for _, key := range k.Keys { for _, key := range *keys {
var secret []byte
if key.Token == nil || key.Signature == nil { if key.Token == nil || key.Signature == nil {
if err = unlockKeyRingNoErrorWhenAlreadyUnlocked(k.KeyRing, passphrase); err != nil { secret = passphrase
} else if secret, err = key.getPassphraseFromToken(userKey); err != nil {
return return
} }
var k *crypto.Key
if k, err = key.unlock(secret); err != nil {
logrus.WithError(err).Warn("Failed to unlock key")
continue continue
} }
message, err := pmcrypto.NewPGPMessageFromArmored(*key.Token) if err = kr.AddKey(k); err != nil {
if err != nil { logrus.WithError(err).Warn("Failed to add key to keyring")
return err continue
}
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)
} }
} }
return nil if kr.CountEntities() == 0 {
} err = errors.New("no keys could be unlocked")
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 return
} }
err = nil
} return kr, err
return
} }
// ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities. // ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities.
var ErrNoKeyringAvailable = errors.New("no keyring available") var ErrNoKeyringAvailable = errors.New("no keyring available")
func (c *client) encrypt(plain string, signer *pmcrypto.KeyRing) (armored string, err error) { func (c *client) encrypt(plain string, signer *crypto.KeyRing) (armored string, err error) {
return encrypt(c.kr, plain, signer) return encrypt(c.userKeyRing, plain, signer)
} }
func encrypt(encrypter *pmcrypto.KeyRing, plain string, signer *pmcrypto.KeyRing) (armored string, err error) { func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (armored string, err error) {
if encrypter == nil || encrypter.FirstKey() == nil { if encrypter == nil {
return "", ErrNoKeyringAvailable 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). // 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 { if err != nil {
return 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) { 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 { if decrypter == nil {
return "", ErrNoKeyringAvailable return "", ErrNoKeyringAvailable
} }
pgpMessage, err := pmcrypto.NewPGPMessageFromArmored(armored) pgpMessage, err := crypto.NewPGPMessageFromArmored(armored)
if err != nil { if err != nil {
return 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) { func (c *client) sign(plain string) (armoredSignature string, err error) {
if c.kr == nil { if c.userKeyRing == nil {
return "", ErrNoKeyringAvailable return "", ErrNoKeyringAvailable
} }
plainMessage := pmcrypto.NewPlainMessageFromString(plain) plainMessage := crypto.NewPlainMessageFromString(plain)
pgpSignature, err := c.kr.SignDetached(plainMessage) pgpSignature, err := c.userKeyRing.SignDetached(plainMessage)
if err != nil { if err != nil {
return return
} }
@ -235,34 +204,44 @@ func (c *client) sign(plain string) (armoredSignature string, err error) {
} }
func (c *client) verify(plain, amroredSignature string) (err error) { func (c *client) verify(plain, amroredSignature string) (err error) {
plainMessage := pmcrypto.NewPlainMessageFromString(plain) plainMessage := crypto.NewPlainMessageFromString(plain)
pgpSignature, err := pmcrypto.NewPGPSignatureFromArmored(amroredSignature) pgpSignature, err := crypto.NewPGPSignatureFromArmored(amroredSignature)
if err != nil { if err != nil {
return return
} }
verifyTime := int64(0) // By default it will use current timestamp. 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) { func encryptAttachment(kr *crypto.KeyRing, data io.Reader, filename string) (encrypted io.Reader, err error) {
if kr == nil || kr.FirstKey() == nil { if kr == nil {
return nil, ErrNoKeyringAvailable return nil, ErrNoKeyringAvailable
} }
firstKey, err := kr.FirstKey()
if err != nil {
return nil, err
}
dataBytes, err := ioutil.ReadAll(data) dataBytes, err := ioutil.ReadAll(data)
if err != nil { if err != nil {
return 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). // 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 { if err != nil {
return return
} }
packets := append(pgpSplitMessage.KeyPacket, pgpSplitMessage.DataPacket...) packets := append(pgpSplitMessage.KeyPacket, pgpSplitMessage.DataPacket...)
return bytes.NewReader(packets), nil 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 { if kr == nil {
return nil, ErrNoKeyringAvailable return nil, ErrNoKeyringAvailable
} }
@ -270,7 +249,7 @@ func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader)
if err != nil { if err != nil {
return return
} }
pgpSplitMessage := pmcrypto.NewPGPSplitMessage(keyPackets, dataBytes) pgpSplitMessage := crypto.NewPGPSplitMessage(keyPackets, dataBytes)
plainMessage, err := kr.DecryptAttachment(pgpSplitMessage) plainMessage, err := kr.DecryptAttachment(pgpSplitMessage)
if err != nil { if err != nil {
return return
@ -278,7 +257,7 @@ func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader)
return plainMessage.NewReader(), nil 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 { if encrypter == nil {
return nil, ErrNoKeyringAvailable return nil, ErrNoKeyringAvailable
} }
@ -286,7 +265,7 @@ func signAttachment(encrypter *pmcrypto.KeyRing, data io.Reader) (signature io.R
if err != nil { if err != nil {
return return
} }
plainMessage := pmcrypto.NewPlainMessage(dataBytes) plainMessage := crypto.NewPlainMessage(dataBytes)
sig, err := encrypter.SignDetached(plainMessage) sig, err := encrypter.SignDetached(plainMessage)
if err != nil { if err != nil {
return return

View File

@ -19,11 +19,9 @@ package pmapi
import ( import (
"encoding/json" "encoding/json"
"strings"
"sync"
"testing" "testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -38,11 +36,16 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) {
addrKeysPrimaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysPrimaryHasToken_JSON", false)) addrKeysPrimaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysPrimaryHasToken_JSON", false))
addrKeysSecondaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysSecondaryHasToken_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") assert.NoError(t, err, "Expected not to receive an error unlocking user key")
type args struct { type args struct {
userKeyring *pmcrypto.KeyRing userKeyring *crypto.KeyRing
passphrase []byte passphrase []byte
} }
tests := []struct { tests := []struct {
@ -73,21 +76,26 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
tempLocker := &sync.Mutex{} kr, err := tt.keys.UnlockAll(tt.args.passphrase, tt.args.userKeyring) // nolint[scopelint]
err := tt.keys.unlockKeyRing(tt.args.userKeyring, tt.args.passphrase, tempLocker) // nolint[scopelint]
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
// assert at least one key has been decrypted // assert at least one key has been decrypted
atLeastOneDecrypted := false 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 atLeastOneDecrypted = true
break break
} }
} }
assert.True(t, atLeastOneDecrypted) assert.True(t, atLeastOneDecrypted)
}) })
} }

View File

@ -31,7 +31,7 @@ import (
"strconv" "strconv"
"strings" "strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"golang.org/x/crypto/openpgp/packet" "golang.org/x/crypto/openpgp/packet"
) )
@ -250,7 +250,7 @@ func (m *Message) IsLegacyMessage() bool {
strings.Contains(m.Body, MessageTail) 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() { if m.IsLegacyMessage() {
return m.DecryptLegacy(kr) return m.DecryptLegacy(kr)
} }
@ -269,7 +269,7 @@ func (m *Message) Decrypt(kr *pmcrypto.KeyRing) (err error) {
return 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) randomKeyStart := strings.Index(m.Body, RandomKeyHeader) + len(RandomKeyHeader)
randomKeyEnd := strings.Index(m.Body, RandomKeyTail) randomKeyEnd := strings.Index(m.Body, RandomKeyTail)
randomKey := m.Body[randomKeyStart:randomKeyEnd] randomKey := m.Body[randomKeyStart:randomKeyEnd]
@ -341,7 +341,7 @@ func decodeBase64UTF8(input string) (output []byte, err error) {
return return
} }
func (m *Message) Encrypt(encrypter, signer *pmcrypto.KeyRing) (err error) { func (m *Message) Encrypt(encrypter, signer *crypto.KeyRing) (err error) {
if m.IsBodyEncrypted() { if m.IsBodyEncrypted() {
err = errors.New("pmapi: trying to encrypt an already encrypted message") err = errors.New("pmapi: trying to encrypt an already encrypted message")
return return

View File

@ -20,10 +20,9 @@ package pmapi
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"testing" "testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -142,10 +141,15 @@ func TestMessage_Decrypt(t *testing.T) {
func TestMessage_Decrypt_Legacy(t *testing.T) { func TestMessage_Decrypt_Legacy(t *testing.T) {
testPrivateKeyLegacy := readTestFile("testPrivateKeyLegacy", false) testPrivateKeyLegacy := readTestFile("testPrivateKeyLegacy", false)
testPrivateKeyRingLegacy, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPrivateKeyLegacy))
key, err := crypto.NewKeyFromArmored(testPrivateKeyLegacy)
Ok(t, err) 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} msg := &Message{Body: testMessageEncryptedLegacy}
@ -163,7 +167,10 @@ func TestMessage_Decrypt_signed(t *testing.T) {
} }
func TestMessage_Encrypt(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) Ok(t, err)
msg := &Message{Body: testMessageCleartext} msg := &Message{Body: testMessageCleartext}
@ -173,7 +180,7 @@ func TestMessage_Encrypt(t *testing.T) {
Ok(t, err) Ok(t, err)
Equals(t, testMessageCleartext, msg.Body) 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 { func routeLabelMessages(tb testing.TB, w http.ResponseWriter, r *http.Request) string {

View File

@ -8,7 +8,7 @@ import (
io "io" io "io"
reflect "reflect" reflect "reflect"
crypto "github.com/ProtonMail/gopenpgp/crypto" crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" 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) 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 // ClearData mocks base method
func (m *MockClient) ClearData() { func (m *MockClient) ClearData() {
m.ctrl.T.Helper() 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)) 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 // KeyRingForAddressID mocks base method
func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) { func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -605,12 +634,11 @@ func (mr *MockClientMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomoc
} }
// Unlock mocks base method // Unlock mocks base method
func (m *MockClient) Unlock(arg0 string) (*crypto.KeyRing, error) { func (m *MockClient) Unlock(arg0 []byte) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", arg0) ret := m.ctrl.Call(m, "Unlock", arg0)
ret0, _ := ret[0].(*crypto.KeyRing) ret0, _ := ret[0].(error)
ret1, _ := ret[1].(error) return ret0
return ret0, ret1
} }
// Unlock indicates an expected call of Unlock // 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) 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 // UpdateLabel mocks base method
func (m *MockClient) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) { func (m *MockClient) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -24,12 +24,12 @@ import (
"github.com/jameskeane/bcrypt" "github.com/jameskeane/bcrypt"
) )
func HashMailboxPassword(password, keySalt string) (hashedPassword string, err error) { func HashMailboxPassword(password, salt string) (hashedPassword string, err error) {
if keySalt == "" { if salt == "" {
hashedPassword = password hashedPassword = password
return return
} }
decodedSalt, err := base64.StdEncoding.DecodeString(keySalt) decodedSalt, err := base64.StdEncoding.DecodeString(salt)
if err != nil { if err != nil {
return return
} }

View File

@ -21,15 +21,15 @@ import (
"io/ioutil" "io/ioutil"
"strings" "strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
) )
const testMailboxPassword = "apple" const testMailboxPassword = "apple"
const testMailboxPasswordLegacy = "123" const testMailboxPasswordLegacy = "123"
var ( var (
testPrivateKeyRing *pmcrypto.KeyRing testPrivateKeyRing *crypto.KeyRing
testPublicKeyRing *pmcrypto.KeyRing testPublicKeyRing *crypto.KeyRing
) )
func init() { func init() {
@ -37,15 +37,27 @@ func init() {
testPublicKey := readTestFile("testPublicKey", false) testPublicKey := readTestFile("testPublicKey", false)
var err error var err error
if testPrivateKeyRing, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPrivateKey)); err != nil {
privKey, err := crypto.NewKeyFromArmored(testPrivateKey)
if err != nil {
panic(err) panic(err)
} }
if testPublicKeyRing, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)); err != nil { privKeyUnlocked, err := privKey.Unlock([]byte(testMailboxPassword))
if err != nil {
panic(err) 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) panic(err)
} }
} }

View File

@ -18,8 +18,8 @@
package pmapi package pmapi
import ( import (
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/getsentry/raven-go" "github.com/getsentry/raven-go"
"github.com/pkg/errors"
) )
// Role values. // Role values.
@ -68,6 +68,10 @@ type User struct {
Private int Private int
Subscribed int Subscribed int
Services int Services int
Deliquent int
Keys PMKeys
VPN struct { VPN struct {
Status int Status int
ExpirationTime int ExpirationTime int
@ -75,8 +79,6 @@ type User struct {
MaxConnect int MaxConnect int
MaxTier int MaxTier int
} }
Deliquent int
Keys PMKeys
} }
// UserRes holds structure of JSON response. // UserRes holds structure of JSON response.
@ -86,9 +88,17 @@ type UserRes struct {
User *User User *User
} }
// KeyRing returns the (possibly unlocked) PMKeys KeyRing. // unlockUser unlocks all the client's user keys using the given passphrase.
func (u *User) KeyRing() *pmcrypto.KeyRing { func (c *client) unlockUser(passphrase []byte) (err error) {
return u.Keys.KeyRing 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. // UpdateUser retrieves details about user and loads its addresses.

View File

@ -23,7 +23,7 @@ import (
"net/url" "net/url"
"testing" "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"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
r "github.com/stretchr/testify/require" r "github.com/stretchr/testify/require"
@ -72,9 +72,9 @@ func TestClient_CurrentUser(t *testing.T) {
r.Nil(t, err) r.Nil(t, err)
// Ignore KeyRings during the check because they have unexported fields and cannot be compared // 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) { func TestClient_PublicKeys(t *testing.T) {

View File

@ -31,7 +31,6 @@ import (
const ( const (
testUserKey = "user_key.json" testUserKey = "user_key.json"
testAddressKey = "address_key.json" testAddressKey = "address_key.json"
testKeyPassphrase = "testpassphrase"
) )
type TestAccount struct { type TestAccount struct {
@ -78,14 +77,9 @@ func newTestAccount(
} }
func (a *TestAccount) initKeys() { func (a *TestAccount) initKeys() {
if a.user.Keys.Keys != nil {
return
}
userKeys := loadPMKeys(readTestFile(testUserKey)) userKeys := loadPMKeys(readTestFile(testUserKey))
_ = userKeys.KeyRing.Unlock([]byte(testKeyPassphrase))
addressKeys := loadPMKeys(readTestFile(testAddressKey)) addressKeys := loadPMKeys(readTestFile(testAddressKey))
_ = addressKeys.KeyRing.Unlock([]byte(testKeyPassphrase))
a.user.Keys = *userKeys a.user.Keys = *userKeys
for _, addressEmail := range a.Addresses().ActiveEmails() { for _, addressEmail := range a.Addresses().ActiveEmails() {

View File

@ -27,7 +27,8 @@
"ID": "userAddress", "ID": "userAddress",
"Email": "user@pm.me", "Email": "user@pm.me",
"Order": 1, "Order": 1,
"Receive": 1 "Receive": 1,
"HasKeys": 1
} }
}, },
"user2fa": { "user2fa": {
@ -35,7 +36,8 @@
"ID": "user2faAddress", "ID": "user2faAddress",
"Email": "user@pm.me", "Email": "user@pm.me",
"Order": 1, "Order": 1,
"Receive": 1 "Receive": 1,
"HasKeys": 1
} }
}, },
"userAddressWithCapitalLetter": { "userAddressWithCapitalLetter": {
@ -43,7 +45,8 @@
"ID": "userAddressWithCapitalLetterAddress", "ID": "userAddressWithCapitalLetterAddress",
"Email": "uSeR@pm.me", "Email": "uSeR@pm.me",
"Order": 1, "Order": 1,
"Receive": 1 "Receive": 1,
"HasKeys": 1
} }
}, },
"userMoreAddresses": { "userMoreAddresses": {
@ -51,13 +54,15 @@
"ID": "primary", "ID": "primary",
"Email": "primaryaddress@pm.me", "Email": "primaryaddress@pm.me",
"Order": 1, "Order": 1,
"Receive": 1 "Receive": 1,
"HasKeys": 1
}, },
"secondary": { "secondary": {
"ID": "secondary", "ID": "secondary",
"Email": "secondaryaddress@pm.me", "Email": "secondaryaddress@pm.me",
"Order": 2, "Order": 2,
"Receive": 1 "Receive": 1,
"HasKeys": 1
}, },
"disabled": { "disabled": {
"ID": "disabled", "ID": "disabled",
@ -75,9 +80,10 @@
}, },
"secondary": { "secondary": {
"ID": "secondary", "ID": "secondary",
"Email": "user@pm.me", "Email": "secondaryaddress@pm.me",
"Order": 2, "Order": 2,
"Receive": 1 "Receive": 1,
"HasKeys": 1
} }
} }
}, },
@ -89,11 +95,11 @@
"userDisabledPrimaryAddress": "password" "userDisabledPrimaryAddress": "password"
}, },
"mailboxPasswords": { "mailboxPasswords": {
"user": "password", "user": "testpassphrase",
"user2fa": "password", "user2fa": "testpassphrase",
"userAddressWithCapitalLetter": "password", "userAddressWithCapitalLetter": "testpassphrase",
"userMoreAddresses": "password", "userMoreAddresses": "testpassphrase",
"userDisabledPrimaryAddress": "password" "userDisabledPrimaryAddress": "testpassphrase"
}, },
"twoFAs": { "twoFAs": {
"user": false, "user": false,

View File

@ -147,6 +147,14 @@ func (api *FakePMAPI) AuthRefresh(token string) (*pmapi.Auth, error) {
return auth, nil 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() { func (api *FakePMAPI) Logout() {
api.controller.clientManager.LogoutClient(api.userID) api.controller.clientManager.LogoutClient(api.userID)
} }
@ -164,5 +172,17 @@ func (api *FakePMAPI) DeleteAuth() error {
} }
func (api *FakePMAPI) ClearData() { 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() api.unsetUser()
} }

View File

@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -35,7 +36,9 @@ type FakePMAPI struct {
auths chan<- *pmapi.Auth auths chan<- *pmapi.Auth
user *pmapi.User user *pmapi.User
userKeyRing *crypto.KeyRing
addresses *pmapi.AddressList addresses *pmapi.AddressList
addrKeyRing map[string]*crypto.KeyRing
labels []*pmapi.Label labels []*pmapi.Label
messages []*pmapi.Message messages []*pmapi.Message
events []*pmapi.Event events []*pmapi.Event
@ -51,6 +54,7 @@ func New(controller *Controller, userID string) *FakePMAPI {
controller: controller, controller: controller,
log: logrus.WithField("pkg", "fakeapi"), log: logrus.WithField("pkg", "fakeapi"),
userID: userID, userID: userID,
addrKeyRing: make(map[string]*crypto.KeyRing),
} }
fakePMAPI.addEvent(&pmapi.Event{ fakePMAPI.addEvent(&pmapi.Event{

View File

@ -18,7 +18,7 @@
package fakeapi package fakeapi
import ( import (
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
@ -29,13 +29,37 @@ func (api *FakePMAPI) GetMailSettings() (pmapi.MailSettings, error) {
return pmapi.MailSettings{}, nil return pmapi.MailSettings{}, nil
} }
func (api *FakePMAPI) Unlock(mailboxPassword string) (*pmcrypto.KeyRing, error) { func (api *FakePMAPI) IsUnlocked() bool {
return &pmcrypto.KeyRing{ return api.userKeyRing != nil
FirstKeyID: "key",
}, 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 return nil
} }
@ -47,6 +71,7 @@ func (api *FakePMAPI) UpdateUser() (*pmapi.User, error) {
if err := api.checkAndRecordCall(GET, "/users", nil); err != nil { if err := api.checkAndRecordCall(GET, "/users", nil); err != nil {
return nil, err return nil, err
} }
return api.user, nil return api.user, nil
} }
@ -84,8 +109,6 @@ func (api *FakePMAPI) Addresses() pmapi.AddressList {
return *api.addresses return *api.addresses
} }
func (api *FakePMAPI) KeyRingForAddressID(addrID string) (*pmcrypto.KeyRing, error) { func (api *FakePMAPI) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) {
return &pmcrypto.KeyRing{ return api.addrKeyRing[addrID], nil
FirstKeyID: "key",
}, nil
} }

View File

@ -71,7 +71,7 @@ Feature: IMAP auth
@ignore-live @ignore-live
Scenario: Authenticates with disabled primary address Scenario: Authenticates with disabled primary address
Given there is connected user "userDisabledPrimaryAddress" 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" Then IMAP response is "OK"
Scenario: Authenticates two users Scenario: Authenticates two users

View File

@ -48,7 +48,7 @@ Feature: SMTP auth
@ignore-live @ignore-live
Scenario: Authenticates with disabled primary address Scenario: Authenticates with disabled primary address
Given there is connected user "userDisabledPrimaryAddress" 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" Then SMTP response is "OK"
Scenario: Authenticates two users Scenario: Authenticates two users

View File

@ -80,11 +80,10 @@ func buildMessage(client pmapi.Client, message *pmapi.Message) (*bytes.Buffer, e
} }
func encryptMessage(client pmapi.Client, message *pmapi.Message) error { func encryptMessage(client pmapi.Client, message *pmapi.Message) error {
addresses, err := client.GetAddresses() kr, err := client.KeyRingForAddressID(message.AddressID)
if err != nil { 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 { if err = message.Encrypt(kr, nil); err != nil {
return errors.Wrap(err, "failed to encrypt message body") return errors.Wrap(err, "failed to encrypt message body")

View File

@ -34,21 +34,25 @@ func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, p
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get auth info") 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 { if err != nil {
return errors.Wrap(err, "failed to auth user") 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 { if err != nil {
return errors.Wrap(err, "failed to hash mailbox password") 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") 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 { if err := cleanup(client, addresses); err != nil {
return errors.Wrap(err, "failed to clean user") return errors.Wrap(err, "failed to clean user")

View File

@ -2,7 +2,7 @@
{ {
"ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==", "ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==",
"Version": 3, "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", "Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353",
"Activation": null, "Activation": null,
"Primary": 1 "Primary": 1