diff --git a/pkg/bridge/bridge.go b/pkg/bridge/bridge.go index 7206a7d..f329345 100644 --- a/pkg/bridge/bridge.go +++ b/pkg/bridge/bridge.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), ) diff --git a/pkg/config/settings/settings.go b/pkg/config/settings/settings.go index 8575b17..32f673d 100644 --- a/pkg/config/settings/settings.go +++ b/pkg/config/settings/settings.go @@ -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") } diff --git a/pkg/imap/backend.go b/pkg/imap/backend.go index c4d9716..9689ea9 100644 --- a/pkg/imap/backend.go +++ b/pkg/imap/backend.go @@ -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. diff --git a/pkg/imap/mailbox_append.go b/pkg/imap/mailbox_append.go index 85cf84e..27abd7e 100644 --- a/pkg/imap/mailbox_append.go +++ b/pkg/imap/mailbox_append.go @@ -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) diff --git a/pkg/keychain/helper.go b/pkg/keychain/helper.go deleted file mode 100644 index 44e358e..0000000 --- a/pkg/keychain/helper.go +++ /dev/null @@ -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) -} diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go deleted file mode 100644 index 0495079..0000000 --- a/pkg/keychain/keychain.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// Copyright (c) 2022 Lukasz Janyst -// -// 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 . - -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) -} diff --git a/pkg/keychain/keychain_test.go b/pkg/keychain/keychain_test.go deleted file mode 100644 index 612d8d1..0000000 --- a/pkg/keychain/keychain_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/pkg/keychain/static.go b/pkg/keychain/static.go deleted file mode 100644 index a022015..0000000 --- a/pkg/keychain/static.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2022 Lukasz Janyst -// -// 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 . - -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 -} diff --git a/pkg/smtp/backend.go b/pkg/smtp/backend.go index e3b38c2..629989a 100644 --- a/pkg/smtp/backend.go +++ b/pkg/smtp/backend.go @@ -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) } diff --git a/pkg/store/store.go b/pkg/store/store.go index 66f564d..8269ffd 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -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") } diff --git a/pkg/store/types.go b/pkg/store/types.go index 234ca56..cd0eb88 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -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 diff --git a/pkg/users/credentials/credentials.go b/pkg/users/credentials/credentials.go index 162d8b9..b499485 100644 --- a/pkg/users/credentials/credentials.go +++ b/pkg/users/credentials/credentials.go @@ -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 + +} diff --git a/pkg/users/credentials/store.go b/pkg/users/credentials/store.go index d9d206f..062a3b3 100644 --- a/pkg/users/credentials/store.go +++ b/pkg/users/credentials/store.go @@ -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) } diff --git a/pkg/users/types.go b/pkg/users/types.go index 1681719..b935acc 100644 --- a/pkg/users/types.go +++ b/pkg/users/types.go @@ -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) diff --git a/pkg/users/user.go b/pkg/users/user.go index 0074905..986e3be 100644 --- a/pkg/users/user.go +++ b/pkg/users/user.go @@ -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) }