common/credCache_windows.go (199 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" "os" "path" "path/filepath" "sync" "syscall" "unsafe" ) // CredCache manages credential caches. type CredCache struct { dpapiFilePath string entropy *dataBlob lock sync.Mutex } const azcopyverbose = "azcopyverbose" const defaultTokenFileName = "accessToken.json" // NewCredCache creates a cred cache. func NewCredCache(options CredCacheOptions) *CredCache { return &CredCache{ dpapiFilePath: options.DPAPIFilePath, entropy: newDataBlob([]byte(azcopyverbose)), } } // 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. func (c *CredCache) hasCachedTokenInternal() (bool, error) { if _, err := os.Stat(c.tokenFilePath()); err == nil { return true, nil } else { if os.IsNotExist(err) { return false, nil } return false, err } } // removeCachedTokenInternal deletes all the cached token. func (c *CredCache) removeCachedTokenInternal() error { tokenFilePath := c.tokenFilePath() if _, err := os.Stat(tokenFilePath); err == nil { // Cached token file existed err = os.Remove(tokenFilePath) if err != nil { // remove failed return fmt.Errorf("failed to remove cached token file with path %q, %v", tokenFilePath, err) } // remove succeeded } else { if !os.IsNotExist(err) { // Failed to stat cached token file return fmt.Errorf("failed to stat cached token file with path %q during removing, %v", tokenFilePath, err) } // token doesn't exist return errors.New("no cached token found for current user") } return nil } // loadTokenInternal restores a Token object from file cache. func (c *CredCache) loadTokenInternal() (*OAuthTokenInfo, error) { tokenFilePath := c.tokenFilePath() b, err := os.ReadFile(tokenFilePath) if err != nil { return nil, fmt.Errorf("failed to read token file %q during loading token: %v", tokenFilePath, err) } decryptedB, err := decrypt(b, c.entropy) if err != nil { return nil, fmt.Errorf("failed to decrypt bytes during loading token: %v", err) } token, err := jsonToTokenInfo(decryptedB) if err != nil { return nil, fmt.Errorf("failed to unmarshal token during loading token, %v", err) } return token, nil } // saveTokenInternal persists an oauth token on disk. // It moves the new file into place so it can safely be used to replace an existing file // that maybe accessed by multiple processes. func (c *CredCache) saveTokenInternal(token OAuthTokenInfo) error { tokenFilePath := c.tokenFilePath() dir := filepath.Dir(tokenFilePath) err := os.MkdirAll(dir, os.ModePerm) if err != nil { return fmt.Errorf("failed to create directory %q to store token in, %v", dir, err) } newFile, err := os.CreateTemp(dir, "token") if err != nil { return fmt.Errorf("failed to create the temp file to write the token, %v", err) } tempPath := newFile.Name() json, err := token.toJSON() if err != nil { return fmt.Errorf("failed to marshal token, %v", err) } b, err := encrypt(json, c.entropy) if err != nil { return fmt.Errorf("failed to encrypt token, %v", err) } if _, err = newFile.Write(b); err != nil { return fmt.Errorf("failed to encode token to file %q while saving token, %v", tempPath, err) } if err := newFile.Close(); err != nil { return fmt.Errorf("failed to close temp file %q, %v", tempPath, err) } // Atomic replace to avoid multi-writer file corruptions if err := os.Rename(tempPath, tokenFilePath); err != nil { return fmt.Errorf("failed to move temporary token to desired output location. src=%q dst=%q, %v", tempPath, tokenFilePath, err) } if err := os.Chmod(tokenFilePath, 0600); err != nil { // read/write for current user return fmt.Errorf("failed to chmod the token file %q, %v", tokenFilePath, err) } return nil } func (c *CredCache) tokenFilePath() string { if cacheFile := GetEnvironmentVariable(EEnvironmentVariable.LoginCacheName()); cacheFile != "" { return path.Join(c.dpapiFilePath, "/", cacheFile) } return path.Join(c.dpapiFilePath, "/", defaultTokenFileName) } // ====================================================================================== // DPAPI facilities // ====================================================================================== var dCrypt32 = syscall.NewLazyDLL("crypt32.dll") // lower case to tie in with Go's sysdll registration var dKernel32 = syscall.NewLazyDLL("kernel32.dll") // Refer to https://msdn.microsoft.com/en-us/library/windows/desktop/aa380261(v=vs.85).aspx for more details. var mCryptProtectData = dCrypt32.NewProc("CryptProtectData") // Refer to https://msdn.microsoft.com/en-us/library/windows/desktop/aa380882(v=vs.85).aspx for more details. var mCryptUnprotectData = dCrypt32.NewProc("CryptUnprotectData") // Refer to https://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx for more details. var mLocalFree = dKernel32.NewProc("LocalFree") // dwFlags for protection. Remote situations where presenting a user interface (UI) is not an option. // When this flag is set and a UI is specified for either the protect or unprotect operation, the operation fails and GetLastError returns the ERROR_PASSWORD_RESTRICTION code. const cryptProtectUIForbidden = 0x1 type dataBlob struct { cbData uint32 pbData *byte } func newDataBlob(d []byte) *dataBlob { if len(d) == 0 { return &dataBlob{} } return &dataBlob{ cbData: uint32(len(d)), pbData: &d[0], } } func (b dataBlob) toByteArray() []byte { d := make([]byte, b.cbData) copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:]) return d } func encrypt(data []byte, entropy *dataBlob) ([]byte, error) { if entropy == nil { return nil, errors.New("entropy is enforced in AzCopy") } var outblob dataBlob defer func() { if outblob.pbData != nil { _, _, _ = mLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData))) } }() r, _, err := mCryptProtectData.Call( uintptr(unsafe.Pointer(newDataBlob(data))), 0, uintptr(unsafe.Pointer(entropy)), 0, 0, cryptProtectUIForbidden, uintptr(unsafe.Pointer(&outblob))) if r == 0 { return nil, err } return outblob.toByteArray(), nil } func decrypt(data []byte, entropy *dataBlob) ([]byte, error) { if entropy == nil { return nil, errors.New("entropy is enforced in AzCopy") } var outblob dataBlob defer func() { if outblob.pbData != nil { _, _, _ = mLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData))) } }() r, _, err := mCryptUnprotectData.Call( uintptr(unsafe.Pointer(newDataBlob(data))), 0, uintptr(unsafe.Pointer(entropy)), 0, 0, cryptProtectUIForbidden, uintptr(unsafe.Pointer(&outblob))) if r == 0 { return nil, err } return outblob.toByteArray(), nil }