common/credCache_darwin.go (141 lines of code) (raw):

// Copyright © 2017 Microsoft <wastore@microsoft.com> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package common import ( "errors" "fmt" "github.com/keybase/go-keychain" "sync" ) // For SSH environment, user need unlock login keychain once, to enable AzCopy to Add/Update/Retrieve/Delete key. // For Mac UI or terminal environment, unlock is not mandatory. // For more details about Apple keychain support: https://developer.apple.com/documentation/security/keychain_services?language=objc // CredCache manages credential caches. type CredCache struct { serviceName string accountName string lock sync.Mutex // KeyChain settings kcSecClass keychain.SecClass kcSynchronizable keychain.Synchronizable kcAccessible keychain.Accessible // kcAccessGroup, bypass AccessGroup setting, as Azcopy is a green software don't need install currently. // for more details, refer to https://developer.apple.com/documentation/security/ksecattraccessgroup?language=objc } func NewCredCache(options CredCacheOptions) *CredCache { return &CredCache{ serviceName: options.ServiceName, accountName: options.AccountName, kcSecClass: keychain.SecClassGenericPassword, // do not synchronize this through ICloud. // for more details, refer to https://developer.apple.com/documentation/security/ksecattrsynchronizable?language=objc kcSynchronizable: keychain.SynchronizableNo, // using AccessibleAfterFirstUnlockThisDeviceOnly, so user can login once, and execute commands silently. // for more details, refer to https://developer.apple.com/documentation/security/ksecattraccessible?language=objc // and https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?language=objc kcAccessible: keychain.AccessibleAfterFirstUnlockThisDeviceOnly, } } // keychain is used for internal integration as well. var NewCredCacheInternalIntegration = NewCredCache // HasCachedToken returns if there is cached token for current executing user. func (c *CredCache) HasCachedToken() (bool, error) { c.lock.Lock() has, err := c.hasCachedTokenInternal() c.lock.Unlock() return has, err } // RemoveCachedToken deletes the cached token. func (c *CredCache) RemoveCachedToken() error { c.lock.Lock() err := c.removeCachedTokenInternal() c.lock.Unlock() return err } // SaveToken saves an oauth token. func (c *CredCache) SaveToken(token OAuthTokenInfo) error { c.lock.Lock() err := c.saveTokenInternal(token) c.lock.Unlock() return err } // LoadToken gets the cached oauth token. func (c *CredCache) LoadToken() (*OAuthTokenInfo, error) { c.lock.Lock() token, err := c.loadTokenInternal() c.lock.Unlock() return token, err } /////////////////////////////////////////////////////////////////////////////////////////////// // This internal method pattern is applied to avoid defer locks. // The reason is: // We use locks to protect shared state from being accessed from multiple threads/goroutines at the same time. // If a bug is in this method that causes a panic, // then the defer will unlock another thread/goroutine allowing it to access the shared state. // BUT, if a panic happened, the shared state is hard to be decide whether in a good or corrupted state. // So currently let the other threads/goroutines hang forever instead of letting them access the potentially corrupted shared state. // Once having bad state, more bad state gets injected into app and figuring out how it happened and how to recover from it is near impossible. // On the other hand, hanging threads is MUCH easier to detect and devs can fix the bug in code to make sure that the panic doesn't happen in the first place. /////////////////////////////////////////////////////////////////////////////////////////////// // hasCachedTokenInternal returns if there is cached token in token manager for current executing user. func (c *CredCache) hasCachedTokenInternal() (bool, error) { query := keychain.NewItem() query.SetSecClass(c.kcSecClass) query.SetService(c.serviceName) query.SetAccount(c.accountName) query.SetMatchLimit(keychain.MatchLimitOne) query.SetReturnAttributes(true) results, err := keychain.QueryItem(query) if err != nil { err = handleGenericKeyChainSecError(err) return false, err } if len(results) < 1 { return false, nil } if len(results) > 1 { return false, errors.New("invalid state, more than one cached token found") } return true, nil } // removeCachedTokenInternal delete the cached token. func (c *CredCache) removeCachedTokenInternal() error { err := keychain.DeleteGenericPasswordItem(c.serviceName, c.accountName) if err != nil { err = handleGenericKeyChainSecError(err) if err == keychain.ErrorItemNotFound { return fmt.Errorf("no cached token found for current user") } return fmt.Errorf("failed to remove cached token, %v", err) } return nil } // saveTokenInternal saves an oauth token in keychain(use user's default keychain, i.e. login keychain). func (c *CredCache) saveTokenInternal(token OAuthTokenInfo) error { b, err := token.toJSON() if err != nil { return fmt.Errorf("failed to marshal during saving token, %v", err) } item := keychain.NewItem() item.SetSecClass(c.kcSecClass) item.SetService(c.serviceName) item.SetAccount(c.accountName) item.SetData(b) item.SetSynchronizable(c.kcSynchronizable) item.SetAccessible(c.kcAccessible) err = keychain.AddItem(item) if err != nil { // Handle duplicate key error if err != keychain.ErrorDuplicateItem { err = handleGenericKeyChainSecError(err) return fmt.Errorf("failed to save token, %v", err) } // Update the key query := keychain.NewItem() query.SetSecClass(c.kcSecClass) query.SetService(c.serviceName) query.SetAccount(c.accountName) query.SetMatchLimit(keychain.MatchLimitOne) query.SetReturnData(true) err := keychain.UpdateItem(query, item) if err != nil { err = handleGenericKeyChainSecError(err) return fmt.Errorf("failed to save token, %v", err) } } return nil } // loadTokenInternal gets an oauth token from keychain. func (c *CredCache) loadTokenInternal() (*OAuthTokenInfo, error) { query := keychain.NewItem() query.SetSecClass(c.kcSecClass) query.SetService(c.serviceName) query.SetAccount(c.accountName) query.SetMatchLimit(keychain.MatchLimitOne) query.SetReturnData(true) results, err := keychain.QueryItem(query) if err != nil { err = handleGenericKeyChainSecError(err) return nil, fmt.Errorf("failed to load token, %v", err) } if len(results) != 1 { return nil, errors.New("failed to find cached token during loading token") } data := results[0].Data token, err := jsonToTokenInfo(data) if err != nil { return nil, fmt.Errorf("failed to unmarshal token during loading token, %v", err) } return token, nil } // handleGenericKeyChainSecError handles generic key chain sec errors. func handleGenericKeyChainSecError(err error) error { if err == keychain.ErrorInteractionNotAllowed { return fmt.Errorf( "if you are using SSH, please run 'security unlock-keychain' to unlock default(login) keychain from SSH first, and then re-run your azcopy command. (Error details: %v)", err) } return err }