mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
feat: migrate to gopenpgp v2
This commit is contained in:
@ -15,6 +15,9 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-308 Better user error message when request is canceled.
|
||||
* GODT-312 Validate recipient emails in send before asking for their public keys.
|
||||
* GODT-368 Bump docker-credential-helpers version.
|
||||
* GODT-280 Migrate to gopenpgp v2
|
||||
* `Unlock()` call on pmapi-client unlocks both User keys and Address keys
|
||||
* Salt is available via `AuthSalt()` method
|
||||
|
||||
### Fixed
|
||||
* GODT-356 Fix crash when removing account while mail client is fetching messages (regression from GODT-204).
|
||||
|
||||
4
go.mod
4
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
|
||||
)
|
||||
|
||||
13
go.sum
13
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=
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Thu 07 May 2020 08:24:48 PM CEST. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Fri 05 Jun 2020 09:09:44 AM CEST. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./release-notes.sh at Thu 07 May 2020 08:46:50 PM CEST. DO NOT EDIT.
|
||||
// Code generated by ./release-notes.sh at Tue 09 Jun 2020 10:21:50 AM CEST. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
76
internal/smtp/repro_test.go
Normal file
76
internal/smtp/repro_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestThing(t *testing.T) {
|
||||
// Load the key.
|
||||
key, err := crypto.NewKeyFromArmored(testPublicKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Put it in a keyring.
|
||||
keyRing, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Filter out expired ones.
|
||||
validKeyRings, err := crypto.FilterExpiredKeys([]*crypto.KeyRing{keyRing})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Filtering shouldn't make them unequal.
|
||||
assert.True(t, isEqual(keyRing, validKeyRings[0]))
|
||||
}
|
||||
|
||||
func isEqual(a, b *crypto.KeyRing) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil && b != nil || a != nil && b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
aKeys, bKeys := a.GetKeys(), b.GetKeys()
|
||||
|
||||
if len(aKeys) != len(bKeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range aKeys {
|
||||
aFPs := aKeys[i].GetSHA256Fingerprints()
|
||||
bFPs := bKeys[i].GetSHA256Fingerprints()
|
||||
|
||||
if !cmp.Equal(aFPs, bFPs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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!")
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
4
pkg/message/testdata/text_plain_bad_sender.eml
vendored
Normal file
4
pkg/message/testdata/text_plain_bad_sender.eml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
From: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <sender@pm.me>
|
||||
To: Receiver <receiver@pm.me>
|
||||
|
||||
body
|
||||
5
pkg/message/testdata/text_plain_bad_subject.eml
vendored
Normal file
5
pkg/message/testdata/text_plain_bad_subject.eml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
From: Sender <sender@pm.me>
|
||||
To: Receiver <receiver@pm.me>
|
||||
Subject: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
|
||||
body
|
||||
12
pkg/message/testdata/text_plain_plain_attachment_latin1.eml
vendored
Normal file
12
pkg/message/testdata/text_plain_plain_attachment_latin1.eml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
From: Sender <sender@pm.me>
|
||||
To: Receiver <receiver@pm.me>
|
||||
Content-Type: multipart/mixed; boundary=longrandomstring
|
||||
|
||||
--longrandomstring
|
||||
|
||||
body
|
||||
--longrandomstring
|
||||
Content-Disposition: attachment
|
||||
|
||||
Aur<EFBFBD>lien is a latin1 name.
|
||||
--longrandomstring--
|
||||
12
pkg/message/testdata/text_plain_plain_attachment_latin2.eml
vendored
Normal file
12
pkg/message/testdata/text_plain_plain_attachment_latin2.eml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
From: Sender <sender@pm.me>
|
||||
To: Receiver <receiver@pm.me>
|
||||
Content-Type: multipart/mixed; boundary=longrandomstring
|
||||
|
||||
--longrandomstring
|
||||
|
||||
body
|
||||
--longrandomstring
|
||||
Content-Disposition: attachment
|
||||
|
||||
Aur<EFBFBD>lien is a latin1 name but this document is latin2.
|
||||
--longrandomstring--
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/jaytaylor/html2text"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -104,63 +104,6 @@ type ClientConfig struct {
|
||||
MinBytesPerSecond int64
|
||||
}
|
||||
|
||||
// Client defines the interface of a PMAPI client.
|
||||
type Client interface {
|
||||
Auth(username, password string, info *AuthInfo) (*Auth, error)
|
||||
AuthInfo(username string) (*AuthInfo, error)
|
||||
AuthRefresh(token string) (*Auth, error)
|
||||
Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error)
|
||||
Logout()
|
||||
DeleteAuth() error
|
||||
IsConnected() bool
|
||||
ClearData()
|
||||
|
||||
CurrentUser() (*User, error)
|
||||
UpdateUser() (*User, error)
|
||||
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
|
||||
UnlockAddresses(passphrase []byte) error
|
||||
|
||||
GetAddresses() (addresses AddressList, err error)
|
||||
Addresses() AddressList
|
||||
ReorderAddresses(addressIDs []string) error
|
||||
|
||||
GetEvent(eventID string) (*Event, error)
|
||||
|
||||
SendMessage(string, *SendMessageReq) (sent, parent *Message, err error)
|
||||
CreateDraft(m *Message, parent string, action int) (created *Message, err error)
|
||||
Import([]*ImportMsgReq) ([]*ImportMsgRes, error)
|
||||
|
||||
CountMessages(addressID string) ([]*MessagesCount, error)
|
||||
ListMessages(filter *MessagesFilter) ([]*Message, int, error)
|
||||
GetMessage(apiID string) (*Message, error)
|
||||
DeleteMessages(apiIDs []string) error
|
||||
LabelMessages(apiIDs []string, labelID string) error
|
||||
UnlabelMessages(apiIDs []string, labelID string) error
|
||||
MarkMessagesRead(apiIDs []string) error
|
||||
MarkMessagesUnread(apiIDs []string) error
|
||||
|
||||
ListLabels() ([]*Label, error)
|
||||
CreateLabel(label *Label) (*Label, error)
|
||||
UpdateLabel(label *Label) (*Label, error)
|
||||
DeleteLabel(labelID string) error
|
||||
EmptyFolder(labelID string, addressID string) error
|
||||
|
||||
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
|
||||
SendSimpleMetric(category, action, label string) error
|
||||
|
||||
GetMailSettings() (MailSettings, error)
|
||||
GetContactEmailByEmail(string, int, int) ([]ContactEmail, error)
|
||||
GetContactByID(string) (Contact, error)
|
||||
DecryptAndVerifyCards([]Card) ([]Card, error)
|
||||
|
||||
GetAttachment(id string) (att io.ReadCloser, err error)
|
||||
CreateAttachment(att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error)
|
||||
DeleteAttachment(attID string) (err error)
|
||||
|
||||
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing, err error)
|
||||
GetPublicKeysForEmail(string) ([]PublicKey, bool, error)
|
||||
}
|
||||
|
||||
// client is a client of the protonmail API. It implements the Client interface.
|
||||
type client struct {
|
||||
cm *ClientManager
|
||||
@ -170,11 +113,12 @@ type client struct {
|
||||
accessToken string
|
||||
userID string
|
||||
requestLocker sync.Locker
|
||||
keyLocker sync.Locker
|
||||
|
||||
user *User
|
||||
addresses AddressList
|
||||
kr *pmcrypto.KeyRing
|
||||
user *User
|
||||
addresses AddressList
|
||||
userKeyRing *crypto.KeyRing
|
||||
addrKeyRing map[string]*crypto.KeyRing
|
||||
keyRingLock sync.Locker
|
||||
|
||||
log *logrus.Entry
|
||||
}
|
||||
@ -186,7 +130,8 @@ func newClient(cm *ClientManager, userID string) *client {
|
||||
hc: getHTTPClient(cm.config, cm.roundTripper),
|
||||
userID: userID,
|
||||
requestLocker: &sync.Mutex{},
|
||||
keyLocker: &sync.Mutex{},
|
||||
keyRingLock: &sync.Mutex{},
|
||||
addrKeyRing: make(map[string]*crypto.KeyRing),
|
||||
log: logrus.WithField("pkg", "pmapi").WithField("userID", userID),
|
||||
}
|
||||
}
|
||||
@ -199,6 +144,39 @@ func getHTTPClient(cfg *ClientConfig, rt http.RoundTripper) (hc *http.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) IsUnlocked() bool {
|
||||
return c.userKeyRing != nil
|
||||
}
|
||||
|
||||
// Unlock unlocks all the user and address keys using the given passphrase.
|
||||
func (c *client) Unlock(passphrase []byte) (err error) {
|
||||
c.keyRingLock.Lock()
|
||||
defer c.keyRingLock.Unlock()
|
||||
|
||||
// If the user already has a keyring, we already unlocked, so no need to try again.
|
||||
if c.userKeyRing != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.CurrentUser(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.user == nil || c.addresses == nil {
|
||||
return errors.New("user data is not loaded")
|
||||
}
|
||||
|
||||
if err = c.unlockUser(passphrase); err != nil {
|
||||
return errors.Wrap(err, "failed to unlock user")
|
||||
}
|
||||
|
||||
if err = c.unlockAddresses(passphrase); err != nil {
|
||||
return errors.Wrap(err, "failed to unlock addresses")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Do makes an API request. It does not check for HTTP status code errors.
|
||||
func (c *client) Do(req *http.Request, retryUnauthorized bool) (res *http.Response, err error) {
|
||||
// Copy the request body in case we need to retry it.
|
||||
@ -258,7 +236,7 @@ func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthori
|
||||
resDate := res.Header.Get("Date")
|
||||
if resDate != "" {
|
||||
if serverTime, err := http.ParseTime(resDate); err == nil {
|
||||
pmcrypto.GetGopenPGP().UpdateTime(serverTime.Unix())
|
||||
crypto.UpdateTime(serverTime.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
82
pkg/pmapi/client_types.go
Normal file
82
pkg/pmapi/client_types.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package pmapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
)
|
||||
|
||||
// Client defines the interface of a PMAPI client.
|
||||
type Client interface {
|
||||
Auth(username, password string, info *AuthInfo) (*Auth, error)
|
||||
AuthInfo(username string) (*AuthInfo, error)
|
||||
AuthRefresh(token string) (*Auth, error)
|
||||
Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error)
|
||||
AuthSalt() (salt string, err error)
|
||||
Logout()
|
||||
DeleteAuth() error
|
||||
IsConnected() bool
|
||||
ClearData()
|
||||
|
||||
CurrentUser() (*User, error)
|
||||
UpdateUser() (*User, error)
|
||||
Unlock(passphrase []byte) (err error)
|
||||
IsUnlocked() bool
|
||||
|
||||
GetAddresses() (addresses AddressList, err error)
|
||||
Addresses() AddressList
|
||||
ReorderAddresses(addressIDs []string) error
|
||||
|
||||
GetEvent(eventID string) (*Event, error)
|
||||
|
||||
SendMessage(string, *SendMessageReq) (sent, parent *Message, err error)
|
||||
CreateDraft(m *Message, parent string, action int) (created *Message, err error)
|
||||
Import([]*ImportMsgReq) ([]*ImportMsgRes, error)
|
||||
|
||||
CountMessages(addressID string) ([]*MessagesCount, error)
|
||||
ListMessages(filter *MessagesFilter) ([]*Message, int, error)
|
||||
GetMessage(apiID string) (*Message, error)
|
||||
DeleteMessages(apiIDs []string) error
|
||||
LabelMessages(apiIDs []string, labelID string) error
|
||||
UnlabelMessages(apiIDs []string, labelID string) error
|
||||
MarkMessagesRead(apiIDs []string) error
|
||||
MarkMessagesUnread(apiIDs []string) error
|
||||
|
||||
ListLabels() ([]*Label, error)
|
||||
CreateLabel(label *Label) (*Label, error)
|
||||
UpdateLabel(label *Label) (*Label, error)
|
||||
DeleteLabel(labelID string) error
|
||||
EmptyFolder(labelID string, addressID string) error
|
||||
|
||||
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
|
||||
SendSimpleMetric(category, action, label string) error
|
||||
|
||||
GetMailSettings() (MailSettings, error)
|
||||
GetContactEmailByEmail(string, int, int) ([]ContactEmail, error)
|
||||
GetContactByID(string) (Contact, error)
|
||||
DecryptAndVerifyCards([]Card) ([]Card, error)
|
||||
|
||||
GetAttachment(id string) (att io.ReadCloser, err error)
|
||||
CreateAttachment(att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error)
|
||||
DeleteAttachment(attID string) (err error)
|
||||
|
||||
KeyRingForAddressID(string) (kr *crypto.KeyRing, err error)
|
||||
GetPublicKeysForEmail(string) ([]PublicKey, bool, error)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
2
test/testdata/user_key.json
vendored
2
test/testdata/user_key.json
vendored
@ -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\nlQPGBF7eQb8BCADckM9r50YFWK5teNVbkauuzOVAqejr4lIiKQ78k/TGy4PYWab9\r\nQF2EeiUrm/Yk5eKn97zxdv7gzT0Eu9WTZ7T9GRdH8WsI4RnK6UYDuXr/GTy9GjVB\r\njEIpHiPVwS0fmyM0oj7ldvHq/ahqjeijuJPJHow+dx4BI4eQ84D4S7zgiMKst1lC\r\nUEqMxMLAUBVFjYds6SLQGG5jeM6oMUCWQOTScU9PoM6WXtdnbq3eu2coGdEy/tp0\r\njgfQJBZpX3k9Gp5R4e4b0uCOwqad2DczvLXmkvW9e0sLhInp3r0YcJsf9mnmNFpR\r\nSzbyZ+3f3zu7QF4emK/dBv0aBvz5doEynfUlABEBAAH+BwMCf1ibmkdLnBPrLPoM\r\nSy9Ov+v20mLTGdmIR9u5PsUKiP5wHMFL6Flyu0rNrcaO9Hxq4hnucSQG7RxowuDq\r\nSzrXbrbVx54KMkJKy5fi9BudwGR2a4t85WZLW7sK86fojAbBGdjCUzNlDmMcKce3\r\nExrfdV05NZ+j+XbFTeKEqLM3qXiJOqgy1TluO+TalvuMKhbtBxrvb/x51plk8bs8\r\nkOsIahD1V1P1Eoky3VUk2YWErgTL9AFtSy5mn4d34AZkPKMWi1epac4jUCPQeW/9\r\neBnVtqRwKSA6SOvbHz0SzcYLBwIPinIAky7hun2fnKmb/ML8RB2zL4qIjt9+qdoZ\r\nckYu/4sOpjMap2WniFO/3tLSQsKQ7j+cwO5KBTzPsBiDrQyt1YieQJgTRvZJcN2J\r\nLXbK93VeORzBBZXO3czKjTHxOGYfr4L58Z3vSIE+0xuBoLCuZJOwJsqF3c7XjRVU\r\nh8MtEc0gcIsGtjGQ9+0ACPq3kGlukZeZpcRy8iGI8s8bm1zwaarC51OUEOl94F3C\r\nXZpL49xy7FWRlQDM+Zo+WQXnlPRjH14ypR0OaairrsETvEhCB28B6N66b4BvFaFs\r\n/sNmWHst+JqGPAzvcO9G9RHGziOfGfmm5RsUXB2CCjTbICMdEoHyxpyHmJb39lG+\r\n9SVP6YXikkmNjmLBif+Yr7pYwj/WoWY+bnLdX8dPuANoamQaKBKdlEy6lMbB+SWY\r\nJokqCsTZoqab4CvkhzlPdodzPSn6aBDX2a4XYG0kRbakGiLr66Q5yQnSXO+zPsb3\r\nPyI/46+B7Ptf3gv5BP0HfoPpvId1nY8OdlDbE0SZBgU/OovAXTxKSxYC4LeeeaL+\r\nBWJ96cMX9Yq8RbNIbJiEpkPtMsYVY6AC9O78RqlHYZ0vIIXrSEPqKxyvFdZEgoAp\r\ngvEecldX4XA+tBFVc2VyIDx1c2VyQHBtLm1lPokBVAQTAQgAPhYhBH6LDdjsjqr6\r\n2b4BFZJ8zbLJyc/VBQJe3kG/AhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4B\r\nAheAAAoJEJJ8zbLJyc/VWeUIAK7IOAI1/qiTKyzbh7qhjV02TtwCzpkk78MWrGoG\r\nvCIRfq2pbyj4hpYqg1AGddyc7o7odJQ3ATPkPzqwDaqwmiKhloimrreIcpnZYb5H\r\n2UTtzJG47DuGwPPyPjYcgjU9jIZnHVIG0DKjJ5RBB3kuttjoEEtj/7c+11FvzyJe\r\nQU6Gs7Sn30yDAT/JL3TZNOlCwoOTqIcsOGqD26r6IWYKtimTBEd+avidWJ9zUqee\r\nBFdKnYVphLCifFAw/8LgFVTITZGwHPWhSo9nKcJidigOHSCztw7LjLY2nGmAT8JJ\r\n7DC4pWktDX9S0qeQGMdwPgiYOrDrA+JNU7KrQcUsK7vnAo2dA8UEXt5BvwEIAJJy\r\neEO7CRPxPMsjm95PnvCt6pXH1cqDjAYCi2h8eaQmXdiLpzkY3M690UN6IHmMJ74V\r\nfObvlr+y3LogSaIdQk7V5AcQKGTQnvgOXqhTBcJCkIrt7nzkPXviSLUrAK52xjBz\r\neG1DIAmhK3ngHereE4AUin+sFeosrLAL3w9Dr2IRgj188vZzaexHCn9s8fwOEyKg\r\nabAXJVopUQuF0FQFw7/Fut+oxLTWmOV2oRpfQuz8NwTGxAKJy713yM8/NTVW6Ckc\r\nqHEByzi8S6KPbshzEGojl8eOZRKjQcshzhnyfsLjJv7Y5bFB45v0D3dBY7CsgZWr\r\naO/23PlyA3STkzJ1uCEAEQEAAf4HAwIPZVhp2zJo2+uj5RJ6EmJOY1I7EJUtzXMO\r\nUeRVsKmK5I06ER0wLQYub97/gB22KN+FHtH9kAezMPdFC405cvZoe6gxjYF9DryO\r\n8CaD+4fHmWj0u+qNlMI+OgCHo5lnX3537tCqoF1gYqLAjv6z82HjD3CQfFL09Ijr\r\n0mA9oHoRl5ORU9/G2HDwmtrxdv/lx1JBx17Qo8yjM9DFtpR277JfRKqMr6rXRveu\r\niQ1Boh4YJTOYeAv1TdykQIkQ4Wx3ok3ZRBd41etR0BhJyO5KWvB3Xth3K13pnDf+\r\nCA0DBs4J+nCqffHknrWWgUXv5aXnD0zwJI90gyqiJFNNcSBPH+TQQnttV2zpb7eK\r\nmck5ykAkIbWLc1ExSZIsUyaHdADM9RyQ/xMT6cHDnczoYAd0L/TjYdXkqDmPuhgN\r\nsh+4k3cpBcFBDMAB4RzeWvmK0YAfxO6fmR0nHddM60AwvVA0ebeQcyU0Igc7gnUv\r\nLMkITr2hydy34fpSPwS4Ap1YTjGHquVOEWDCQzVRB8CoJXV8RsvwvOHKQr2Or6dV\r\n1tU9wVIVs41ES/yjGPp95zgclAh6s5GbigT2Ncj7mk3nMuLkUYiffcxMRnT8yUUe\r\nkX0yzoEYTcXPkfOhoLOGhEOBzTGTGw/wwmxF4gOxhA7sZoS2K1sJXQHArzskeqYQ\r\nCSRL/YfRA1wnSoUBUN/p9HbTc/kuXHijWrNEOETyWyUsCAgE7F6OknJTxppDLi3l\r\nfWVKuALGXPjdjtsrmsi0LGEAjq9TkQMqD8bJLpTQbYPNd9ALTiOx1eabUKMX8EGd\r\nGdjynEW7eojw2bhEu0IVEpxBcxW64yVo0fIOaXV/Rfh2e3MejEa2a1MKP+V511PF\r\npVc+l2c7/CWhT3H8PAFL+jq5i8aRRNd0fcxZr0n6mrK5WfHZ6WcFe2w1k4kBPAQY\r\nAQgAJhYhBH6LDdjsjqr62b4BFZJ8zbLJyc/VBQJe3kG/AhsMBQkDwmcAAAoJEJJ8\r\nzbLJyc/VIIoH/iXOjIIoY48/zvd83DTel/QyEAWYbDW0H6VzWQ1Xtz8FO5AOMXOE\r\nZnFX9oY1AUH4S1TSUnram2cu5LFfVWXvmT5U3xOM7oA+RgI/Kg3QS0384KzJzf6G\r\nuSj3i91dJYJ7iaVXu2BxPT/aoWsJlcezky7Q7ap4M3qLFUf1ubnZMPVz8IEo6eYX\r\nsHC0Zdcx850Iy9H8jZo7EHg3Q0B2JKKYEGvD/9W/M8WIhXo9Ky3JIQ5q+L80wfiM\r\nuMWFYnCtCXPt858SS56BgAkSLxxC3lkPu32mzLBCAlXTr85LtklbQPw+uWB3afCS\r\na7RmMPBR30nWAMrvteun5z2n8rizgEZHcmE=\r\n=2e5K\r\n-----END PGP PRIVATE KEY BLOCK-----",
|
||||
"Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353",
|
||||
"Activation": null,
|
||||
"Primary": 1
|
||||
|
||||
Reference in New Issue
Block a user