You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
516 lines
15 KiB
516 lines
15 KiB
// 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 users provides core business logic providing API over credentials store and PM API. |
|
package users |
|
|
|
import ( |
|
"strings" |
|
"sync" |
|
|
|
"github.com/ProtonMail/proton-bridge/internal/events" |
|
"github.com/ProtonMail/proton-bridge/internal/metrics" |
|
"github.com/ProtonMail/proton-bridge/pkg/listener" |
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi" |
|
imapBackend "github.com/emersion/go-imap/backend" |
|
"github.com/hashicorp/go-multierror" |
|
"github.com/pkg/errors" |
|
logrus "github.com/sirupsen/logrus" |
|
) |
|
|
|
var ( |
|
log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals] |
|
isApplicationOutdated = false //nolint[gochecknoglobals] |
|
) |
|
|
|
// Users is a struct handling users. |
|
type Users struct { |
|
config Configer |
|
panicHandler PanicHandler |
|
events listener.Listener |
|
clientManager ClientManager |
|
credStorer CredentialsStorer |
|
storeFactory StoreMaker |
|
|
|
// users is a list of accounts that have been added to the app. |
|
// They are stored sorted in the credentials store in the order |
|
// that they were added to the app chronologically. |
|
// People are used to that and so we preserve that ordering here. |
|
users []*User |
|
|
|
// useOnlyActiveAddresses determines whether credentials keeps only active |
|
// addresses or all of them. Each usage has to be consisteng, e.g., once |
|
// user is added, it saves address list to credentials and next time loads |
|
// as is, without requesting server again. |
|
useOnlyActiveAddresses bool |
|
|
|
// idleUpdates is a channel which the imap backend listens to and which it |
|
// uses to send idle updates to the mail client (eg thunderbird). |
|
// The user stores should send idle updates on this channel. |
|
idleUpdates chan imapBackend.Update |
|
|
|
lock sync.RWMutex |
|
|
|
// stopAll can be closed to stop all goroutines from looping (watchAppOutdated, watchAPIAuths, heartbeat etc). |
|
stopAll chan struct{} |
|
} |
|
|
|
func New( |
|
config Configer, |
|
panicHandler PanicHandler, |
|
eventListener listener.Listener, |
|
clientManager ClientManager, |
|
credStorer CredentialsStorer, |
|
storeFactory StoreMaker, |
|
useOnlyActiveAddresses bool, |
|
) *Users { |
|
log.Trace("Creating new users") |
|
|
|
u := &Users{ |
|
config: config, |
|
panicHandler: panicHandler, |
|
events: eventListener, |
|
clientManager: clientManager, |
|
credStorer: credStorer, |
|
storeFactory: storeFactory, |
|
useOnlyActiveAddresses: useOnlyActiveAddresses, |
|
idleUpdates: make(chan imapBackend.Update), |
|
lock: sync.RWMutex{}, |
|
stopAll: make(chan struct{}), |
|
} |
|
|
|
go func() { |
|
defer panicHandler.HandlePanic() |
|
u.watchAppOutdated() |
|
}() |
|
|
|
go func() { |
|
defer panicHandler.HandlePanic() |
|
u.watchAPIAuths() |
|
}() |
|
|
|
if u.credStorer == nil { |
|
log.Error("No credentials store is available") |
|
} else if err := u.loadUsersFromCredentialsStore(); err != nil { |
|
log.WithError(err).Error("Could not load all users from credentials store") |
|
} |
|
|
|
return u |
|
} |
|
|
|
func (u *Users) loadUsersFromCredentialsStore() (err error) { |
|
u.lock.Lock() |
|
defer u.lock.Unlock() |
|
|
|
userIDs, err := u.credStorer.List() |
|
if err != nil { |
|
return |
|
} |
|
|
|
for _, userID := range userIDs { |
|
l := log.WithField("user", userID) |
|
|
|
user, newUserErr := newUser(u.panicHandler, userID, u.events, u.credStorer, u.clientManager, u.storeFactory) |
|
if newUserErr != nil { |
|
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping") |
|
continue |
|
} |
|
|
|
u.users = append(u.users, user) |
|
|
|
if initUserErr := user.init(u.idleUpdates); initUserErr != nil { |
|
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user") |
|
} |
|
} |
|
|
|
return err |
|
} |
|
|
|
func (u *Users) watchAppOutdated() { |
|
ch := make(chan string) |
|
|
|
u.events.Add(events.UpgradeApplicationEvent, ch) |
|
|
|
for { |
|
select { |
|
case <-ch: |
|
isApplicationOutdated = true |
|
u.closeAllConnections() |
|
|
|
case <-u.stopAll: |
|
return |
|
} |
|
} |
|
} |
|
|
|
// watchAPIAuths receives auths from the client manager and sends them to the appropriate user. |
|
func (u *Users) watchAPIAuths() { |
|
for { |
|
select { |
|
case auth := <-u.clientManager.GetAuthUpdateChannel(): |
|
log.Debug("Users received auth from ClientManager") |
|
|
|
user, ok := u.hasUser(auth.UserID) |
|
if !ok { |
|
log.WithField("userID", auth.UserID).Info("User not available for auth update") |
|
continue |
|
} |
|
|
|
if auth.Auth != nil { |
|
user.updateAuthToken(auth.Auth) |
|
} else if err := user.logout(); err != nil { |
|
log.WithError(err). |
|
WithField("userID", auth.UserID). |
|
Error("User logout failed while watching API auths") |
|
} |
|
|
|
case <-u.stopAll: |
|
return |
|
} |
|
} |
|
} |
|
|
|
func (u *Users) closeAllConnections() { |
|
for _, user := range u.users { |
|
user.closeAllConnections() |
|
} |
|
} |
|
|
|
// Login authenticates a user by username/password, returning an authorised client and an auth object. |
|
// The authorisation scope may not yet be full if the user has 2FA enabled. |
|
func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) { |
|
u.crashBandicoot(username) |
|
|
|
// We need to use anonymous client because we don't yet have userID and so can't save auth tokens yet. |
|
authClient = u.clientManager.GetAnonymousClient() |
|
|
|
authInfo, err := authClient.AuthInfo(username) |
|
if err != nil { |
|
log.WithField("username", username).WithError(err).Error("Could not get auth info for user") |
|
return |
|
} |
|
|
|
if auth, err = authClient.Auth(username, password, authInfo); err != nil { |
|
log.WithField("username", username).WithError(err).Error("Could not get auth for user") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// FinishLogin finishes the login procedure and adds the user into the credentials store. |
|
func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassphrase string) (user *User, err error) { //nolint[funlen] |
|
defer func() { |
|
if err == pmapi.ErrUpgradeApplication { |
|
u.events.Emit(events.UpgradeApplicationEvent, "") |
|
} |
|
if err != nil { |
|
log.WithError(err).Debug("Login not finished; removing auth session") |
|
if delAuthErr := authClient.DeleteAuth(); delAuthErr != nil { |
|
log.WithError(delAuthErr).Error("Failed to clear login session after unlock") |
|
} |
|
} |
|
// The anonymous client will be removed from list and authentication will not be deleted. |
|
authClient.Logout() |
|
}() |
|
|
|
apiUser, hashedPassphrase, err := getAPIUser(authClient, mbPassphrase) |
|
if err != nil { |
|
log.WithError(err).Error("Failed to get API user") |
|
return |
|
} |
|
|
|
log.Info("Got API user") |
|
|
|
var ok bool |
|
if user, ok = u.hasUser(apiUser.ID); ok { |
|
if err = u.connectExistingUser(user, auth, hashedPassphrase); err != nil { |
|
log.WithError(err).Error("Failed to connect existing user") |
|
return |
|
} |
|
} else { |
|
if err = u.addNewUser(apiUser, auth, hashedPassphrase); err != nil { |
|
log.WithError(err).Error("Failed to add new user") |
|
return |
|
} |
|
} |
|
|
|
// Old credentials use username as key (user ID) which needs to be removed |
|
// once user logs in again with proper ID fetched from API. |
|
if _, ok := u.hasUser(apiUser.Name); ok { |
|
if err := u.DeleteUser(apiUser.Name, true); err != nil { |
|
log.WithError(err).Error("Failed to delete old user") |
|
} |
|
} |
|
|
|
u.events.Emit(events.UserRefreshEvent, apiUser.ID) |
|
|
|
return u.GetUser(apiUser.ID) |
|
} |
|
|
|
// connectExistingUser connects an existing user. |
|
func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassphrase string) (err error) { |
|
if user.IsConnected() { |
|
return errors.New("user is already connected") |
|
} |
|
|
|
log.Info("Connecting existing user") |
|
|
|
// Update the user's password in the cred store in case they changed it. |
|
if err = u.credStorer.UpdatePassword(user.ID(), hashedPassphrase); err != nil { |
|
return errors.Wrap(err, "failed to update password of user in credentials store") |
|
} |
|
|
|
client := u.clientManager.GetClient(user.ID()) |
|
|
|
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil { |
|
return errors.Wrap(err, "failed to refresh auth token of new client") |
|
} |
|
|
|
if err = u.credStorer.UpdateToken(user.ID(), auth.GenToken()); err != nil { |
|
return errors.Wrap(err, "failed to update token of user in credentials store") |
|
} |
|
|
|
if err = user.init(u.idleUpdates); err != nil { |
|
return errors.Wrap(err, "failed to initialise user") |
|
} |
|
|
|
return |
|
} |
|
|
|
// addNewUser adds a new user. |
|
func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassphrase string) (err error) { |
|
u.lock.Lock() |
|
defer u.lock.Unlock() |
|
|
|
client := u.clientManager.GetClient(apiUser.ID) |
|
|
|
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil { |
|
return errors.Wrap(err, "failed to refresh token in new client") |
|
} |
|
|
|
if apiUser, err = client.CurrentUser(); err != nil { |
|
return errors.Wrap(err, "failed to update API user") |
|
} |
|
|
|
var emails []string //nolint[prealloc] |
|
if u.useOnlyActiveAddresses { |
|
emails = client.Addresses().ActiveEmails() |
|
} else { |
|
emails = client.Addresses().AllEmails() |
|
} |
|
|
|
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassphrase, emails); err != nil { |
|
return errors.Wrap(err, "failed to add user to credentials store") |
|
} |
|
|
|
user, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.clientManager, u.storeFactory) |
|
if err != nil { |
|
return errors.Wrap(err, "failed to create user") |
|
} |
|
|
|
// The user needs to be part of the users list in order for it to receive an auth during initialisation. |
|
u.users = append(u.users, user) |
|
|
|
if err = user.init(u.idleUpdates); err != nil { |
|
u.users = u.users[:len(u.users)-1] |
|
return errors.Wrap(err, "failed to initialise user") |
|
} |
|
|
|
u.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel)) |
|
|
|
return err |
|
} |
|
|
|
func getAPIUser(client pmapi.Client, mbPassphrase string) (user *pmapi.User, hashedPassphrase string, err error) { |
|
salt, err := client.AuthSalt() |
|
if err != nil { |
|
log.WithError(err).Error("Could not get salt") |
|
return |
|
} |
|
|
|
hashedPassphrase, err = pmapi.HashMailboxPassword(mbPassphrase, salt) |
|
if err != nil { |
|
log.WithError(err).Error("Could not hash mailbox password") |
|
return |
|
} |
|
|
|
// We unlock the user's PGP key here to detect if the user's mailbox password is wrong. |
|
if err = client.Unlock([]byte(hashedPassphrase)); err != nil { |
|
log.WithError(err).Error("Wrong mailbox password") |
|
return |
|
} |
|
|
|
if user, err = client.CurrentUser(); err != nil { |
|
log.WithError(err).Error("Could not load user data") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// GetUsers returns all added users into keychain (even logged out users). |
|
func (u *Users) GetUsers() []*User { |
|
u.lock.RLock() |
|
defer u.lock.RUnlock() |
|
|
|
return u.users |
|
} |
|
|
|
// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address. |
|
func (u *Users) GetUser(query string) (*User, error) { |
|
u.crashBandicoot(query) |
|
|
|
u.lock.RLock() |
|
defer u.lock.RUnlock() |
|
|
|
for _, user := range u.users { |
|
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) { |
|
return user, nil |
|
} |
|
for _, address := range user.GetAddresses() { |
|
if strings.EqualFold(address, query) { |
|
return user, nil |
|
} |
|
} |
|
} |
|
|
|
return nil, errors.New("user " + query + " not found") |
|
} |
|
|
|
// ClearData closes all connections (to release db files and so on) and clears all data. |
|
func (u *Users) ClearData() error { |
|
var result *multierror.Error |
|
for _, user := range u.users { |
|
if err := user.Logout(); err != nil { |
|
result = multierror.Append(result, err) |
|
} |
|
if err := user.closeStore(); err != nil { |
|
result = multierror.Append(result, err) |
|
} |
|
} |
|
if err := u.config.ClearData(); err != nil { |
|
result = multierror.Append(result, err) |
|
} |
|
return result.ErrorOrNil() |
|
} |
|
|
|
// DeleteUser deletes user completely; it logs user out from the API, stops any |
|
// active connection, deletes from credentials store and removes from the Bridge struct. |
|
func (u *Users) DeleteUser(userID string, clearStore bool) error { |
|
u.lock.Lock() |
|
defer u.lock.Unlock() |
|
|
|
log := log.WithField("user", userID) |
|
|
|
for idx, user := range u.users { |
|
if user.ID() == userID { |
|
if err := user.Logout(); err != nil { |
|
log.WithError(err).Error("Cannot logout user") |
|
// We can try to continue to remove the user. |
|
// Token will still be valid, but will expire eventually. |
|
} |
|
|
|
if err := user.closeStore(); err != nil { |
|
log.WithError(err).Error("Failed to close user store") |
|
} |
|
if clearStore { |
|
// Clear cache after closing connections (done in logout). |
|
if err := user.clearStore(); err != nil { |
|
log.WithError(err).Error("Failed to clear user") |
|
} |
|
} |
|
|
|
if err := u.credStorer.Delete(userID); err != nil { |
|
log.WithError(err).Error("Cannot remove user") |
|
return err |
|
} |
|
u.users = append(u.users[:idx], u.users[idx+1:]...) |
|
return nil |
|
} |
|
} |
|
|
|
return errors.New("user " + userID + " not found") |
|
} |
|
|
|
// SendMetric sends a metric. We don't want to return any errors, only log them. |
|
func (u *Users) SendMetric(m metrics.Metric) { |
|
c := u.clientManager.GetAnonymousClient() |
|
defer c.Logout() |
|
|
|
cat, act, lab := m.Get() |
|
if err := c.SendSimpleMetric(string(cat), string(act), string(lab)); err != nil { |
|
log.Error("Sending metric failed: ", err) |
|
} |
|
|
|
log.WithFields(logrus.Fields{ |
|
"cat": cat, |
|
"act": act, |
|
"lab": lab, |
|
}).Debug("Metric successfully sent") |
|
} |
|
|
|
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent. |
|
func (u *Users) GetIMAPUpdatesChannel() chan imapBackend.Update { |
|
if u.idleUpdates == nil { |
|
log.Warn("IMAP updates channel is nil") |
|
} |
|
|
|
return u.idleUpdates |
|
} |
|
|
|
// AllowProxy instructs the app to use DoH to access an API proxy if necessary. |
|
// It also needs to work before the app is initialised (because we may need to use the proxy at startup). |
|
func (u *Users) AllowProxy() { |
|
u.clientManager.AllowProxy() |
|
} |
|
|
|
// DisallowProxy instructs the app to not use DoH to access an API proxy if necessary. |
|
// It also needs to work before the app is initialised (because we may need to use the proxy at startup). |
|
func (u *Users) DisallowProxy() { |
|
u.clientManager.DisallowProxy() |
|
} |
|
|
|
// CheckConnection returns whether there is an internet connection. |
|
// This should use the connection manager when it is eventually implemented. |
|
func (u *Users) CheckConnection() error { |
|
return u.clientManager.CheckConnection() |
|
} |
|
|
|
// StopWatchers stops all goroutines. |
|
func (u *Users) StopWatchers() { |
|
close(u.stopAll) |
|
} |
|
|
|
// hasUser returns whether the struct currently has a user with ID `id`. |
|
func (u *Users) hasUser(id string) (user *User, ok bool) { |
|
for _, u := range u.users { |
|
if u.ID() == id { |
|
user, ok = u, true |
|
return |
|
} |
|
} |
|
|
|
return |
|
} |
|
|
|
// "Easter egg" for testing purposes. |
|
func (u *Users) crashBandicoot(username string) { |
|
if username == "crash@bandicoot" { |
|
panic("Your wish is my command… I crash!") |
|
} |
|
}
|
|
|