From 082a803e473e24746e70a68a4b10194d1d048d96 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Mon, 21 Dec 2020 10:56:13 +0100 Subject: [PATCH] feat: switchable keychain --- internal/app/base/base.go | 16 +- internal/config/settings/settings.go | 2 + internal/frontend/qt-common/accounts.go | 4 +- internal/frontend/qt/accounts.go | 4 +- internal/users/credentials/store.go | 42 +---- pkg/keychain/helper_darwin.go | 145 +++++++++++++++++ pkg/keychain/helper_linux.go | 51 ++++++ ...{keychain_windows.go => helper_windows.go} | 24 ++- pkg/keychain/keychain.go | 144 +++++++++-------- pkg/keychain/keychain_darwin.go | 140 ---------------- pkg/keychain/keychain_default.go | 48 ++++++ pkg/keychain/keychain_linux.go | 73 --------- pkg/keychain/keychain_test.go | 152 ------------------ 13 files changed, 345 insertions(+), 500 deletions(-) create mode 100644 pkg/keychain/helper_darwin.go create mode 100644 pkg/keychain/helper_linux.go rename pkg/keychain/{keychain_windows.go => helper_windows.go} (72%) delete mode 100644 pkg/keychain/keychain_darwin.go create mode 100644 pkg/keychain/keychain_default.go delete mode 100644 pkg/keychain/keychain_linux.go delete mode 100644 pkg/keychain/keychain_test.go diff --git a/internal/app/base/base.go b/internal/app/base/base.go index eda68722..6f4296eb 100644 --- a/internal/app/base/base.go +++ b/internal/app/base/base.go @@ -51,6 +51,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/versioner" + "github.com/ProtonMail/proton-bridge/pkg/keychain" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/sentry" @@ -142,12 +143,12 @@ func New( // nolint[funlen] listener := listener.New() events.SetupEvents(listener) - // NOTE: If we can't load the credentials for whatever reason, - // do we really want to error out? Need to signal to frontend. - creds, err := credentials.NewStore(keychainName) + // If we can't load the keychain for whatever reason, + // we signal to frontend and supply a dummy keychain that always returns errors. + kc, err := keychain.NewKeychain(settingsObj, keychainName) if err != nil { - logrus.WithError(err).Error("Could not get credentials store") listener.Emit(events.CredentialsErrorEvent, err.Error()) + kc = keychain.NewMissingKeychain() } jar, err := cookies.NewCookieJar(settingsObj) @@ -158,7 +159,6 @@ func New( // nolint[funlen] cm := pmapi.NewClientManager(pmapi.GetAPIConfig(configName, constants.Version)) cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener)) cm.SetCookieJar(jar) - sentryReporter.SetUserAgentProvider(cm) key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) @@ -188,8 +188,6 @@ func New( // nolint[funlen] runtime.GOOS, ) - tls := tls.New(settingsPath) - exe, err := os.Executable() if err != nil { return nil, err @@ -208,12 +206,12 @@ func New( // nolint[funlen] Lock: lock, Cache: cache, Listener: listener, - Creds: creds, + Creds: credentials.NewStore(kc), CM: cm, CookieJar: jar, Updater: updater, Versioner: versioner, - TLS: tls, + TLS: tls.New(settingsPath), Autostart: autostart, Name: appName, diff --git a/internal/config/settings/settings.go b/internal/config/settings/settings.go index 864a5859..3b43c968 100644 --- a/internal/config/settings/settings.go +++ b/internal/config/settings/settings.go @@ -42,6 +42,7 @@ const ( LastVersionKey = "last_used_version" UpdateChannelKey = "update_channel" RolloutKey = "rollout" + PreferredKeychainKey = "preferred_keychain" ) type Settings struct { @@ -78,6 +79,7 @@ func (s *Settings) setDefaultValues() { s.setDefault(LastVersionKey, "") s.setDefault(UpdateChannelKey, "") s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) + s.setDefault(PreferredKeychainKey, "") s.setDefault(APIPortKey, DefaultAPIPort) s.setDefault(IMAPPortKey, DefaultIMAPPort) diff --git a/internal/frontend/qt-common/accounts.go b/internal/frontend/qt-common/accounts.go index aa66d2f2..495f1528 100644 --- a/internal/frontend/qt-common/accounts.go +++ b/internal/frontend/qt-common/accounts.go @@ -137,7 +137,7 @@ func (a *Accounts) ClearKeychain() { for _, user := range a.um.GetUsers() { if err := a.um.DeleteUser(user.ID(), false); err != nil { log.Error("While deleting user: ", err) - if err == keychain.ErrNoKeychainInstalled { // Probably not needed anymore. + if err == keychain.ErrNoKeychain { // Probably not needed anymore. a.qml.NotifyHasNoKeychain() } } @@ -249,7 +249,7 @@ func (a *Accounts) DeleteAccount(iAccount int, removePreferences bool) { userID := a.Model.get(iAccount).UserID() if err := a.um.DeleteUser(userID, removePreferences); err != nil { log.Warn("deleteUser: cannot remove user: ", err) - if err == keychain.ErrNoKeychainInstalled { + if err == keychain.ErrNoKeychain { a.qml.NotifyHasNoKeychain() return } diff --git a/internal/frontend/qt/accounts.go b/internal/frontend/qt/accounts.go index 3d9c44a4..8fc0766d 100644 --- a/internal/frontend/qt/accounts.go +++ b/internal/frontend/qt/accounts.go @@ -94,7 +94,7 @@ func (s *FrontendQt) clearKeychain() { for _, user := range s.bridge.GetUsers() { if err := s.bridge.DeleteUser(user.ID(), false); err != nil { log.Error("While deleting user: ", err) - if err == keychain.ErrNoKeychainInstalled { // Probably not needed anymore. + if err == keychain.ErrNoKeychain { // Probably not needed anymore. s.Qml.NotifyHasNoKeychain() } } @@ -203,7 +203,7 @@ func (s *FrontendQt) deleteAccount(iAccount int, removePreferences bool) { userID := s.Accounts.get(iAccount).UserID() if err := s.bridge.DeleteUser(userID, removePreferences); err != nil { log.Warn("deleteUser: cannot remove user: ", err) - if err == keychain.ErrNoKeychainInstalled { + if err == keychain.ErrNoKeychain { s.Qml.NotifyHasNoKeychain() return } diff --git a/internal/users/credentials/store.go b/internal/users/credentials/store.go index a3230a6f..41a9b2c3 100644 --- a/internal/users/credentials/store.go +++ b/internal/users/credentials/store.go @@ -31,15 +31,12 @@ var storeLocker = sync.RWMutex{} //nolint[gochecknoglobals] // Store is an encrypted credentials store. type Store struct { - secrets *keychain.Access + secrets *keychain.Keychain } // NewStore creates a new encrypted credentials store. -func NewStore(appName string) (*Store, error) { - secrets, err := keychain.NewAccess(appName) - return &Store{ - secrets: secrets, - }, err +func NewStore(keychain *keychain.Keychain) *Store { + return &Store{secrets: keychain} } func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (creds *Credentials, err error) { @@ -52,10 +49,6 @@ func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails [ "emails": emails, }).Trace("Adding new credentials") - if err = s.checkKeychain(); err != nil { - return - } - creds = &Credentials{ UserID: userID, Name: userName, @@ -164,10 +157,6 @@ func (s *Store) List() (userIDs []string, err error) { log.Trace("Listing credentials in credentials store") - if err = s.checkKeychain(); err != nil { - return - } - var allUserIDs []string if allUserIDs, err = s.secrets.List(); err != nil { log.WithError(err).Error("Could not list credentials") @@ -242,11 +231,7 @@ func (s *Store) Get(userID string) (creds *Credentials, err error) { func (s *Store) get(userID string) (creds *Credentials, err error) { log := log.WithField("user", userID) - if err = s.checkKeychain(); err != nil { - return - } - - secret, err := s.secrets.Get(userID) + _, secret, err := s.secrets.Get(userID) if err != nil { log.WithError(err).Error("Could not get credentials from native keychain") return @@ -265,32 +250,15 @@ func (s *Store) get(userID string) (creds *Credentials, err error) { // saveCredentials encrypts and saves password to the keychain store. func (s *Store) saveCredentials(credentials *Credentials) (err error) { - if err = s.checkKeychain(); err != nil { - return - } - - credentials.Version = keychain.KeychainVersion + credentials.Version = keychain.Version return s.secrets.Put(credentials.UserID, credentials.Marshal()) } -func (s *Store) checkKeychain() (err error) { - if s.secrets == nil { - err = keychain.ErrNoKeychainInstalled - log.WithError(err).Error("Store is unusable") - } - - return -} - // Delete removes credentials from the store. func (s *Store) Delete(userID string) (err error) { storeLocker.Lock() defer storeLocker.Unlock() - if err = s.checkKeychain(); err != nil { - return - } - return s.secrets.Delete(userID) } diff --git a/pkg/keychain/helper_darwin.go b/pkg/keychain/helper_darwin.go new file mode 100644 index 00000000..eed1e928 --- /dev/null +++ b/pkg/keychain/helper_darwin.go @@ -0,0 +1,145 @@ +// Copyright (c) 2021 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 ( + "errors" + "fmt" + "strings" + + "github.com/docker/docker-credential-helpers/credentials" + "github.com/keybase/go-keychain" +) + +const ( + MacOSKeychain = "macos-keychain" +) + +func init() { // nolint[noinit] + Helpers = make(map[string]helper) + + // MacOS always provides a keychain. + Helpers[MacOSKeychain] = newMacOSHelper +} + +func newMacOSHelper(url string) (credentials.Helper, error) { + return &macOSHelper{url: url}, nil +} + +type macOSHelper struct { + url string +} + +func newQuery(service, account string) keychain.Item { + query := keychain.NewItem() + query.SetSecClass(keychain.SecClassGenericPassword) + query.SetService(service) + query.SetAccount(account) + return query +} + +func (h *macOSHelper) Add(creds *credentials.Credentials) error { + secrets, err := h.List() + if err != nil { + return err + } + + // Adding a secret that already exists results in an error so we first delete the secret. + if _, ok := secrets[creds.ServerURL]; ok { + if err := h.Delete(creds.ServerURL); err != nil { + return err + } + } + + hostURL, userID, err := splitSecretURL(creds.ServerURL) + if err != nil { + return err + } + + query := newQuery(hostURL, userID) + query.SetData([]byte(creds.Secret)) + return keychain.AddItem(query) +} + +func (h *macOSHelper) Delete(secretURL string) error { + hostURL, userID, err := splitSecretURL(secretURL) + if err != nil { + return err + } + + query := newQuery(hostURL, userID) + if err := keychain.DeleteItem(query); err != nil { + return err + } + + return nil +} + +func (h *macOSHelper) Get(secretURL string) (string, string, error) { + hostURL, userID, err := splitSecretURL(secretURL) + if err != nil { + return "", "", err + } + + query := newQuery(hostURL, userID) + query.SetMatchLimit(keychain.MatchLimitOne) + query.SetReturnData(true) + + results, err := keychain.QueryItem(query) + if err != nil { + return "", "", err + } + + if len(results) != 1 { + return "", "", errors.New("ambiguous results") + } + + return userID, string(results[0].Data), nil +} + +func (h *macOSHelper) List() (map[string]string, error) { + userIDByURL := make(map[string]string) + + userIDs, err := keychain.GetGenericPasswordAccounts(h.url) + if err != nil { + return nil, err + } + + for _, userID := range userIDs { + userIDByURL[h.secretURL(userID)] = userID + } + + return userIDByURL, nil +} + +// secretURL returns the URL referring to a userID's secrets. +// NOTE: This is the same as the implementation in type `Keychain`. +// I didn't want to make this type depend on `Keychain` but I also don't like duplicate methods... +func (h *macOSHelper) secretURL(userID string) string { + return fmt.Sprintf("%v/%v", h.url, userID) +} + +func splitSecretURL(secretURL string) (string, string, error) { + split := strings.Split(secretURL, "/") + + if len(split) < 2 { + return "", "", errors.New("malformed secret") + } + + return strings.Join(split[:len(split)-1], "/"), split[len(split)-1], nil +} diff --git a/pkg/keychain/helper_linux.go b/pkg/keychain/helper_linux.go new file mode 100644 index 00000000..f099a555 --- /dev/null +++ b/pkg/keychain/helper_linux.go @@ -0,0 +1,51 @@ +// Copyright (c) 2021 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 ( + "os/exec" + + "github.com/docker/docker-credential-helpers/credentials" + "github.com/docker/docker-credential-helpers/pass" + "github.com/docker/docker-credential-helpers/secretservice" +) + +const ( + Pass = "pass-app" + GnomeKeyring = "gnome-keyring" +) + +func init() { // nolint[noinit] + Helpers = make(map[string]helper) + + if _, err := exec.LookPath("pass"); err == nil { + Helpers[Pass] = newPassHelper + } + + if _, err := exec.LookPath("gnome-keyring"); err == nil { + Helpers[GnomeKeyring] = newGnomeKeyringHelper + } +} + +func newPassHelper(string) (credentials.Helper, error) { + return &pass.Pass{}, nil +} + +func newGnomeKeyringHelper(string) (credentials.Helper, error) { + return &secretservice.Secretservice{}, nil +} diff --git a/pkg/keychain/keychain_windows.go b/pkg/keychain/helper_windows.go similarity index 72% rename from pkg/keychain/keychain_windows.go rename to pkg/keychain/helper_windows.go index 6590bc1a..b45677d6 100644 --- a/pkg/keychain/keychain_windows.go +++ b/pkg/keychain/helper_windows.go @@ -22,19 +22,15 @@ import ( "github.com/docker/docker-credential-helpers/wincred" ) -func newKeychain() (credentials.Helper, error) { - log.Debug("Creating wincred") +const WindowsCredentials = "windows-credentials" + +func init() { // nolint[noinit] + Helpers = make(map[string]helper) + + // Windows always provides a keychain. + Helpers[WindowsCredentials] = newWinCredHelper +} + +func newWinCredHelper(string) (credentials.Helper, error) { return &wincred.Wincred{}, nil } - -func (s *Access) KeychainName(userID string) string { - return s.KeychainURL + "/" + userID -} - -func (s *Access) KeychainOldName(userID string) string { - return s.KeychainOldURL + "/" + userID -} - -func (s *Access) ListKeychain() (map[string]string, error) { - return s.helper.List() -} diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 60081dfd..3eb8e0a4 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -20,112 +20,114 @@ package keychain import ( "errors" - "strings" + "fmt" + "reflect" "sync" + "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/docker/docker-credential-helpers/credentials" - "github.com/sirupsen/logrus" ) -const ( - KeychainVersion = "k11" //nolint[golint] -) +// helper constructs a keychain helper. +type helper func(string) (credentials.Helper, error) + +// Version is the keychain data version. +const Version = "k11" var ( - log = logrus.WithField("pkg", "bridgeUtils/keychain") //nolint[gochecknoglobals] + // ErrNoKeychain indicates that no suitable keychain implementation could be loaded. + ErrNoKeychain = errors.New("no keychain") // nolint[noglobals] - ErrWrongKeychainURL = errors.New("wrong keychain base URL") - ErrMacKeychainRebuild = errors.New("keychain error -25293") - ErrMacKeychainList = errors.New("function `osxkeychain.List()` is not valid function for mac keychain. Use `Access.ListKeychain()` instead") - ErrNoKeychainInstalled = errors.New("no keychain management installed on this system") - accessLocker = &sync.Mutex{} //nolint[gochecknoglobals] + // Helpers holds all discovered keychain helpers. It is populated in init(). + Helpers map[string]helper // nolint[noglobals] ) -// NewAccess creates a new native keychain. -func NewAccess(appName string) (*Access, error) { - newHelper, err := newKeychain() +// NewKeychain creates a new native keychain. +func NewKeychain(s *settings.Settings, keychainName string) (*Keychain, error) { + // There must be at least one keychain helper available. + if len(Helpers) < 1 { + return nil, ErrNoKeychain + } + + // hostURL uniquely identifies the app's keychain items within the system keychain. + hostURL := fmt.Sprintf("protonmail/%v/users", keychainName) + + // If the preferred keychain is unsupported, set a default one. + if _, ok := Helpers[s.Get(settings.PreferredKeychainKey)]; !ok { + s.Set(settings.PreferredKeychainKey, reflect.ValueOf(Helpers).MapKeys()[0].Interface().(string)) + } + + // Load the user's preferred keychain helper. + helper, err := Helpers[s.Get(settings.PreferredKeychainKey)](hostURL) if err != nil { return nil, err } - return &Access{ - helper: newHelper, - KeychainURL: "protonmail/" + appName + "/users", - KeychainOldURL: "protonmail/users", - KeychainMacURL: "ProtonMail" + strings.Title(appName) + "Service", - KeychainOldMacURL: "ProtonMailService", - }, nil + + return newKeychain(helper, hostURL), nil } -type Access struct { +func newKeychain(helper credentials.Helper, url string) *Keychain { + return &Keychain{ + helper: helper, + url: url, + locker: &sync.Mutex{}, + } +} + +type Keychain struct { helper credentials.Helper - KeychainURL, - KeychainOldURL, - KeychainMacURL, - KeychainOldMacURL string + url string + locker sync.Locker } -func (s *Access) List() (userIDs []string, err error) { - accessLocker.Lock() - defer accessLocker.Unlock() - - var userIDByURL map[string]string - userIDByURL, err = s.ListKeychain() +func (kc *Keychain) List() ([]string, error) { + kc.locker.Lock() + defer kc.locker.Unlock() + userIDsByURL, err := kc.helper.List() if err != nil { - return + return nil, err } - for itemURL, userID := range userIDByURL { - if itemURL == s.KeychainName(userID) { - userIDs = append(userIDs, userID) + var userIDs []string // nolint[prealloc] + + for url, userID := range userIDsByURL { + if url != kc.secretURL(userID) { + continue } - // Clean up old keychain name. - if itemURL == s.KeychainOldName(userID) { - _ = s.helper.Delete(s.KeychainOldName(userID)) - } + userIDs = append(userIDs, userID) } - return + return userIDs, nil } -func (s *Access) Delete(userID string) (err error) { - accessLocker.Lock() - defer accessLocker.Unlock() - return s.helper.Delete(s.KeychainName(userID)) +func (kc *Keychain) Delete(userID string) (err error) { + kc.locker.Lock() + defer kc.locker.Unlock() + + return kc.helper.Delete(kc.secretURL(userID)) } -func (s *Access) Get(userID string) (secret string, err error) { - accessLocker.Lock() - defer accessLocker.Unlock() - _, secret, err = s.helper.Get(s.KeychainName(userID)) - return +func (kc *Keychain) Get(userID string) (string, string, error) { + kc.locker.Lock() + defer kc.locker.Unlock() + + return kc.helper.Get(kc.secretURL(userID)) } -func (s *Access) Put(userID, secret string) error { - accessLocker.Lock() - defer accessLocker.Unlock() +func (kc *Keychain) Put(userID, secret string) error { + kc.locker.Lock() + defer kc.locker.Unlock() - // On macOS, adding a credential that already exists does not update it and returns an error. - // So let's remove it first. - _ = s.helper.Delete(s.KeychainName(userID)) - - cred := &credentials.Credentials{ - ServerURL: s.KeychainName(userID), + return kc.helper.Add(&credentials.Credentials{ + ServerURL: kc.secretURL(userID), Username: userID, Secret: secret, - } - - return s.helper.Add(cred) + }) } -func splitServiceAndID(keychainName string) (serviceName string, userID string, err error) { //nolint[unused] - splitted := strings.FieldsFunc(keychainName, func(c rune) bool { return c == '/' }) - n := len(splitted) - if n <= 1 { - return "", "", ErrWrongKeychainURL - } - userID = splitted[len(splitted)-1] - serviceName = strings.Join(splitted[:len(splitted)-1], "/") - return +// 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_darwin.go b/pkg/keychain/keychain_darwin.go deleted file mode 100644 index 507aee25..00000000 --- a/pkg/keychain/keychain_darwin.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2021 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 ( - "strings" - - "github.com/docker/docker-credential-helpers/credentials" - mackeychain "github.com/keybase/go-keychain" -) - -func (s *Access) KeychainName(userID string) string { - return s.KeychainMacURL + "/" + userID -} - -func (s *Access) KeychainOldName(userID string) string { - return s.KeychainOldMacURL + "/" + userID -} - -type osxkeychain struct { -} - -func newKeychain() (credentials.Helper, error) { - log.Debug("Creating osckeychain") - return &osxkeychain{}, nil -} - -func newQuery(serviceName, username string) mackeychain.Item { - query := mackeychain.NewItem() - query.SetSecClass(mackeychain.SecClassGenericPassword) - query.SetService(serviceName) - query.SetAccount(username) - return query -} - -func parseError(original error) error { - if original != nil && strings.Contains(original.Error(), "25293") { - return ErrMacKeychainRebuild - } - return original -} - -// Add appends credentials to the store (assuming old record with same ID is already deleted). -func (s *osxkeychain) Add(cred *credentials.Credentials) error { - serviceName, userID, err := splitServiceAndID(cred.ServerURL) - if err != nil { - return err - } - - query := newQuery(serviceName, userID) - query.SetData([]byte(cred.Secret)) - err = mackeychain.AddItem(query) - return parseError(err) -} - -// Delete removes credentials from the store. -func (s *osxkeychain) Delete(serverURL string) error { - serviceName, userID, err := splitServiceAndID(serverURL) - if err != nil { - return err - } - - query := newQuery(serviceName, userID) - err = mackeychain.DeleteItem(query) - if err != nil && !strings.Contains(err.Error(), "25300") { // Missing item is not error. - return err - } - return nil -} - -// Get retrieves credentials from the store. -// It returns username and secret as strings. -func (s *osxkeychain) Get(serverURL string) (userID string, secret string, err error) { - serviceName, userID, err := splitServiceAndID(serverURL) - if err != nil { - return - } - - query := newQuery(serviceName, userID) - query.SetMatchLimit(mackeychain.MatchLimitOne) - query.SetReturnData(true) - results, err := mackeychain.QueryItem(query) - if err != nil { - return "", "", parseError(err) - } - - if len(results) == 1 { - secret = string(results[0].Data) - } - - return -} - -// ListKeychain lists items in our services. -func (s *Access) ListKeychain() (userIDByURL map[string]string, err error) { - // Pick up correct service name and trim '/'. - serviceName, _, err := splitServiceAndID(s.KeychainOldName("not-id")) - if err != nil { - return - } - - userIDByURL = make(map[string]string) - - if oldIDs, err := mackeychain.GetGenericPasswordAccounts(serviceName); err == nil { - for _, userIDold := range oldIDs { - userIDByURL[s.KeychainOldName(userIDold)] = userIDold - } - } - - serviceName, _, _ = splitServiceAndID(s.KeychainName("not-id")) - if userIDs, err := mackeychain.GetGenericPasswordAccounts(serviceName); err == nil { - for _, userID := range userIDs { - userIDByURL[s.KeychainName(userID)] = userID - } - } - - return -} - -// List returns the stored serverURLs and their associated usernames. -// NOTE: This is not valid for go-keychain. Use ListKeychain instead. -func (s *osxkeychain) List() (userIDByURL map[string]string, err error) { - err = ErrMacKeychainList - return -} diff --git a/pkg/keychain/keychain_default.go b/pkg/keychain/keychain_default.go new file mode 100644 index 00000000..0272ed1e --- /dev/null +++ b/pkg/keychain/keychain_default.go @@ -0,0 +1,48 @@ +// Copyright (c) 2021 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 implements a native secure password store for each platform. +package keychain + +import ( + "github.com/docker/docker-credential-helpers/credentials" +) + +// NewMissingKeychain returns a new keychain that always returns an error. +func NewMissingKeychain() *Keychain { + return newKeychain(&missingHelper{}, "") +} + +// missingHelper is a helper which is used when no other helper is available. +// It always returns ErrNoKeychain. +type missingHelper struct{} + +func (h *missingHelper) Add(*credentials.Credentials) error { + return ErrNoKeychain +} + +func (h *missingHelper) Delete(string) error { + return ErrNoKeychain +} + +func (h *missingHelper) Get(string) (string, string, error) { + return "", "", ErrNoKeychain +} + +func (h *missingHelper) List() (map[string]string, error) { + return nil, ErrNoKeychain +} diff --git a/pkg/keychain/keychain_linux.go b/pkg/keychain/keychain_linux.go deleted file mode 100644 index 41f6a0c0..00000000 --- a/pkg/keychain/keychain_linux.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2021 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 ( - "github.com/docker/docker-credential-helpers/credentials" - "github.com/docker/docker-credential-helpers/pass" - "github.com/docker/docker-credential-helpers/secretservice" -) - -func newKeychain() (credentials.Helper, error) { - log.Debug("Creating pass") - passHelper := &pass.Pass{} - passErr := checkPassIsUsable(passHelper) - if passErr == nil { - return passHelper, nil - } - - log.Debug("Creating secretservice") - sserviceHelper := &secretservice.Secretservice{} - _, sserviceErr := sserviceHelper.List() - if sserviceErr == nil { - return sserviceHelper, nil - } - - log.Error("No keychain! Pass: ", passErr, ", secretService: ", sserviceErr) - return nil, ErrNoKeychainInstalled -} - -func checkPassIsUsable(passHelper *pass.Pass) (err error) { - creds := &credentials.Credentials{ - ServerURL: "initCheck/pass", - Username: "pass", - Secret: "pass", - } - - if err = passHelper.Add(creds); err != nil { - return - } - // Pass is not asked about unlock until you try to decrypt. - if _, _, err = passHelper.Get(creds.ServerURL); err != nil { - return - } - _ = passHelper.Delete(creds.ServerURL) // Doesn't matter if you are able to clear. - return -} - -func (s *Access) KeychainName(userID string) string { - return s.KeychainURL + "/" + userID -} - -func (s *Access) KeychainOldName(userID string) string { - return s.KeychainOldURL + "/" + userID -} - -func (s *Access) ListKeychain() (map[string]string, error) { - return s.helper.List() -} diff --git a/pkg/keychain/keychain_test.go b/pkg/keychain/keychain_test.go deleted file mode 100644 index 01a8064a..00000000 --- a/pkg/keychain/keychain_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2021 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 TestSplitServiceAndID(t *testing.T) { - acc, err := NewAccess("bridge") - require.NoError(t, err) - expectedUserID := "user" - - acc.KeychainURL = "Something/With/Several/Slashes/" - acc.KeychainMacURL = acc.KeychainURL - expectedServiceName := acc.KeychainURL - serviceName, userID, err := splitServiceAndID(acc.KeychainName(expectedUserID)) - require.NoError(t, err) - require.Equal(t, expectedUserID, userID) - require.Equal(t, expectedServiceName, serviceName+"/") - - acc.KeychainURL = "SomethingWithoutSlash" - acc.KeychainMacURL = acc.KeychainURL - expectedServiceName = acc.KeychainURL - serviceName, userID, err = splitServiceAndID(acc.KeychainName(expectedUserID)) - require.NoError(t, err) - require.Equal(t, expectedUserID, userID) - require.Equal(t, expectedServiceName, serviceName) -} - -func TestInsertReadRemove(t *testing.T) { // nolint[funlen] - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - access, err := NewAccess("bridge") - require.NoError(t, err) - access.KeychainURL = "protonmail/testchain/users" - access.KeychainMacURL = "ProtonMailTestChainService" - - // Clear before test. - for id := range testData { - // Keychain can be empty. - _ = access.Delete(id) - } - - for id, secret := range testData { - expectedList, _ := access.List() - // Add expected secrets. - expectedSecret := secret - require.NoError(t, access.Put(id, expectedSecret)) - - // Check list. - actualList, err := access.List() - require.NoError(t, err) - expectedList = append(expectedList, id) - require.ElementsMatch(t, expectedList, actualList) - - // Get and check what was inserted. - actualSecret, err := access.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, access.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 = access.List() - require.NoError(t, err) - require.ElementsMatch(t, expectedList, actualList) - - // Get and check what changed. - actualSecret, err = access.Get(id) - require.NoError(t, err) - require.Equal(t, expectedSecret, actualSecret) - - if id != "user1" { - // Remove. - err = access.Delete(id) - require.NoError(t, err) - - // Check removed. - actualList, err = access.List() - require.NoError(t, err) - expectedList = expectedList[:len(expectedList)-1] - require.ElementsMatch(t, expectedList, actualList) - } - } - - // Clear first. - err = access.Delete("user1") - require.NoError(t, err) - - actualList, err := access.List() - require.NoError(t, err) - for id := range testData { - require.NotContains(t, actualList, id) - } -}