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-312 Validate recipient emails in send before asking for their public keys.
* GODT-368 Bump docker-credential-helpers version.
* GODT-280 Migrate to gopenpgp v2
* `Unlock()` call on pmapi-client unlocks both User keys and Address keys
* Salt is available via `AuthSalt()` method
### Fixed
* GODT-356 Fix crash when removing account while mail client is fetching messages (regression from GODT-204).

4
go.mod
View File

@ -18,7 +18,7 @@ require (
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed
github.com/ProtonMail/gopenpgp/v2 v2.0.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
@ -70,5 +70,5 @@ replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c
)

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/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
@ -15,14 +15,14 @@ github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2Ksf
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g=
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
@ -148,6 +148,7 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// 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
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
// 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

View File

@ -32,7 +32,7 @@ import (
"text/template"
"time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
@ -82,7 +82,11 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
return errors.New("no available address for encryption")
}
m.AddressID = addr.ID
kr := addr.KeyRing()
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
if err != nil {
return err
}
// Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
@ -184,7 +188,7 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *pmcrypto.KeyRing) (err error) { // nolint[funlen]
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) { // nolint[funlen]
b := &bytes.Buffer{}
// Overwrite content for main header for import.
@ -240,7 +244,7 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *
}
// Create encrypted writer.
pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil)
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
if err != nil {
return err
}
@ -722,7 +726,7 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
return structure, msgBody, err
}
func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *pmcrypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
multipartType, err := im.setMessageContentType(m)
if err != nil {
return

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -114,33 +114,7 @@ func TestNewUserUnlockFails(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password")),
m.credentialsStore.EXPECT().Logout("user").Return(nil),
m.pmapiClient.EXPECT().Logout(),
m.credentialsStore.EXPECT().Logout("user").Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
)
checkNewUserHasCredentials(testCredentialsDisconnected, m)
}
func TestNewUserUnlockAddressesFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
gomock.InOrder(
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password")),
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(errors.New("bad password")),
m.credentialsStore.EXPECT().Logout("user").Return(nil),
m.pmapiClient.EXPECT().Logout(),
m.credentialsStore.EXPECT().Logout("user").Return(nil),

View File

@ -182,17 +182,8 @@ func (u *Users) closeAllConnections() {
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := users.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := users.FinishLogin(client, auth, mailboxPassword)
// Login authenticates a user by username/password, returning an authorised client and an auth object.
// The authorisation scope may not yet be full if the user has 2FA enabled.
func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
u.crashBandicoot(username)
@ -214,7 +205,6 @@ func (u *Users) Login(username, password string) (authClient pmapi.Client, auth
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
defer func() {
if err == pmapi.ErrUpgradeApplication {
@ -230,20 +220,22 @@ func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPasswor
authClient.Logout()
}()
apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword)
apiUser, passphrase, err := getAPIUser(authClient, mbPassword)
if err != nil {
log.WithError(err).Error("Failed to get API user")
return
}
log.Info("Got API user")
var ok bool
if user, ok = u.hasUser(apiUser.ID); ok {
if err = u.connectExistingUser(user, auth, hashedPassword); err != nil {
if err = u.connectExistingUser(user, auth, passphrase); err != nil {
log.WithError(err).Error("Failed to connect existing user")
return
}
} else {
if err = u.addNewUser(apiUser, auth, hashedPassword); err != nil {
if err = u.addNewUser(apiUser, auth, passphrase); err != nil {
log.WithError(err).Error("Failed to add new user")
return
}
@ -255,13 +247,15 @@ func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPasswor
}
// connectExistingUser connects an existing user.
func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword string) (err error) {
func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, passphrase string) (err error) {
if user.IsConnected() {
return errors.New("user is already connected")
}
log.Info("Connecting existing user")
// Update the user's password in the cred store in case they changed it.
if err = u.credStorer.UpdatePassword(user.ID(), hashedPassword); err != nil {
if err = u.credStorer.UpdatePassword(user.ID(), passphrase); err != nil {
return errors.Wrap(err, "failed to update password of user in credentials store")
}
@ -283,7 +277,7 @@ func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword
}
// addNewUser adds a new user.
func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword string) (err error) {
func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, passphrase string) (err error) {
u.lock.Lock()
defer u.lock.Unlock()
@ -299,7 +293,7 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword
activeEmails := client.Addresses().ActiveEmails()
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassword, activeEmails); err != nil {
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), passphrase, activeEmails); err != nil {
return errors.Wrap(err, "failed to add user to credentials store")
}
@ -321,21 +315,27 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword
return err
}
func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) {
hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
func getAPIUser(client pmapi.Client, mbPassword string) (user *pmapi.User, passphrase string, err error) {
salt, err := client.AuthSalt()
if err != nil {
log.WithError(err).Error("Could not get salt")
return
}
passphrase, err = pmapi.HashMailboxPassword(mbPassword, salt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
return
}
// We unlock the user's PGP key here to detect if the user's mailbox password is wrong.
if _, err = client.Unlock(hashedPassword); err != nil {
if err = client.Unlock([]byte(passphrase)); err != nil {
log.WithError(err).Error("Wrong mailbox password")
return
}
if user, err = client.CurrentUser(); err != nil {
log.WithError(err).Error("Could not load API user")
log.WithError(err).Error("Could not load user data")
return
}

View File

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

View File

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

View File

@ -49,11 +49,9 @@ func TestMain(m *testing.M) {
var (
testAuth = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "tok",
KeySalt: "", // No salting in tests.
}
testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "reftok",
KeySalt: "", // No salting in tests.
}
testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals]
@ -209,8 +207,7 @@ func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
@ -219,8 +216,7 @@ func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil),
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil),
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil),
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses),

