GODT-1522: Rebuild macOS keychain notification

This commit is contained in:
Jakub
2022-03-17 10:57:51 +01:00
parent 0ed78f1ccb
commit 478345e277
9 changed files with 91 additions and 6 deletions

View File

@ -601,6 +601,14 @@ Window {
root.notifyHasNoKeychain() root.notifyHasNoKeychain()
} }
} }
Button {
text: "Rebuild keychain"
colorScheme: root.colorScheme
onClicked: {
root.notifyRebuildKeychain()
}
}
} }
} }
@ -825,6 +833,7 @@ Window {
} }
signal changeKeychainFinished() signal changeKeychainFinished()
signal notifyHasNoKeychain() signal notifyHasNoKeychain()
signal notifyRebuildKeychain()
signal noActiveKeyForRecipient(string email) signal noActiveKeyForRecipient(string email)
signal showMainWindow() signal showMainWindow()

View File

@ -115,4 +115,9 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.noKeychain notification: root.notifications.noKeychain
} }
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.rebuildKeychain
}
} }

View File

@ -73,7 +73,8 @@ QtObject {
root.enableLocalCache, root.enableLocalCache,
root.resetBridge, root.resetBridge,
root.deleteAccount, root.deleteAccount,
root.noKeychain root.noKeychain,
root.rebuildKeychain
] ]
// Connection // Connection
@ -901,4 +902,36 @@ QtObject {
} }
] ]
} }
property Notification rebuildKeychain: Notification {
title: qsTr("Your macOS keychain might be corrupted")
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
brief: title
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
property var supportLink: "https://protonmail.com/support/knowledge-base/macos-keychain-corrupted"
Connections {
target: root.backend
onNotifyRebuildKeychain: {
console.log("notifications")
root.rebuildKeychain.active = true
}
}
action: [
Action {
text: qsTr("Open the support page")
onTriggered: {
Qt.openUrlExternally(root.rebuildKeychain.supportLink)
root.backend.quit()
}
}
]
}
} }

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
) )
func (f *FrontendQt) watchEvents() { func (f *FrontendQt) watchEvents() {
@ -64,7 +65,11 @@ func (f *FrontendQt) watchEvents() {
if strings.Contains(errorDetails, "SMTP failed") { if strings.Contains(errorDetails, "SMTP failed") {
f.qml.PortIssueSMTP() f.qml.PortIssueSMTP()
} }
case <-credentialsErrorCh: case reason := <-credentialsErrorCh:
if reason == keychain.ErrMacKeychainRebuild.Error() {
f.qml.NotifyRebuildKeychain()
continue
}
f.qml.NotifyHasNoKeychain() f.qml.NotifyHasNoKeychain()
case email := <-noActiveKeyForRecipientCh: case email := <-noActiveKeyForRecipientCh:
f.qml.NoActiveKeyForRecipient(email) f.qml.NoActiveKeyForRecipient(email)

View File

@ -142,6 +142,7 @@ type QMLBackend struct {
_ func(keychain string) `slot:"changeKeychain"` _ func(keychain string) `slot:"changeKeychain"`
_ func() `signal:"changeKeychainFinished"` _ func() `signal:"changeKeychainFinished"`
_ func() `signal:"notifyHasNoKeychain"` _ func() `signal:"notifyHasNoKeychain"`
_ func() `signal:"notifyRebuildKeychain"`
_ func(email string) `signal:noActiveKeyForRecipient` _ func(email string) `signal:noActiveKeyForRecipient`
_ func() `signal:showMainWindow` _ func() `signal:showMainWindow`

View File

@ -69,6 +69,7 @@ func newUser(
creds, err := credStorer.Get(userID) creds, err := credStorer.Get(userID)
if err != nil { if err != nil {
notifyKeychainRepair(eventListener, err)
return nil, nil, errors.Wrap(err, "failed to load user credentials") return nil, nil, errors.Wrap(err, "failed to load user credentials")
} }
@ -162,6 +163,7 @@ func (u *User) handleAuthRefresh(auth *pmapi.AuthRefresh) {
creds, err := u.credStorer.UpdateToken(u.userID, auth.UID, auth.RefreshToken) creds, err := u.credStorer.UpdateToken(u.userID, auth.UID, auth.RefreshToken)
if err != nil { if err != nil {
notifyKeychainRepair(u.listener, err)
u.log.WithError(err).Error("Failed to update refresh token in credentials store") u.log.WithError(err).Error("Failed to update refresh token in credentials store")
return return
} }
@ -408,6 +410,7 @@ func (u *User) UpdateUser(ctx context.Context) error {
creds, err := u.credStorer.UpdateEmails(u.userID, u.client.Addresses().ActiveEmails()) creds, err := u.credStorer.UpdateEmails(u.userID, u.client.Addresses().ActiveEmails())
if err != nil { if err != nil {
notifyKeychainRepair(u.listener, err)
return err return err
} }
@ -445,6 +448,7 @@ func (u *User) SwitchAddressMode() error {
creds, err := u.credStorer.SwitchAddressMode(u.userID) creds, err := u.credStorer.SwitchAddressMode(u.userID)
if err != nil { if err != nil {
notifyKeychainRepair(u.listener, err)
return errors.Wrap(err, "could not switch credentials store address mode") return errors.Wrap(err, "could not switch credentials store address mode")
} }
@ -490,9 +494,11 @@ func (u *User) Logout() error {
creds, err := u.credStorer.Logout(u.userID) creds, err := u.credStorer.Logout(u.userID)
if err != nil { if err != nil {
notifyKeychainRepair(u.listener, err)
u.log.WithError(err).Warn("Could not log user out from credentials store") u.log.WithError(err).Warn("Could not log user out from credentials store")
if err := u.credStorer.Delete(u.userID); err != nil { if err := u.credStorer.Delete(u.userID); err != nil {
notifyKeychainRepair(u.listener, err)
u.log.WithError(err).Error("Could not delete user from credentials store") u.log.WithError(err).Error("Could not delete user from credentials store")
} }
} else { } else {

View File

@ -27,6 +27,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
@ -130,6 +131,7 @@ func (u *Users) loadUsersFromCredentialsStore() error {
userIDs, err := u.credStorer.List() userIDs, err := u.credStorer.List()
if err != nil { if err != nil {
notifyKeychainRepair(u.events, err)
return err return err
} }
@ -188,6 +190,7 @@ func (u *Users) loadConnectedUser(ctx context.Context, user *User, creds *creden
// Update the user's credentials with the latest auth used to connect this user. // Update the user's credentials with the latest auth used to connect this user.
if creds, err = u.credStorer.UpdateToken(creds.UserID, auth.UID, auth.RefreshToken); err != nil { if creds, err = u.credStorer.UpdateToken(creds.UserID, auth.UID, auth.RefreshToken); err != nil {
notifyKeychainRepair(u.events, err)
return errors.Wrap(err, "could not create get user's refresh token") return errors.Wrap(err, "could not create get user's refresh token")
} }
@ -226,12 +229,14 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []by
// Update the user's credentials with the latest auth used to connect this user. // Update the user's credentials with the latest auth used to connect this user.
if _, err := u.credStorer.UpdateToken(auth.UserID, auth.UID, auth.RefreshToken); err != nil { if _, err := u.credStorer.UpdateToken(auth.UserID, auth.UID, auth.RefreshToken); err != nil {
notifyKeychainRepair(u.events, err)
return nil, errors.Wrap(err, "failed to load user credentials") return nil, errors.Wrap(err, "failed to load user credentials")
} }
// Update the password in case the user changed it. // Update the password in case the user changed it.
creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase) creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase)
if err != nil { if err != nil {
notifyKeychainRepair(u.events, err)
return nil, errors.Wrap(err, "failed to update password of user in credentials store") return nil, errors.Wrap(err, "failed to update password of user in credentials store")
} }
@ -260,6 +265,7 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi
defer u.lock.Unlock() defer u.lock.Unlock()
if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, client.Addresses().ActiveEmails()); err != nil { if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, client.Addresses().ActiveEmails()); err != nil {
notifyKeychainRepair(u.events, err)
return errors.Wrap(err, "failed to add user credentials to credentials store") return errors.Wrap(err, "failed to add user credentials to credentials store")
} }
@ -384,6 +390,7 @@ func (u *Users) DeleteUser(userID string, clearStore bool) error {
} }
if err := u.credStorer.Delete(userID); err != nil { if err := u.credStorer.Delete(userID); err != nil {
notifyKeychainRepair(u.events, err)
log.WithError(err).Error("Cannot remove user") log.WithError(err).Error("Cannot remove user")
return err return err
} }
@ -443,3 +450,9 @@ func (u *Users) crashBandicoot(username string) {
panic("Your wish is my command… I crash!") panic("Your wish is my command… I crash!")
} }
} }
func notifyKeychainRepair(l listener.Listener, err error) {
if err == keychain.ErrMacKeychainRebuild {
l.Emit(events.CredentialsErrorEvent, err.Error())
}
}

View File

@ -40,6 +40,16 @@ func init() { // nolint[noinit]
defaultHelper = MacOSKeychain defaultHelper = MacOSKeychain
} }
func parseError(original error) error {
if original == nil {
return nil
}
if strings.Contains(original.Error(), "25293") {
return ErrMacKeychainRebuild
}
return original
}
func newMacOSHelper(url string) (credentials.Helper, error) { func newMacOSHelper(url string) (credentials.Helper, error) {
return &macOSHelper{url: url}, nil return &macOSHelper{url: url}, nil
} }
@ -76,7 +86,7 @@ func (h *macOSHelper) Add(creds *credentials.Credentials) error {
query := newQuery(hostURL, userID) query := newQuery(hostURL, userID)
query.SetData([]byte(creds.Secret)) query.SetData([]byte(creds.Secret))
return keychain.AddItem(query) return parseError(keychain.AddItem(query))
} }
func (h *macOSHelper) Delete(secretURL string) error { func (h *macOSHelper) Delete(secretURL string) error {
@ -87,7 +97,7 @@ func (h *macOSHelper) Delete(secretURL string) error {
query := newQuery(hostURL, userID) query := newQuery(hostURL, userID)
return keychain.DeleteItem(query) return parseError(keychain.DeleteItem(query))
} }
func (h *macOSHelper) Get(secretURL string) (string, string, error) { func (h *macOSHelper) Get(secretURL string) (string, string, error) {
@ -102,7 +112,7 @@ func (h *macOSHelper) Get(secretURL string) (string, string, error) {
results, err := keychain.QueryItem(query) results, err := keychain.QueryItem(query)
if err != nil { if err != nil {
return "", "", err return "", "", parseError(err)
} }
if len(results) == 0 { if len(results) == 0 {
@ -121,7 +131,7 @@ func (h *macOSHelper) List() (map[string]string, error) {
userIDs, err := keychain.GetGenericPasswordAccounts(h.url) userIDs, err := keychain.GetGenericPasswordAccounts(h.url)
if err != nil { if err != nil {
return nil, err return nil, parseError(err)
} }
for _, userID := range userIDs { for _, userID := range userIDs {

View File

@ -37,6 +37,9 @@ var (
// ErrNoKeychain indicates that no suitable keychain implementation could be loaded. // ErrNoKeychain indicates that no suitable keychain implementation could be loaded.
ErrNoKeychain = errors.New("no keychain") // nolint[noglobals] ErrNoKeychain = errors.New("no keychain") // nolint[noglobals]
// ErrMacKeychainRebuild is returned on macOS with blocked or corrupted keychain.
ErrMacKeychainRebuild = errors.New("keychain error -25293")
// Helpers holds all discovered keychain helpers. It is populated in init(). // Helpers holds all discovered keychain helpers. It is populated in init().
Helpers map[string]helperConstructor // nolint[noglobals] Helpers map[string]helperConstructor // nolint[noglobals]