peroxide: Simplify the Credential Store

Issue #13
create-reload-action
Lukasz Janyst 4 years ago
parent 612659eecb
commit 6aecc72d8b
No known key found for this signature in database
GPG Key ID: 32DE641041F17A9A
  1. 13
      pkg/bridge/bridge.go
  2. 2
      pkg/config/settings/settings.go
  3. 8
      pkg/imap/backend.go
  4. 4
      pkg/imap/mailbox_append.go
  5. 22
      pkg/keychain/helper.go
  6. 113
      pkg/keychain/keychain.go
  7. 148
      pkg/keychain/keychain_test.go
  8. 178
      pkg/keychain/static.go
  9. 11
      pkg/smtp/backend.go
  10. 6
      pkg/store/store.go
  11. 1
      pkg/store/types.go
  12. 123
      pkg/users/credentials/credentials.go
  13. 248
      pkg/users/credentials/store.go
  14. 5
      pkg/users/types.go
  15. 58
      pkg/users/user.go

@ -30,7 +30,6 @@ import (
"github.com/ljanyst/peroxide/pkg/cookies"
"github.com/ljanyst/peroxide/pkg/events"
"github.com/ljanyst/peroxide/pkg/imap"
"github.com/ljanyst/peroxide/pkg/keychain"
"github.com/ljanyst/peroxide/pkg/listener"
"github.com/ljanyst/peroxide/pkg/logging"
"github.com/ljanyst/peroxide/pkg/message"
@ -71,11 +70,6 @@ func (b *Bridge) Configure(configFile string) error {
listener := listener.New()
events.SetupEvents(listener)
kc, err := keychain.NewKeychain("bridge")
if err != nil {
return err
}
cfg := pmapi.NewConfig()
cfg.UpgradeApplicationHandler = func() {
log.Error("Application needs to be upgraded")
@ -106,10 +100,15 @@ func (b *Bridge) Configure(configFile string) error {
settingsObj.GetInt(settings.AttachmentWorkers),
)
credStore, err := credentials.NewStore(settingsObj.Get(settings.CredentialsStore))
if err != nil {
return err
}
u := users.New(
listener,
cm,
credentials.NewStore(kc),
credStore,
store.NewStoreFactory(settingsObj, listener, cache, builder),
)

@ -42,6 +42,7 @@ const (
X509Cert = "X509Cert"
CookieJar = "CookieJar"
ServerAddress = "ServerAddress"
CredentialsStore = "CredentialsStore"
)
type Settings struct {
@ -86,5 +87,6 @@ func (s *Settings) setDefaultValues() {
s.setDefault(X509Key, filepath.Join(s.settingsDir, "key.pem"))
s.setDefault(X509Cert, filepath.Join(s.settingsDir, "cert.pem"))
s.setDefault(CookieJar, filepath.Join(s.settingsDir, "cookies.json"))
s.setDefault(CredentialsStore, filepath.Join(s.settingsDir, "credentials.json"))
s.setDefault(ServerAddress, "127.0.0.1")
}

@ -121,11 +121,9 @@ func (ib *imapBackend) createUser(address, username, password string) (*imapUser
}
// Make sure you return the same user for all valid addresses when in combined mode.
if user.IsCombinedAddressMode() {
address = strings.ToLower(user.GetPrimaryAddress())
if combinedUser, ok := ib.users[address]; ok {
return combinedUser, nil
}
address = strings.ToLower(user.GetPrimaryAddress())
if combinedUser, ok := ib.users[address]; ok {
return combinedUser, nil
}
// Client can log in only using address so we can properly close all IMAP connections.

@ -114,9 +114,7 @@ func (im *imapMailbox) createMessage(imapFlags []string, date time.Time, r imap.
if internalID != "" {
if msg, err := im.storeMailbox.GetMessage(internalID); err == nil {
if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID {
return im.labelExistingMessage(msg)
}
return im.labelExistingMessage(msg)
}
}
return im.importMessage(kr, hdr, body, imapFlags, date)

@ -1,22 +0,0 @@
// Copied from github.com/docker/docker-credential-helpers to aviod dependency
// on cgo. MIT License. Copyright (c) 2016 David Calavera
package keychain
type Credentials struct {
ServerURL string
Username string
Secret string
}
type Helper interface {
// Add appends credentials to the store.
Add(*Credentials) error
// Delete removes credentials from the store.
Delete(serverURL string) error
// Get retrieves credentials from the store.
// It returns username and secret as strings.
Get(serverURL string) (string, string, error)
// List returns the stored serverURLs and their associated usernames.
List() (map[string]string, error)
}

@ -1,113 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
// Copyright (c) 2022 Lukasz Janyst <lukasz@jany.st>
//
// This file is part of Peroxide.
//
// Peroxide 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.
//
// Peroxide 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 Peroxide. If not, see <https://www.gnu.org/licenses/>.
package keychain
import (
"fmt"
"sync"
)
// Version is the keychain data version.
const Version = "k11"
// NewKeychain creates a new native keychain.
func NewKeychain(keychainName string) (*Keychain, error) {
helper, err := newStaticKeychain()
if err != nil {
return nil, err
}
return newKeychain(helper, fmt.Sprintf("protonmail/%v/users", keychainName)), nil
}
func newKeychain(helper Helper, url string) *Keychain {
return &Keychain{
helper: helper,
url: url,
locker: &sync.Mutex{},
}
}
type Keychain struct {
helper Helper
url string
locker sync.Locker
}
func (kc *Keychain) List() ([]string, error) {
kc.locker.Lock()
defer kc.locker.Unlock()
userIDsByURL, err := kc.helper.List()
if err != nil {
return nil, err
}
var userIDs []string // nolint[prealloc]
for url, userID := range userIDsByURL {
if url != kc.secretURL(userID) {
continue
}
userIDs = append(userIDs, userID)
}
return userIDs, nil
}
func (kc *Keychain) Delete(userID string) error {
kc.locker.Lock()
defer kc.locker.Unlock()
userIDsByURL, err := kc.helper.List()
if err != nil {
return err
}
if _, ok := userIDsByURL[kc.secretURL(userID)]; !ok {
return nil
}
return kc.helper.Delete(kc.secretURL(userID))
}
// Get returns the username and secret for the given userID.
func (kc *Keychain) Get(userID string) (string, string, error) {
kc.locker.Lock()
defer kc.locker.Unlock()
return kc.helper.Get(kc.secretURL(userID))
}
func (kc *Keychain) Put(userID, secret string) error {
kc.locker.Lock()
defer kc.locker.Unlock()
return kc.helper.Add(&Credentials{
ServerURL: kc.secretURL(userID),
Username: userID,
Secret: secret,
})
}
// secretURL returns the URL referring to a userID's secrets.
func (kc *Keychain) secretURL(userID string) string {
return fmt.Sprintf("%v/%v", kc.url, userID)
}

@ -1,148 +0,0 @@
// Copyright (c) 2022 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 keychain
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/require"
)
var suffix = []byte("\x00avoidFix\x00\x00\x00\x00\x00\x00\x00") //nolint[gochecknoglobals]
var testData = map[string]string{ //nolint[gochecknoglobals]
"user1": base64.StdEncoding.EncodeToString(append([]byte("data1"), suffix...)),
"user2": base64.StdEncoding.EncodeToString(append([]byte("data2"), suffix...)),
}
func TestInsertReadRemove(t *testing.T) {
keychain := newKeychain(newTestHelper(), "protonmail/bridge/users")
for id, secret := range testData {
expectedList, _ := keychain.List()
// Add expected secrets.
expectedSecret := secret
require.NoError(t, keychain.Put(id, expectedSecret))
// Check list.
actualList, err := keychain.List()
require.NoError(t, err)
expectedList = append(expectedList, id)
require.ElementsMatch(t, expectedList, actualList)
// Get and check what was inserted.
_, actualSecret, err := keychain.Get(id)
require.NoError(t, err)
require.Equal(t, expectedSecret, actualSecret)
// Put what changed.
expectedSecret = "edited_" + id
expectedSecret = base64.StdEncoding.EncodeToString(append([]byte(expectedSecret), suffix...))
nJobs := 100
nWorkers := 3
jobs := make(chan interface{}, nJobs)
done := make(chan interface{})
for i := 0; i < nWorkers; i++ {
go func() {
for {
_, more := <-jobs
if more {
require.NoError(t, keychain.Put(id, expectedSecret))
} else {
done <- nil
return
}
}
}()
}
for i := 0; i < nJobs; i++ {
jobs <- nil
}
close(jobs)
for i := 0; i < nWorkers; i++ {
<-done
}
// Check list.
actualList, err = keychain.List()
require.NoError(t, err)
require.ElementsMatch(t, expectedList, actualList)
// Get and check what changed.
_, actualSecret, err = keychain.Get(id)
require.NoError(t, err)
require.Equal(t, expectedSecret, actualSecret)
if id != "user1" {
// Remove.
err = keychain.Delete(id)
require.NoError(t, err)
// Check removed.
actualList, err = keychain.List()
require.NoError(t, err)
expectedList = expectedList[:len(expectedList)-1]
require.ElementsMatch(t, expectedList, actualList)
}
}
// Clear first.
require.NoError(t, keychain.Delete("user1"))
actualList, err := keychain.List()
require.NoError(t, err)
for id := range testData {
require.NotContains(t, actualList, id)
}
}
type testHelper map[string]*Credentials
func newTestHelper() testHelper {
return make(testHelper)
}
func (h testHelper) Add(creds *Credentials) error {
h[creds.ServerURL] = creds
return nil
}
func (h testHelper) Delete(url string) error {
delete(h, url)
return nil
}
func (h testHelper) Get(url string) (string, string, error) {
creds := h[url]
return creds.Username, creds.Secret, nil
}
func (h testHelper) List() (map[string]string, error) {
list := make(map[string]string)
for url, creds := range h {
list[url] = creds.Username
}
return list, nil
}

@ -1,178 +0,0 @@
// Copyright (c) 2022 Lukasz Janyst <lukasz@jany.st>
//
// This file is part of Peroxide.
//
// Peroxide 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.
//
// Peroxide 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 Peroxide. If not, see <https://www.gnu.org/licenses/>.
package keychain
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"golang.org/x/crypto/nacl/secretbox"
"github.com/ljanyst/peroxide/pkg/files"
)
const path = "~/.peroxide-creds"
var secretKey [32]byte
// Stores credentials in a file in user's home directory
type Static struct {
credentials map[string]Credentials
}
func (s *Static) Add(c *Credentials) error {
s.credentials[c.ServerURL] = *c
return dumpStaticStore(path, s.credentials)
}
func (s *Static) Delete(serverURL string) error {
delete(s.credentials, serverURL)
return dumpStaticStore(path, s.credentials)
}
func (s *Static) Get(serverURL string) (string, string, error) {
if cred, ok := s.credentials[serverURL]; ok {
return cred.Username, cred.Secret, nil
}
return "", "", fmt.Errorf("no usernames for %s", serverURL)
}
func (s *Static) List() (map[string]string, error) {
list := make(map[string]string)
for _, v := range s.credentials {
list[v.ServerURL] = v.Username
}
return list, nil
}
func decryptSecrets(credentials map[string]Credentials) (map[string]Credentials, error) {
creds := make(map[string]Credentials)
for k, v := range credentials {
encrypted, err := base64.StdEncoding.DecodeString(v.Secret)
if err != nil {
return nil, fmt.Errorf("Unable to decode the secret for server %s: %s", k, err)
}
// Read the nonce
var nonce [24]byte
copy(nonce[:], encrypted[:24])
// Decrypt
decrypted, ok := secretbox.Open(nil, encrypted[24:], &nonce, &secretKey)
if !ok {
return nil, fmt.Errorf("Unable to decrypt the secret for server %s", k)
}
creds[k] = Credentials{
ServerURL: v.ServerURL,
Username: v.Username,
Secret: string(decrypted),
}
}
return creds, nil
}
func encryptSecrets(credentials map[string]Credentials) (map[string]Credentials, error) {
creds := make(map[string]Credentials)
for k, v := range credentials {
// Nonce must be different for each message
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
return nil, err
}
// Encrypt the secret and append the result to the nonce
encrypted := secretbox.Seal(nonce[:], []byte(v.Secret), &nonce, &secretKey)
creds[k] = Credentials{
ServerURL: v.ServerURL,
Username: v.Username,
Secret: base64.StdEncoding.EncodeToString(encrypted),
}
}
return creds, nil
}
func loadStaticStore(path string) (map[string]Credentials, error) {
fileName := files.ExpandTilde(path)
data, err := ioutil.ReadFile(fileName)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("Unable to read the credential store file %s: %s", fileName, err)
}
creds := make(map[string]Credentials)
if os.IsNotExist(err) {
return creds, nil
}
err = json.Unmarshal(data, &creds)
if err != nil {
return nil, fmt.Errorf("Malformed credential store %s: %s", fileName, err)
}
return decryptSecrets(creds)
}
func dumpStaticStore(path string, credentials map[string]Credentials) error {
creds, err := encryptSecrets(credentials)
fileName := files.ExpandTilde(path)
if err != nil {
return fmt.Errorf("Unable to encrypt the credential store %s: %s", fileName, err)
}
data, err := json.MarshalIndent(creds, "", " ")
if err != nil {
return fmt.Errorf("Unable to serialize the credential store %s: %s", fileName, err)
}
err = ioutil.WriteFile(fileName, data, 0600)
if err != nil {
return fmt.Errorf("Unable to write the credential store file %s: %s", fileName, err)
}
return nil
}
func newStaticKeychain() (Helper, error) {
key := os.Getenv("PEROXIDE_CREDENTIALS_KEY")
if key == "" {
return nil, fmt.Errorf("PEROXIDE_CREDENTIALS_KEY envvar not set")
}
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, fmt.Errorf("Unable to decode the credentials key: %s", err)
}
if len(keyBytes) != len(secretKey) {
return nil, fmt.Errorf("Decoded credentials key is not %d bytes long", len(secretKey))
}
copy(secretKey[:], keyBytes)
creds, err := loadStaticStore(path)
if err != nil {
return nil, err
}
return &Static{creds}, nil
}

@ -74,16 +74,9 @@ func (sb *smtpBackend) Login(_ *goSMTPBackend.ConnectionState, username, passwor
return nil, err
}
// Client can log in only using address so we can properly close all SMTP connections.
addressID, err := user.GetAddressID(username)
if err != nil {
log.Error("Cannot get addressID: ", err)
return nil, err
}
// AddressID is only for split mode--it has to be empty for combined mode.
if user.IsCombinedAddressMode() {
addressID = ""
}
addressID := ""
return newSMTPUser(sb.eventListener, sb, user, username, addressID)
}