View File

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

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

View File

@ -52,12 +52,6 @@ func routeGetAddresses(tb testing.TB, w http.ResponseWriter, r *http.Request) st
return "addresses/get_response.json"
}
func routeGetSalts(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
Ok(tb, checkMethodAndPath(r, "GET", "/keys/salts"))
Ok(tb, isAuthReq(r, testUID, testAccessToken))
return "keys/salts/get_response.json"
}
func TestAddressList(t *testing.T) {
input := "1"
addr := testAddressList.ByID(input)

View File

@ -26,7 +26,7 @@ import (
"mime/multipart"
"net/textproto"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
type header textproto.MIMEHeader
@ -114,7 +114,7 @@ func (a *Attachment) UnmarshalJSON(b []byte) error {
}
// Decrypt decrypts this attachment's data from r using the keys from kr.
func (a *Attachment) Decrypt(r io.Reader, kr *pmcrypto.KeyRing) (decrypted io.Reader, err error) {
func (a *Attachment) Decrypt(r io.Reader, kr *crypto.KeyRing) (decrypted io.Reader, err error) {
keyPackets, err := base64.StdEncoding.DecodeString(a.KeyPackets)
if err != nil {
return
@ -123,11 +123,11 @@ func (a *Attachment) Decrypt(r io.Reader, kr *pmcrypto.KeyRing) (decrypted io.Re
}
// Encrypt encrypts an attachment.
func (a *Attachment) Encrypt(kr *pmcrypto.KeyRing, att io.Reader) (encrypted io.Reader, err error) {
func (a *Attachment) Encrypt(kr *crypto.KeyRing, att io.Reader) (encrypted io.Reader, err error) {
return encryptAttachment(kr, att, a.Name)
}
func (a *Attachment) DetachedSign(kr *pmcrypto.KeyRing, att io.Reader) (signed io.Reader, err error) {
func (a *Attachment) DetachedSign(kr *crypto.KeyRing, att io.Reader) (signed io.Reader, err error) {
return signAttachment(kr, att)
}

View File

@ -25,7 +25,6 @@ import (
"net/http"
"strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/pkg/srp"
)
@ -112,7 +111,6 @@ type Auth struct {
Scope string
uid string // Read from AuthRes.
RefreshToken string
KeySalt string
EventID string
PasswordMode int
TwoFA *TwoFactorInfo `json:"2FA,omitempty"`
@ -145,10 +143,6 @@ func (s *Auth) HasMailboxPassword() bool {
return s.PasswordMode == 2
}
func (s *Auth) hasFullScope() bool {
return strings.Contains(s.Scope, "full")
}
type AuthRes struct {
Res
Auth
@ -327,15 +321,6 @@ func (c *client) Auth(username, password string, info *AuthInfo) (auth *Auth, er
auth = authRes.getAuth()
c.sendAuth(auth)
// Auth has to be fully unlocked to get key salt. During `Auth` it can happen
// only to accounts without 2FA. For 2FA accounts, it's done in `Auth2FA`.
if auth.hasFullScope() {
err = c.setKeySaltToAuth(auth)
if err != nil {
return nil, err
}
}
return auth, err
}
@ -367,55 +352,9 @@ func (c *client) Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error) {
}
}
if err := c.setKeySaltToAuth(auth); err != nil {
return nil, err
}
return auth2FARes.getAuth2FA(), nil
}
func (c *client) setKeySaltToAuth(auth *Auth) error {
// KeySalt already set up, no need to do it again.
if auth.KeySalt != "" {
return nil
}
user, err := c.CurrentUser()
if err != nil {
return err
}
salts, err := c.GetKeySalts()
if err != nil {
return err
}
for _, s := range salts {
if s.ID == user.KeyRing().FirstKeyID {
auth.KeySalt = s.KeySalt
break
}
}
return nil
}
// Unlock decrypts the key ring.
// If the password is invalid, IsUnlockError(err) will return true.
func (c *client) Unlock(password string) (kr *pmcrypto.KeyRing, err error) {
if _, err = c.CurrentUser(); err != nil {
return
}
c.keyLocker.Lock()
defer c.keyLocker.Unlock()
kr = c.user.KeyRing()
if err = unlockKeyRingNoErrorWhenAlreadyUnlocked(kr, []byte(password)); err != nil {
return
}
c.kr = kr
return kr, err
}
// AuthRefresh will refresh an expired access token.
func (c *client) AuthRefresh(uidAndRefreshToken string) (auth *Auth, err error) {
// If we don't yet have a saved access token, save this one in case the refresh fails!
@ -466,6 +405,25 @@ func (c *client) AuthRefresh(uidAndRefreshToken string) (auth *Auth, err error)
return auth, err
}
func (c *client) AuthSalt() (string, error) {
salts, err := c.GetKeySalts()
if err != nil {
return "", err
}
if _, err := c.CurrentUser(); err != nil {
return "", err
}
for _, s := range salts {
if s.ID == c.user.Keys[0].ID {
return s.KeySalt, nil
}
}
return "", errors.New("no matching salt found")
}
// Logout instructs the client manager to log this client out.
func (c *client) Logout() {
c.cm.LogoutClient(c.userID)
@ -499,7 +457,18 @@ func (c *client) IsConnected() bool {
func (c *client) ClearData() {
c.uid = ""
c.accessToken = ""
c.kr = nil
c.addresses = nil
c.user = nil
if c.userKeyRing != nil {
c.userKeyRing.ClearPrivateParams()
c.userKeyRing = nil
}
for addrID, addr := range c.addrKeyRing {
if addr != nil {
addr.ClearPrivateParams()
delete(c.addrKeyRing, addrID)
}
}
}

View File

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

View File

@ -32,7 +32,7 @@ import (
"sync"
"time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/jaytaylor/html2text"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -104,63 +104,6 @@ type ClientConfig struct {
MinBytesPerSecond int64
}
// Client defines the interface of a PMAPI client.
type Client interface {
Auth(username, password string, info *AuthInfo) (*Auth, error)
AuthInfo(username string) (*AuthInfo, error)
AuthRefresh(token string) (*Auth, error)
Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error)
Logout()
DeleteAuth() error
IsConnected() bool
ClearData()
CurrentUser() (*User, error)
UpdateUser() (*User, error)
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
UnlockAddresses(passphrase []byte) error
GetAddresses() (addresses AddressList, err error)
Addresses() AddressList
ReorderAddresses(addressIDs []string) error
GetEvent(eventID string) (*Event, error)
SendMessage(string, *SendMessageReq) (sent, parent *Message, err error)
CreateDraft(m *Message, parent string, action int) (created *Message, err error)
Import([]*ImportMsgReq) ([]*ImportMsgRes, error)
CountMessages(addressID string) ([]*MessagesCount, error)
ListMessages(filter *MessagesFilter) ([]*Message, int, error)
GetMessage(apiID string) (*Message, error)
DeleteMessages(apiIDs []string) error
LabelMessages(apiIDs []string, labelID string) error
UnlabelMessages(apiIDs []string, labelID string) error
MarkMessagesRead(apiIDs []string) error
MarkMessagesUnread(apiIDs []string) error
ListLabels() ([]*Label, error)
CreateLabel(label *Label) (*Label, error)
UpdateLabel(label *Label) (*Label, error)
DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
SendSimpleMetric(category, action, label string) error
GetMailSettings() (MailSettings, error)
GetContactEmailByEmail(string, int, int) ([]ContactEmail, error)
GetContactByID(string) (Contact, error)
DecryptAndVerifyCards([]Card) ([]Card, error)
GetAttachment(id string) (att io.ReadCloser, err error)
CreateAttachment(att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error)
DeleteAttachment(attID string) (err error)
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing, err error)
GetPublicKeysForEmail(string) ([]PublicKey, bool, error)
}
// client is a client of the protonmail API. It implements the Client interface.
type client struct {
cm *ClientManager
@ -170,11 +113,12 @@ type client struct {
accessToken string
userID string
requestLocker sync.Locker
keyLocker sync.Locker
user *User
addresses AddressList
kr *pmcrypto.KeyRing
user *User
addresses AddressList
userKeyRing *crypto.KeyRing
addrKeyRing map[string]*crypto.KeyRing
keyRingLock sync.Locker
log *logrus.Entry
}
@ -186,7 +130,8 @@ func newClient(cm *ClientManager, userID string) *client {
hc: getHTTPClient(cm.config, cm.roundTripper),
userID: userID,
requestLocker: &sync.Mutex{},
keyLocker: &sync.Mutex{},
keyRingLock: &sync.Mutex{},
addrKeyRing: make(map[string]*crypto.KeyRing),
log: logrus.WithField("pkg", "pmapi").WithField("userID", userID),
}
}
@ -199,6 +144,39 @@ func getHTTPClient(cfg *ClientConfig, rt http.RoundTripper) (hc *http.Client) {
}
}
func (c *client) IsUnlocked() bool {
return c.userKeyRing != nil
}
// Unlock unlocks all the user and address keys using the given passphrase.
func (c *client) Unlock(passphrase []byte) (err error) {
c.keyRingLock.Lock()
defer c.keyRingLock.Unlock()
// If the user already has a keyring, we already unlocked, so no need to try again.
if c.userKeyRing != nil {
return
}
if _, err = c.CurrentUser(); err != nil {
return
}
if c.user == nil || c.addresses == nil {
return errors.New("user data is not loaded")
}
if err = c.unlockUser(passphrase); err != nil {
return errors.Wrap(err, "failed to unlock user")
}
if err = c.unlockAddresses(passphrase); err != nil {
return errors.Wrap(err, "failed to unlock addresses")
}
return
}
// Do makes an API request. It does not check for HTTP status code errors.
func (c *client) Do(req *http.Request, retryUnauthorized bool) (res *http.Response, err error) {
// Copy the request body in case we need to retry it.
@ -258,7 +236,7 @@ func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthori
resDate := res.Header.Get("Date")
if resDate != "" {
if serverTime, err := http.ParseTime(resDate); err == nil {
pmcrypto.GetGopenPGP().UpdateTime(serverTime.Unix())
crypto.UpdateTime(serverTime.Unix())
}
}

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) {
c := newTestClient(newTestClientManager(testClientConfig))
c.kr = testPrivateKeyRing
c.userKeyRing = testPrivateKeyRing
cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext)
assert.Nil(t, err)
@ -669,7 +669,7 @@ func TestClient_Encrypt(t *testing.T) {
func TestClient_Decrypt(t *testing.T) {
c := newTestClient(newTestClientManager(testClientConfig))
c.kr = testPrivateKeyRing
c.userKeyRing = testPrivateKeyRing
cardCleartext, err := c.DecryptAndVerifyCards(testCardsEncrypted)
assert.Nil(t, err)

View File

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

View File

@ -20,183 +20,152 @@ package pmapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"sync"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// clearableKey is a region of memory intended to hold a private key and which can be securely
// cleared by calling clear().
type clearableKey []byte
// UnmarshalJSON Removes quotation and unescapes CR, LF.
func (pk *clearableKey) UnmarshalJSON(b []byte) (err error) {
b = bytes.Trim(b, "\"")
b = bytes.ReplaceAll(b, []byte("\\n"), []byte("\n"))
b = bytes.ReplaceAll(b, []byte("\\r"), []byte("\r"))
*pk = b
return
}
// clear irreversibly destroys the full range of `clearableKey` by filling it with zeros to ensure
// nobody can see what was in there (e.g. while waiting for the garbage collector to clean it up).
func (pk *clearableKey) clear() {
for b := range *pk {
(*pk)[b] = 0
}
}
type PMKey struct {
ID string
Version int
Flags int
Fingerprint string
PrivateKey *crypto.Key
Primary int
Token *string `json:",omitempty"`
Signature *string `json:",omitempty"`
}
type PMKeys struct {
Keys []PMKey
KeyRing *pmcrypto.KeyRing
}
func (key *PMKey) UnmarshalJSON(b []byte) (err error) {
type _PMKey PMKey
func (k *PMKeys) UnmarshalJSON(b []byte) (err error) {
var rawKeys []struct {
PMKey
PrivateKey clearableKey
}
if err = json.Unmarshal(b, &rawKeys); err != nil {
rawKey := struct {
_PMKey
PrivateKey string
}{}
if err = json.Unmarshal(b, &rawKey); err != nil {
return
}
k.KeyRing = &pmcrypto.KeyRing{}
for _, rawKey := range rawKeys {
err = k.KeyRing.ReadFrom(bytes.NewReader(rawKey.PrivateKey), true)
rawKey.PrivateKey.clear()
if err != nil {
return
}
k.Keys = append(k.Keys, rawKey.PMKey)
}
if len(k.Keys) > 0 {
k.KeyRing.FirstKeyID = k.Keys[0].ID
*key = PMKey(rawKey._PMKey)
if key.PrivateKey, err = crypto.NewKeyFromArmored(rawKey.PrivateKey); err != nil {
return errors.Wrap(err, "failed to create crypto key from armored private key")
}
return
}
// unlockKeyRing tries to unlock them with the provided keyRing using the token
// and if the token is not available it will use passphrase. It will not fail
// if keyring contains at least one unlocked private key.
func (k *PMKeys) unlockKeyRing(userKeyring *pmcrypto.KeyRing, passphrase []byte, locker sync.Locker) (err error) {
locker.Lock()
defer locker.Unlock()
func (key PMKey) getPassphraseFromToken(kr *crypto.KeyRing) (passphrase []byte, err error) {
if kr == nil {
return nil, errors.New("no user key was provided")
}
if k == nil {
err = errors.New("keys is a nil object")
msg, err := crypto.NewPGPMessageFromArmored(*key.Token)
if err != nil {
return
}
for _, key := range k.Keys {
sig, err := crypto.NewPGPSignatureFromArmored(*key.Signature)
if err != nil {
return
}
token, err := kr.Decrypt(msg, nil, 0)
if err != nil {
return
}
if err = kr.VerifyDetached(token, sig, 0); err != nil {
return
}
return token.GetBinary(), nil
}
func (key PMKey) unlock(passphrase []byte) (unlockedKey *crypto.Key, err error) {
if unlockedKey, err = key.PrivateKey.Unlock(passphrase); err != nil {
return
}
ok, err := unlockedKey.Check()
if err != nil {
return
}
if !ok {
err = errors.New("private and public keys do not match")
return
}
return
}
type PMKeys []PMKey
// UnlockAll goes through each key and unlocks it, returning a keyring containing all unlocked keys,
// or an error if at least one could not be unlocked.
// The passphrase is used to unlock the key unless the key's token and signature are both non-nil,
// in which case the given userkey is used to deduce the passphrase.
func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *crypto.KeyRing, err error) {
if kr, err = crypto.NewKeyRing(nil); err != nil {
return
}
for _, key := range *keys {
var secret []byte
if key.Token == nil || key.Signature == nil {
if err = unlockKeyRingNoErrorWhenAlreadyUnlocked(k.KeyRing, passphrase); err != nil {
return
}
secret = passphrase
} else if secret, err = key.getPassphraseFromToken(userKey); err != nil {
return
}
var k *crypto.Key
if k, err = key.unlock(secret); err != nil {
logrus.WithError(err).Warn("Failed to unlock key")
continue
}
message, err := pmcrypto.NewPGPMessageFromArmored(*key.Token)
if err != nil {
return err
}
signature, err := pmcrypto.NewPGPSignatureFromArmored(*key.Signature)
if err != nil {
return err
}
if userKeyring == nil {
return errors.New("userkey required to decrypt tokens but wasn't provided")
}
token, err := userKeyring.Decrypt(message, nil, 0)
if err != nil {
return err
}
err = userKeyring.VerifyDetached(token, signature, 0)
if err != nil {
return err
}
err = unlockKeyRingNoErrorWhenAlreadyUnlocked(k.KeyRing, token.GetBinary())
if err != nil {
return fmt.Errorf("wrong token: %v", err)
if err = kr.AddKey(k); err != nil {
logrus.WithError(err).Warn("Failed to add key to keyring")
continue
}
}
return nil
}
type unlockError struct {
error
}
func (err *unlockError) Error() string {
return "Invalid mailbox password (" + err.error.Error() + ")"
}
// IsUnlockError checks whether the error is due to failure to unlock (which is represented by an unexported type).
func IsUnlockError(err error) bool {
_, ok := err.(*unlockError)
return ok
}
func unlockKeyRingNoErrorWhenAlreadyUnlocked(kr *pmcrypto.KeyRing, passphrase []byte) (err error) {
if err = kr.Unlock(passphrase); err != nil {
// Do not fail if it has already unlocked keys.
hasUnlockedKey := false
for _, e := range kr.GetEntities() {
if e.PrivateKey != nil && !e.PrivateKey.Encrypted {
hasUnlockedKey = true
break
}
for _, se := range e.Subkeys {
if se.PrivateKey != nil && (!se.Sig.FlagsValid || se.Sig.FlagEncryptStorage || se.Sig.FlagEncryptCommunications) && !e.PrivateKey.Encrypted {
hasUnlockedKey = true
break
}
}
if hasUnlockedKey {
break
}
}
if !hasUnlockedKey {
err = &unlockError{err}
return
}
err = nil
if kr.CountEntities() == 0 {
err = errors.New("no keys could be unlocked")
return
}
return
return kr, err
}
// ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities.
var ErrNoKeyringAvailable = errors.New("no keyring available")
func (c *client) encrypt(plain string, signer *pmcrypto.KeyRing) (armored string, err error) {
return encrypt(c.kr, plain, signer)
func (c *client) encrypt(plain string, signer *crypto.KeyRing) (armored string, err error) {
return encrypt(c.userKeyRing, plain, signer)
}
func encrypt(encrypter *pmcrypto.KeyRing, plain string, signer *pmcrypto.KeyRing) (armored string, err error) {
if encrypter == nil || encrypter.FirstKey() == nil {
func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (armored string, err error) {
if encrypter == nil {
return "", ErrNoKeyringAvailable
}
plainMessage := pmcrypto.NewPlainMessageFromString(plain)
firstKey, err := encrypter.FirstKey()
if err != nil {
return "", err
}
plainMessage := crypto.NewPlainMessageFromString(plain)
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
pgpMessage, err := encrypter.FirstKey().Encrypt(plainMessage, signer)
pgpMessage, err := firstKey.Encrypt(plainMessage, signer)
if err != nil {
return
}
@ -204,14 +173,14 @@ func encrypt(encrypter *pmcrypto.KeyRing, plain string, signer *pmcrypto.KeyRing
}
func (c *client) decrypt(armored string) (plain string, err error) {
return decrypt(c.kr, armored)
return decrypt(c.userKeyRing, armored)
}
func decrypt(decrypter *pmcrypto.KeyRing, armored string) (plainBody string, err error) {
func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody string, err error) {
if decrypter == nil {
return "", ErrNoKeyringAvailable
}
pgpMessage, err := pmcrypto.NewPGPMessageFromArmored(armored)
pgpMessage, err := crypto.NewPGPMessageFromArmored(armored)
if err != nil {
return
}
@ -223,11 +192,11 @@ func decrypt(decrypter *pmcrypto.KeyRing, armored string) (plainBody string, err
}
func (c *client) sign(plain string) (armoredSignature string, err error) {
if c.kr == nil {
if c.userKeyRing == nil {
return "", ErrNoKeyringAvailable
}
plainMessage := pmcrypto.NewPlainMessageFromString(plain)
pgpSignature, err := c.kr.SignDetached(plainMessage)
plainMessage := crypto.NewPlainMessageFromString(plain)
pgpSignature, err := c.userKeyRing.SignDetached(plainMessage)
if err != nil {
return
}
@ -235,34 +204,44 @@ func (c *client) sign(plain string) (armoredSignature string, err error) {
}
func (c *client) verify(plain, amroredSignature string) (err error) {
plainMessage := pmcrypto.NewPlainMessageFromString(plain)
pgpSignature, err := pmcrypto.NewPGPSignatureFromArmored(amroredSignature)
plainMessage := crypto.NewPlainMessageFromString(plain)
pgpSignature, err := crypto.NewPGPSignatureFromArmored(amroredSignature)
if err != nil {
return
}
verifyTime := int64(0) // By default it will use current timestamp.
return c.kr.VerifyDetached(plainMessage, pgpSignature, verifyTime)
return c.userKeyRing.VerifyDetached(plainMessage, pgpSignature, verifyTime)
}
func encryptAttachment(kr *pmcrypto.KeyRing, data io.Reader, filename string) (encrypted io.Reader, err error) {
if kr == nil || kr.FirstKey() == nil {
func encryptAttachment(kr *crypto.KeyRing, data io.Reader, filename string) (encrypted io.Reader, err error) {
if kr == nil {
return nil, ErrNoKeyringAvailable
}
firstKey, err := kr.FirstKey()
if err != nil {
return nil, err
}
dataBytes, err := ioutil.ReadAll(data)
if err != nil {
return
}
plainMessage := pmcrypto.NewPlainMessage(dataBytes)
plainMessage := crypto.NewPlainMessage(dataBytes)
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
pgpSplitMessage, err := kr.FirstKey().EncryptAttachment(plainMessage, filename)
pgpSplitMessage, err := firstKey.EncryptAttachment(plainMessage, filename)
if err != nil {
return
}
packets := append(pgpSplitMessage.KeyPacket, pgpSplitMessage.DataPacket...)
return bytes.NewReader(packets), nil
}
func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader) (decrypted io.Reader, err error) {
func decryptAttachment(kr *crypto.KeyRing, keyPackets []byte, data io.Reader) (decrypted io.Reader, err error) {
if kr == nil {
return nil, ErrNoKeyringAvailable
}
@ -270,7 +249,7 @@ func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader)
if err != nil {
return
}
pgpSplitMessage := pmcrypto.NewPGPSplitMessage(keyPackets, dataBytes)
pgpSplitMessage := crypto.NewPGPSplitMessage(keyPackets, dataBytes)
plainMessage, err := kr.DecryptAttachment(pgpSplitMessage)
if err != nil {
return
@ -278,7 +257,7 @@ func decryptAttachment(kr *pmcrypto.KeyRing, keyPackets []byte, data io.Reader)
return plainMessage.NewReader(), nil
}
func signAttachment(encrypter *pmcrypto.KeyRing, data io.Reader) (signature io.Reader, err error) {
func signAttachment(encrypter *crypto.KeyRing, data io.Reader) (signature io.Reader, err error) {
if encrypter == nil {
return nil, ErrNoKeyringAvailable
}
@ -286,7 +265,7 @@ func signAttachment(encrypter *pmcrypto.KeyRing, data io.Reader) (signature io.R
if err != nil {
return
}
plainMessage := pmcrypto.NewPlainMessage(dataBytes)
plainMessage := crypto.NewPlainMessage(dataBytes)
sig, err := encrypter.SignDetached(plainMessage)
if err != nil {
return

View File

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

View File

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

View File

@ -20,10 +20,9 @@ package pmapi
import (
"fmt"
"net/http"
"strings"
"testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/assert"
)
@ -142,10 +141,15 @@ func TestMessage_Decrypt(t *testing.T) {
func TestMessage_Decrypt_Legacy(t *testing.T) {
testPrivateKeyLegacy := readTestFile("testPrivateKeyLegacy", false)
testPrivateKeyRingLegacy, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPrivateKeyLegacy))
key, err := crypto.NewKeyFromArmored(testPrivateKeyLegacy)
Ok(t, err)
Ok(t, testPrivateKeyRingLegacy.Unlock([]byte(testMailboxPasswordLegacy)))
unlockedKey, err := key.Unlock([]byte(testMailboxPasswordLegacy))
Ok(t, err)
testPrivateKeyRingLegacy, err := crypto.NewKeyRing(unlockedKey)
Ok(t, err)
msg := &Message{Body: testMessageEncryptedLegacy}
@ -163,7 +167,10 @@ func TestMessage_Decrypt_signed(t *testing.T) {
}
func TestMessage_Encrypt(t *testing.T) {
signer, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testMessageSigner))
key, err := crypto.NewKeyFromArmored(testMessageSigner)
Ok(t, err)
signer, err := crypto.NewKeyRing(key)
Ok(t, err)
msg := &Message{Body: testMessageCleartext}
@ -173,7 +180,7 @@ func TestMessage_Encrypt(t *testing.T) {
Ok(t, err)
Equals(t, testMessageCleartext, msg.Body)
Equals(t, testIdentity, signer.Identities()[0])
Equals(t, testIdentity, signer.GetIdentities()[0])
}
func routeLabelMessages(tb testing.TB, w http.ResponseWriter, r *http.Request) string {

View File

@ -8,7 +8,7 @@ import (
io "io"
reflect "reflect"
crypto "github.com/ProtonMail/gopenpgp/crypto"
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
)
@ -110,6 +110,21 @@ func (mr *MockClientMockRecorder) AuthRefresh(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRefresh", reflect.TypeOf((*MockClient)(nil).AuthRefresh), arg0)
}
// AuthSalt mocks base method
func (m *MockClient) AuthSalt() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthSalt")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthSalt indicates an expected call of AuthSalt
func (mr *MockClientMockRecorder) AuthSalt() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthSalt", reflect.TypeOf((*MockClient)(nil).AuthSalt))
}
// ClearData mocks base method
func (m *MockClient) ClearData() {
m.ctrl.T.Helper()
@ -432,6 +447,20 @@ func (mr *MockClientMockRecorder) IsConnected() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockClient)(nil).IsConnected))
}
// IsUnlocked mocks base method
func (m *MockClient) IsUnlocked() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsUnlocked")
ret0, _ := ret[0].(bool)
return ret0
}
// IsUnlocked indicates an expected call of IsUnlocked
func (mr *MockClientMockRecorder) IsUnlocked() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUnlocked", reflect.TypeOf((*MockClient)(nil).IsUnlocked))
}
// KeyRingForAddressID mocks base method
func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
@ -605,12 +634,11 @@ func (mr *MockClientMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomoc
}
// Unlock mocks base method
func (m *MockClient) Unlock(arg0 string) (*crypto.KeyRing, error) {
func (m *MockClient) Unlock(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
ret1, _ := ret[1].(error)
return ret0, ret1
ret0, _ := ret[0].(error)
return ret0
}
// Unlock indicates an expected call of Unlock
@ -619,20 +647,6 @@ func (mr *MockClientMockRecorder) Unlock(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockClient)(nil).Unlock), arg0)
}
// UnlockAddresses mocks base method
func (m *MockClient) UnlockAddresses(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlockAddresses", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UnlockAddresses indicates an expected call of UnlockAddresses
func (mr *MockClientMockRecorder) UnlockAddresses(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlockAddresses", reflect.TypeOf((*MockClient)(nil).UnlockAddresses), arg0)
}
// UpdateLabel mocks base method
func (m *MockClient) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()

View File

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

View File

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

View File

@ -18,8 +18,8 @@
package pmapi
import (
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/getsentry/raven-go"
"github.com/pkg/errors"
)
// Role values.
@ -68,15 +68,17 @@ type User struct {
Private int
Subscribed int
Services int
VPN struct {
Deliquent int
Keys PMKeys
VPN struct {
Status int
ExpirationTime int
PlanName string
MaxConnect int
MaxTier int
}
Deliquent int
Keys PMKeys
}
// UserRes holds structure of JSON response.
@ -86,9 +88,17 @@ type UserRes struct {
User *User
}
// KeyRing returns the (possibly unlocked) PMKeys KeyRing.
func (u *User) KeyRing() *pmcrypto.KeyRing {
return u.Keys.KeyRing
// unlockUser unlocks all the client's user keys using the given passphrase.
func (c *client) unlockUser(passphrase []byte) (err error) {
if c.userKeyRing != nil {
return
}
if c.userKeyRing, err = c.user.Keys.UnlockAll(passphrase, nil); err != nil {
return errors.Wrap(err, "failed to unlock user keys")
}
return
}
// UpdateUser retrieves details about user and loads its addresses.

View File

@ -23,7 +23,7 @@ import (
"net/url"
"testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
r "github.com/stretchr/testify/require"
@ -72,9 +72,9 @@ func TestClient_CurrentUser(t *testing.T) {
r.Nil(t, err)
// Ignore KeyRings during the check because they have unexported fields and cannot be compared
r.True(t, cmp.Equal(user, testCurrentUser, cmpopts.IgnoreTypes(&pmcrypto.KeyRing{})))
r.True(t, cmp.Equal(user, testCurrentUser, cmpopts.IgnoreTypes(&crypto.Key{})))
r.Nil(t, c.UnlockAddresses([]byte(testMailboxPassword)))
r.Nil(t, c.Unlock([]byte(testMailboxPassword)))
}
func TestClient_PublicKeys(t *testing.T) {

View File

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

View File

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

View File

@ -147,6 +147,14 @@ func (api *FakePMAPI) AuthRefresh(token string) (*pmapi.Auth, error) {
return auth, nil
}
func (api *FakePMAPI) AuthSalt() (string, error) {
if err := api.checkInternetAndRecordCall(GET, "/keys/salts", nil); err != nil {
return "", err
}
return "", nil
}
func (api *FakePMAPI) Logout() {
api.controller.clientManager.LogoutClient(api.userID)
}
@ -164,5 +172,17 @@ func (api *FakePMAPI) DeleteAuth() error {
}
func (api *FakePMAPI) ClearData() {
if api.userKeyRing != nil {
api.userKeyRing.ClearPrivateParams()
api.userKeyRing = nil
}
for addrID, addr := range api.addrKeyRing {
if addr != nil {
addr.ClearPrivateParams()
delete(api.addrKeyRing, addrID)
}
}
api.unsetUser()
}

View File

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus"
)
@ -33,12 +34,14 @@ type FakePMAPI struct {
controller *Controller
eventIDGenerator idGenerator
auths chan<- *pmapi.Auth
user *pmapi.User
addresses *pmapi.AddressList
labels []*pmapi.Label
messages []*pmapi.Message
events []*pmapi.Event
auths chan<- *pmapi.Auth
user *pmapi.User
userKeyRing *crypto.KeyRing
addresses *pmapi.AddressList
addrKeyRing map[string]*crypto.KeyRing
labels []*pmapi.Label
messages []*pmapi.Message
events []*pmapi.Event
// uid represents the API UID. It is the unique session ID.
uid, lastToken string
@ -48,9 +51,10 @@ type FakePMAPI struct {
func New(controller *Controller, userID string) *FakePMAPI {
fakePMAPI := &FakePMAPI{
controller: controller,
log: logrus.WithField("pkg", "fakeapi"),
userID: userID,
controller: controller,
log: logrus.WithField("pkg", "fakeapi"),
userID: userID,
addrKeyRing: make(map[string]*crypto.KeyRing),
}
fakePMAPI.addEvent(&pmapi.Event{

View File

@ -18,7 +18,7 @@
package fakeapi
import (
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
@ -29,13 +29,37 @@ func (api *FakePMAPI) GetMailSettings() (pmapi.MailSettings, error) {
return pmapi.MailSettings{}, nil
}
func (api *FakePMAPI) Unlock(mailboxPassword string) (*pmcrypto.KeyRing, error) {
return &pmcrypto.KeyRing{
FirstKeyID: "key",
}, nil
func (api *FakePMAPI) IsUnlocked() bool {
return api.userKeyRing != nil
}
func (api *FakePMAPI) UnlockAddresses(password []byte) error {
func (api *FakePMAPI) Unlock(passphrase []byte) (err error) {
if api.userKeyRing != nil {
return
}
if api.userKeyRing, err = api.user.Keys.UnlockAll(passphrase, nil); err != nil {
return
}
for _, a := range *api.addresses {
if a.HasKeys == pmapi.MissingKeys {
continue
}
if api.addrKeyRing[a.ID] != nil {
continue
}
var kr *crypto.KeyRing
if kr, err = a.Keys.UnlockAll(passphrase, api.userKeyRing); err != nil {
return
}
api.addrKeyRing[a.ID] = kr
}
return nil
}
@ -47,6 +71,7 @@ func (api *FakePMAPI) UpdateUser() (*pmapi.User, error) {
if err := api.checkAndRecordCall(GET, "/users", nil); err != nil {
return nil, err
}
return api.user, nil
}
@ -84,8 +109,6 @@ func (api *FakePMAPI) Addresses() pmapi.AddressList {
return *api.addresses
}
func (api *FakePMAPI) KeyRingForAddressID(addrID string) (*pmcrypto.KeyRing, error) {
return &pmcrypto.KeyRing{
FirstKeyID: "key",
}, nil
func (api *FakePMAPI) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) {
return api.addrKeyRing[addrID], nil
}

View File

@ -71,7 +71,7 @@ Feature: IMAP auth
@ignore-live
Scenario: Authenticates with disabled primary address
Given there is connected user "userDisabledPrimaryAddress"
When IMAP client authenticates "userDisabledPrimaryAddress" with address "primary"
When IMAP client authenticates "userDisabledPrimaryAddress" with address "disabled"
Then IMAP response is "OK"
Scenario: Authenticates two users

View File

@ -48,7 +48,7 @@ Feature: SMTP auth
@ignore-live
Scenario: Authenticates with disabled primary address
Given there is connected user "userDisabledPrimaryAddress"
When SMTP client authenticates "userDisabledPrimaryAddress" with address "primary"
When SMTP client authenticates "userDisabledPrimaryAddress" with address "secondary"
Then SMTP response is "OK"
Scenario: Authenticates two users

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 {
addresses, err := client.GetAddresses()
kr, err := client.KeyRingForAddressID(message.AddressID)
if err != nil {
return errors.Wrap(err, "failed to get address")
return errors.Wrap(err, "failed to get keyring for address")
}
kr := addresses.ByID(message.AddressID).KeyRing()
if err = message.Encrypt(kr, nil); err != nil {
return errors.Wrap(err, "failed to encrypt message body")

View File

@ -34,21 +34,25 @@ func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, p
if err != nil {
return errors.Wrap(err, "failed to get auth info")
}
auth, err := client.Auth(user.Name, password, authInfo)
_, err = client.Auth(user.Name, password, authInfo)
if err != nil {
return errors.Wrap(err, "failed to auth user")
}
mailboxPassword, err := pmapi.HashMailboxPassword(password, auth.KeySalt)
salt, err := client.AuthSalt()
if err != nil {
return errors.Wrap(err, "failed to get salt")
}
mailboxPassword, err := pmapi.HashMailboxPassword(password, salt)
if err != nil {
return errors.Wrap(err, "failed to hash mailbox password")
}
if _, err := client.Unlock(mailboxPassword); err != nil {
if err := client.Unlock([]byte(mailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user")
}
if err := client.UnlockAddresses([]byte(mailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock addresses")
}
if err := cleanup(client, addresses); err != nil {
return errors.Wrap(err, "failed to clean user")

View File

@ -2,7 +2,7 @@
{
"ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==",
"Version": 3,
"PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: OpenPGP.js v0.7.1\r\nComment: http://openpgpjs.org\r\n\r\n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\nqqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==\r\n=2wIY\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n",
"PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n\r\n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\r\na7RmMPBR30nWAMrvteun5z2n8rizgEZHcmE=\r\n=2e5K\r\n-----END PGP PRIVATE KEY BLOCK-----",
"Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353",
"Activation": null,
"Primary": 1