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