github.com/openshift-online/ocm-sdk-go@v0.1.473/authentication/securestore/main.go (about) 1 package securestore 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "errors" 7 "fmt" 8 "io" 9 "runtime" 10 "strings" 11 12 "github.com/99designs/keyring" 13 gokeyring "github.com/zalando/go-keyring" 14 ) 15 16 const ( 17 KindInternetPassword = "Internet password" // MacOS Keychain item kind 18 ItemKey = "RedHatSSO" 19 CollectionName = "login" // Common OS default collection name 20 MaxWindowsByteSize = 2500 // Windows Credential Manager has a 2500 byte limit 21 ) 22 23 var ( 24 ErrKeyringUnavailable = fmt.Errorf("keyring is valid but is not available on the current OS") 25 ErrKeyringInvalid = fmt.Errorf("keyring is invalid, expected one of: [%v]", strings.Join(AllowedBackends, ", ")) 26 AllowedBackends = []string{ 27 string(keyring.WinCredBackend), 28 string(keyring.KeychainBackend), 29 string(keyring.SecretServiceBackend), 30 string(keyring.PassBackend), 31 } 32 ) 33 34 func getKeyringConfig(backend string) keyring.Config { 35 return keyring.Config{ 36 AllowedBackends: []keyring.BackendType{keyring.BackendType(backend)}, 37 // Generic 38 ServiceName: ItemKey, 39 // MacOS 40 KeychainName: CollectionName, 41 KeychainTrustApplication: true, 42 KeychainSynchronizable: false, 43 KeychainAccessibleWhenUnlocked: false, 44 // Windows 45 WinCredPrefix: ItemKey, 46 // Secret Service 47 LibSecretCollectionName: CollectionName, 48 } 49 } 50 51 // IsBackendAvailable provides validation that the desired backend is available on the current OS. 52 func IsBackendAvailable(backend string) (isAvailable bool) { 53 if backend == "" { 54 return false 55 } 56 57 for _, avail := range AvailableBackends() { 58 if avail == backend { 59 isAvailable = true 60 break 61 } 62 } 63 64 return isAvailable 65 } 66 67 // AvailableBackends provides a slice of all available backend keys on the current OS. 68 func AvailableBackends() []string { 69 b := []string{} 70 71 if isDarwin() { 72 // Assume Keychain is always available on Darwin. It will not return from keyring.AvailableBackends() 73 b = append(b, "keychain") 74 } 75 76 // Intersection between available backends from OS and allowed backends 77 for _, avail := range keyring.AvailableBackends() { 78 for _, allowed := range AllowedBackends { 79 if string(avail) == allowed { 80 b = append(b, allowed) 81 } 82 } 83 } 84 85 return b 86 } 87 88 // UpsertConfigToKeyring will upsert the provided credentials to the desired OS secure store. 89 func UpsertConfigToKeyring(backend string, creds []byte) error { 90 if err := ValidateBackend(backend); err != nil { 91 return err 92 } 93 94 if isDarwin() && isKeychain(backend) { 95 return keychainUpsert(creds) 96 } 97 98 ring, err := keyring.Open(getKeyringConfig(backend)) 99 if err != nil { 100 return err 101 } 102 103 compressed, err := compressConfig(creds) 104 if err != nil { 105 return err 106 } 107 108 // check if available backend contains windows credential manager and exceeds the byte limit 109 if len(compressed) > MaxWindowsByteSize && 110 backend == string(keyring.WinCredBackend) { 111 return fmt.Errorf("credentials are too large for Windows Credential Manager: %d bytes (max %d)", len(compressed), MaxWindowsByteSize) 112 } 113 114 err = ring.Set(keyring.Item{ 115 Label: ItemKey, 116 Key: ItemKey, 117 Description: KindInternetPassword, 118 Data: compressed, 119 }) 120 121 return err 122 } 123 124 // RemoveConfigFromKeyring will remove the credentials from the first priority OS secure store. 125 func RemoveConfigFromKeyring(backend string) error { 126 if err := ValidateBackend(backend); err != nil { 127 return err 128 } 129 130 if isDarwin() && isKeychain(backend) { 131 return keychainRemove() 132 } 133 134 ring, err := keyring.Open(getKeyringConfig(backend)) 135 if err != nil { 136 return err 137 } 138 139 err = ring.Remove(ItemKey) 140 if err != nil { 141 if errors.Is(err, keyring.ErrKeyNotFound) { 142 // Ignore not found errors, key is already removed 143 return nil 144 } 145 } 146 147 return err 148 } 149 150 // GetConfigFromKeyring will retrieve the credentials from the first priority OS secure store. 151 func GetConfigFromKeyring(backend string) ([]byte, error) { 152 if err := ValidateBackend(backend); err != nil { 153 return nil, err 154 } 155 156 if isDarwin() && isKeychain(backend) { 157 return keychainGet() 158 } 159 160 credentials := []byte("") 161 162 ring, err := keyring.Open(getKeyringConfig(backend)) 163 if err != nil { 164 return nil, err 165 } 166 167 i, err := ring.Get(ItemKey) 168 if err != nil && !errors.Is(err, keyring.ErrKeyNotFound) { 169 return credentials, err 170 } else if errors.Is(err, keyring.ErrKeyNotFound) { 171 // Not found, continue 172 } else { 173 credentials = i.Data 174 } 175 176 if len(credentials) == 0 { 177 // No creds to decompress, return early 178 return credentials, nil 179 } 180 181 creds, err := decompressConfig(credentials) 182 if err != nil { 183 return nil, err 184 } 185 186 return creds, nil 187 188 } 189 190 // Validates that the requested backend is valid and available, returns an error if not. 191 func ValidateBackend(backend string) error { 192 if backend == "" { 193 return ErrKeyringInvalid 194 } else { 195 isAllowedBackend := false 196 for _, allowed := range AllowedBackends { 197 if allowed == backend { 198 isAllowedBackend = true 199 break 200 } 201 } 202 if !isAllowedBackend { 203 return ErrKeyringInvalid 204 } 205 } 206 207 if !IsBackendAvailable(backend) { 208 return ErrKeyringUnavailable 209 } 210 211 return nil 212 } 213 214 func keychainGet() ([]byte, error) { 215 credentials, err := gokeyring.Get(ItemKey, ItemKey) 216 if err != nil && !errors.Is(err, gokeyring.ErrNotFound) { 217 return []byte(credentials), err 218 } else if errors.Is(err, gokeyring.ErrNotFound) { 219 return []byte(""), nil 220 } 221 222 if len(credentials) == 0 { 223 // No creds to decompress, return early 224 return []byte(""), nil 225 } 226 227 creds, err := decompressConfig([]byte(credentials)) 228 if err != nil { 229 return nil, err 230 } 231 return creds, nil 232 } 233 234 func keychainUpsert(creds []byte) error { 235 compressed, err := compressConfig(creds) 236 if err != nil { 237 return err 238 } 239 240 err = gokeyring.Set(ItemKey, ItemKey, string(compressed)) 241 if err != nil { 242 return err 243 } 244 245 return nil 246 } 247 248 func keychainRemove() error { 249 err := gokeyring.Delete(ItemKey, ItemKey) 250 if err != nil { 251 if errors.Is(err, gokeyring.ErrNotFound) { 252 // Ignore not found errors, key is already removed 253 return nil 254 } 255 if strings.Contains(err.Error(), "Keychain Error. (-25244)") { 256 return fmt.Errorf("%s\nThis application may not have permission to delete from the Keychain. Please check the permissions in the Keychain and try again", err.Error()) 257 } 258 } 259 260 return err 261 } 262 263 // Compresses credential bytes to help ensure all OS secure stores can store the data. 264 // Windows Credential Manager has a 2500 byte limit. 265 func compressConfig(creds []byte) ([]byte, error) { 266 var b bytes.Buffer 267 gz := gzip.NewWriter(&b) 268 269 _, err := gz.Write(creds) 270 if err != nil { 271 return nil, err 272 } 273 274 err = gz.Close() 275 if err != nil { 276 return nil, err 277 } 278 279 return b.Bytes(), nil 280 } 281 282 // Decompresses credential bytes 283 func decompressConfig(creds []byte) ([]byte, error) { 284 reader := bytes.NewReader(creds) 285 gzreader, err := gzip.NewReader(reader) 286 if err != nil { 287 return nil, err 288 } 289 290 output, err := io.ReadAll(gzreader) 291 if err != nil { 292 return nil, err 293 } 294 295 return output, err 296 } 297 298 // isDarwin returns true if the current OS runtime is "darwin" 299 func isDarwin() bool { 300 return runtime.GOOS == "darwin" 301 } 302 303 // isKeychain returns true if the backend is "keychain" 304 func isKeychain(backend string) bool { 305 return backend == "keychain" 306 }