feat: migrate to gopenpgp v2

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,82 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package pmapi
import (
"io"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
// Client defines the interface of a PMAPI client.
type Client interface {
Auth(username, password string, info *AuthInfo) (*Auth, error)
AuthInfo(username string) (*AuthInfo, error)
AuthRefresh(token string) (*Auth, error)
Auth2FA(twoFactorCode string, auth *Auth) (*Auth2FA, error)
AuthSalt() (salt string, err error)
Logout()
DeleteAuth() error
IsConnected() bool
ClearData()
CurrentUser() (*User, error)
UpdateUser() (*User, error)
Unlock(passphrase []byte) (err error)
IsUnlocked() bool
GetAddresses() (addresses AddressList, err error)
Addresses() AddressList
ReorderAddresses(addressIDs []string) error
GetEvent(eventID string) (*Event, error)
SendMessage(string, *SendMessageReq) (sent, parent *Message, err error)
CreateDraft(m *Message, parent string, action int) (created *Message, err error)
Import([]*ImportMsgReq) ([]*ImportMsgRes, error)
CountMessages(addressID string) ([]*MessagesCount, error)
ListMessages(filter *MessagesFilter) ([]*Message, int, error)
GetMessage(apiID string) (*Message, error)
DeleteMessages(apiIDs []string) error
LabelMessages(apiIDs []string, labelID string) error
UnlabelMessages(apiIDs []string, labelID string) error
MarkMessagesRead(apiIDs []string) error
MarkMessagesUnread(apiIDs []string) error
ListLabels() ([]*Label, error)
CreateLabel(label *Label) (*Label, error)
UpdateLabel(label *Label) (*Label, error)
DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
SendSimpleMetric(category, action, label string) error
GetMailSettings() (MailSettings, error)
GetContactEmailByEmail(string, int, int) ([]ContactEmail, error)
GetContactByID(string) (Contact, error)
DecryptAndVerifyCards([]Card) ([]Card, error)
GetAttachment(id string) (att io.ReadCloser, err error)
CreateAttachment(att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error)
DeleteAttachment(attID string) (err error)
KeyRingForAddressID(string) (kr *crypto.KeyRing, err error)
GetPublicKeysForEmail(string) ([]PublicKey, bool, error)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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