@ -267,11 +267,7 @@ func (store *Store) init(firstInit bool) (err error) {
// If it's the first time we are creating the store, use the mode set in the
// user's credentials, otherwise read it from the DB (if present).
if firstInit {
if store.user.IsCombinedAddressMode() {
err = store.setAddressMode(combinedMode)
} else {
err = store.setAddressMode(splitMode)
}
err = store.setAddressMode(combinedMode)
if err != nil {
return errors.Wrap(err, "first init setting store address mode")
}

@ -28,7 +28,6 @@ type BridgeUser interface {
ID() string
GetAddressID(address string) (string, error)
IsConnected() bool
IsCombinedAddressMode() bool
GetPrimaryAddress() string
GetStoreAddresses() []string
GetClient() pmapi.Client

@ -22,123 +22,18 @@ package credentials
import (
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/sirupsen/logrus"
)
const (
sep = "\x00"
itemLengthBridge = 9
itemLengthImportExport = 6 // Old format for Import-Export.
)
var (
log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("malformed credentials")
)
type Credentials struct {
UserID, // Do not marshal; used as a key.
Name,
Emails,
APIToken string
UserID string
Name string
Emails []string
APIToken string
MailboxPassword []byte
BridgePassword,
Version string
Timestamp int64
IsHidden, // Deprecated.
IsCombinedAddressMode bool
}
func (s *Credentials) Marshal() string {
items := []string{
s.Name, // 0
s.Emails, // 1
s.APIToken, // 2
string(s.MailboxPassword), // 3
s.BridgePassword, // 4
s.Version, // 5
"", // 6
"", // 7
"", // 8
}
items[6] = fmt.Sprint(s.Timestamp)
if s.IsHidden {
items[7] = "1"
}
if s.IsCombinedAddressMode {
items[8] = "1"
}
str := strings.Join(items, sep)
return base64.StdEncoding.EncodeToString([]byte(str))
}
func (s *Credentials) Unmarshal(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
items := strings.Split(string(b), sep)
if len(items) != itemLengthBridge && len(items) != itemLengthImportExport {
return ErrWrongFormat
}
s.Name = items[0]
s.Emails = items[1]
s.APIToken = items[2]
s.MailboxPassword = []byte(items[3])
switch len(items) {
case itemLengthBridge:
s.BridgePassword = items[4]
s.Version = items[5]
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
s.Timestamp = 0
}
if s.IsHidden = false; items[7] == "1" {
s.IsHidden = true
}
if s.IsCombinedAddressMode = false; items[8] == "1" {
s.IsCombinedAddressMode = true
}
case itemLengthImportExport:
s.Version = items[4]
if _, err = fmt.Sscan(items[5], &s.Timestamp); err != nil {
s.Timestamp = 0
}
}
return nil
}
func (s *Credentials) SetEmailList(list []string) {
s.Emails = strings.Join(list, ";")
}
func (s *Credentials) EmailList() []string {
return strings.Split(s.Emails, ";")
}
func (s *Credentials) CheckPassword(password string) error {
if subtle.ConstantTimeCompare([]byte(s.BridgePassword), []byte(password)) != 1 {
log.WithFields(logrus.Fields{
"userID": s.UserID,
}).Debug("Incorrect bridge password")
return fmt.Errorf("backend/credentials: incorrect password")
}
return nil
BridgePassword string
}
func (s *Credentials) Logout() {
@ -164,3 +59,11 @@ func (s *Credentials) SplitAPIToken() (string, string, error) {
return split[0], split[1], nil
}
func (s *Credentials) CheckPassword(password string) error {
if subtle.ConstantTimeCompare([]byte(s.BridgePassword), []byte(password)) != 1 {
return fmt.Errorf("backend/credentials: incorrect password")
}
return nil
}

@ -18,31 +18,45 @@
package credentials
import (
"encoding/json"
"errors"
"fmt"
"os"
"sort"
"sync"
"time"
"github.com/ljanyst/peroxide/pkg/keychain"
"github.com/sirupsen/logrus"
)
var storeLocker = sync.RWMutex{} //nolint[gochecknoglobals]
var (
ErrNotFound = errors.New("Credentials not found")
log = logrus.WithField("pkg", "credentials")
)
// Store is an encrypted credentials store.
type Store struct {
secrets *keychain.Keychain
lock sync.RWMutex
creds map[string]*Credentials
filePath string
}
// NewStore creates a new encrypted credentials store.
func NewStore(keychain *keychain.Keychain) *Store {
return &Store{secrets: keychain}
func NewStore(filePath string) (*Store, error) {
s := &Store{
creds: make(map[string]*Credentials),
filePath: filePath,
}
err := s.loadCredentials()
if err != nil {
return nil, err
}
return s, nil
}
func (s *Store) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
s.lock.Lock()
defer s.lock.Unlock()
log.WithFields(logrus.Fields{
"user": userID,
@ -53,220 +67,146 @@ func (s *Store) Add(userID, userName, uid, ref string, mailboxPassword []byte, e
creds := &Credentials{
UserID: userID,
Name: userName,
Emails: emails,
APIToken: uid + ":" + ref,
MailboxPassword: mailboxPassword,
IsHidden: false,
}
creds.SetEmailList(emails)
currentCredentials, err := s.get(userID)
if err == nil {
currentCredentials, ok := s.creds[userID]
if ok {
log.Info("Updating credentials of existing user")
creds.BridgePassword = currentCredentials.BridgePassword
creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode
creds.Timestamp = currentCredentials.Timestamp
} else {
log.Info("Generating credentials for new user")
creds.BridgePassword = generatePassword()
creds.IsCombinedAddressMode = true
creds.Timestamp = time.Now().Unix()
}
if err := s.saveCredentials(creds); err != nil {
return nil, err
}
return creds, nil
}
func (s *Store) SwitchAddressMode(userID string) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
s.creds[userID] = creds
credentials, err := s.get(userID)
if err != nil {
if err := s.saveCredentials(); err != nil {
return nil, err
}
credentials.IsCombinedAddressMode = !credentials.IsCombinedAddressMode
credentials.BridgePassword = generatePassword()
return credentials, s.saveCredentials(credentials)
return creds, nil
}
func (s *Store) UpdateEmails(userID string, emails []string) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
s.lock.Lock()
defer s.lock.Unlock()
credentials, err := s.get(userID)
if err != nil {
return nil, err
credentials, ok := s.creds[userID]
if !ok {
return nil, ErrNotFound
}
credentials.SetEmailList(emails)
credentials.Emails = emails
return credentials, s.saveCredentials(credentials)
return credentials, s.saveCredentials()
}
func (s *Store) UpdatePassword(userID string, password []byte) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
s.lock.Lock()
defer s.lock.Unlock()
credentials, err := s.get(userID)
if err != nil {
return nil, err
credentials, ok := s.creds[userID]
if !ok {
return nil, ErrNotFound
}
credentials.MailboxPassword = password
return credentials, s.saveCredentials(credentials)
return credentials, s.saveCredentials()
}
func (s *Store) UpdateToken(userID, uid, ref string) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
s.lock.Lock()
defer s.lock.Unlock()
credentials, err := s.get(userID)
if err != nil {
return nil, err
credentials, ok := s.creds[userID]
if !ok {
return nil, ErrNotFound
}
credentials.APIToken = uid + ":" + ref
return credentials, s.saveCredentials(credentials)
return credentials, s.saveCredentials()
}
func (s *Store) Logout(userID string) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
s.lock.Lock()
defer s.lock.Unlock()
credentials, err := s.get(userID)
if err != nil {
return nil, err
credentials, ok := s.creds[userID]
if !ok {
return nil, ErrNotFound
}
credentials.Logout()
return credentials, s.saveCredentials(credentials)
return credentials, s.saveCredentials()
}
// List returns a list of usernames that have credentials stored.
func (s *Store) List() (userIDs []string, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
func (s *Store) List() ([]string, error) {
s.lock.RLock()
defer s.lock.RUnlock()
log.Trace("Listing credentials in credentials store")
var allUserIDs []string
if allUserIDs, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials")
return
}
credentialList := []*Credentials{}
for _, userID := range allUserIDs {
creds, getErr := s.get(userID)
if getErr != nil {
log.WithField("userID", userID).WithError(getErr).Warn("Failed to get credentials")
continue
}
if creds.Timestamp == 0 {
continue
}
// Old credentials using username as a key does not work with new code.
// We need to ask user to login again to get ID from API and migrate creds.
if creds.UserID == creds.Name && creds.APIToken != "" {
creds.Logout()
_ = s.saveCredentials(creds)
}
credentialList = append(credentialList, creds)
userIDs := []string{}
for id := range s.creds {
userIDs = append(userIDs, id)
}
sort.Strings(userIDs)
sort.Slice(credentialList, func(i, j int) bool {
return credentialList[i].Timestamp < credentialList[j].Timestamp
})
for _, credentials := range credentialList {
userIDs = append(userIDs, credentials.UserID)
}
return userIDs, err
return userIDs, nil
}
func (s *Store) GetAndCheckPassword(userID, password string) (creds *Credentials, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
log.WithFields(logrus.Fields{
"userID": userID,
}).Debug("Checking bridge password")
func (s *Store) Get(userID string) (creds *Credentials, err error) {
s.lock.RLock()
defer s.lock.RUnlock()
credentials, err := s.Get(userID)
if err != nil {
return nil, err
creds, ok := s.creds[userID]
if !ok {
return nil, ErrNotFound
}
return creds, nil
}
if err := credentials.CheckPassword(password); err != nil {
log.WithFields(logrus.Fields{
"userID": userID,
"err": err,
}).Debug("Incorrect bridge password")
// Delete removes credentials from the store.
func (s *Store) Delete(userID string) (err error) {
s.lock.Lock()
defer s.lock.Unlock()
return nil, err
_, ok := s.creds[userID]
if !ok {
return ErrNotFound
}
return credentials, nil
delete(s.creds, userID)
return nil
}
func (s *Store) Get(userID string) (creds *Credentials, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
return s.get(userID)
}
func (s *Store) get(userID string) (*Credentials, error) {
log := log.WithField("user", userID)
_, secret, err := s.secrets.Get(userID)
func (s *Store) saveCredentials() error {
f, err := os.Create(s.filePath)
if err != nil {
return nil, err
return err
}
defer f.Close()
if secret == "" {
return nil, errors.New("secret is empty")
}
credentials := &Credentials{UserID: userID}
if err := credentials.Unmarshal(secret); err != nil {
log.WithError(fmt.Errorf("malformed secret: %w", err)).Error("Could not unmarshal secret")
if err := s.secrets.Delete(userID); err != nil {
log.WithError(err).Error("Failed to remove malformed secret")
}
return nil, err
}
return credentials, nil
return json.NewEncoder(f).Encode(s.creds)
}
// saveCredentials encrypts and saves password to the keychain store.
func (s *Store) saveCredentials(credentials *Credentials) error {
credentials.Version = keychain.Version
return s.secrets.Put(credentials.UserID, credentials.Marshal())
}
func (s *Store) loadCredentials() error {
f, err := os.Open(s.filePath)
if os.IsNotExist(err) {
return nil
}
// Delete removes credentials from the store.
func (s *Store) Delete(userID string) (err error) {
storeLocker.Lock()
defer storeLocker.Unlock()
if err != nil {
return err
}
defer f.Close()
return s.secrets.Delete(userID)
return json.NewDecoder(f).Decode(&s.creds)
}

@ -22,15 +22,10 @@ import (
"github.com/ljanyst/peroxide/pkg/users/credentials"
)
type Locator interface {
Clear() error
}
type CredentialsStorer interface {
List() (userIDs []string, err error)
Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error)
Get(userID string) (*credentials.Credentials, error)
SwitchAddressMode(userID string) (*credentials.Credentials, error)
UpdateEmails(userID string, emails []string) (*credentials.Credentials, error)
UpdatePassword(userID string, password []byte) (*credentials.Credentials, error)
UpdateToken(userID, uid, ref string) (*credentials.Credentials, error)

@ -295,18 +295,6 @@ func (u *User) unlockIfNecessary() error {
return nil
}
// IsCombinedAddressMode returns whether user is set in combined or split mode.
// Combined mode is the default mode and is what users typically need.
// Split mode is mostly for outlook as it cannot handle sending e-mails from an
// address other than the primary one.
func (u *User) IsCombinedAddressMode() bool {
if u.store != nil {
return u.store.IsCombinedMode()
}
return u.creds.IsCombinedAddressMode
}
// GetPrimaryAddress returns the user's original address (which is
// not necessarily the same as the primary address, because a primary address
// might be an alias and be in position one).
@ -314,7 +302,7 @@ func (u *User) GetPrimaryAddress() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.EmailList()[0]
return u.creds.Emails[0]
}
// GetStoreAddresses returns all addresses used by the store (so in combined mode,
@ -323,11 +311,7 @@ func (u *User) GetStoreAddresses() []string {
u.lock.RLock()
defer u.lock.RUnlock()
if u.IsCombinedAddressMode() {
return u.creds.EmailList()[:1]
}
return u.creds.EmailList()
return u.creds.Emails[:1]
}
// GetAddresses returns list of all addresses.
@ -335,7 +319,7 @@ func (u *User) GetAddresses() []string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.EmailList()
return u.creds.Emails
}
// GetAddressID returns the API ID of the given address.
@ -452,40 +436,6 @@ func (u *User) UpdateUser(ctx context.Context) error {
return nil
}
// SwitchAddressMode changes mode from combined to split and vice versa. The mode to switch to is determined by the
// state of the user's credentials in the credentials store. See `IsCombinedAddressMode` for more details.
func (u *User) SwitchAddressMode() error {
u.log.Trace("Switching user address mode")
u.lock.Lock()
defer u.lock.Unlock()
u.CloseAllConnections()
if u.store == nil {
return errors.New("store is not initialised")
}
newAddressModeState := !u.IsCombinedAddressMode()
if err := u.store.UseCombinedMode(newAddressModeState); err != nil {
return errors.Wrap(err, "could not switch store address mode")
}
if u.creds.IsCombinedAddressMode == newAddressModeState {
return nil
}
creds, err := u.credStorer.SwitchAddressMode(u.userID)
if err != nil {
return errors.Wrap(err, "could not switch credentials store address mode")
}
u.creds = creds
return nil
}
// Logout logs out the user from pmapi, the credentials store, the mail store, and tries to remove as much
// sensitive data as possible.
func (u *User) Logout() error {
@ -538,7 +488,7 @@ func (u *User) closeEventLoopAndCacher() {
// CloseAllConnections calls CloseConnection for all users addresses.
func (u *User) CloseAllConnections() {
for _, address := range u.creds.EmailList() {
for _, address := range u.creds.Emails {
u.CloseConnection(address)
}

Loading…
Cancel
Save