github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/secret_store_darwin.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 //go:build darwin 5 // +build darwin 6 7 package libkb 8 9 import ( 10 "encoding/base64" 11 "fmt" 12 "os" 13 "strings" 14 15 keychain "github.com/keybase/go-keychain" 16 ) 17 18 const slotSep = "/" 19 20 type keychainSlottedAccount struct { 21 name NormalizedUsername 22 slot int 23 } 24 25 func newKeychainSlottedAccount(name NormalizedUsername, slot int) keychainSlottedAccount { 26 return keychainSlottedAccount{ 27 name: name, 28 slot: slot, 29 } 30 } 31 32 // keychainSlottedAccount is used in case we can not longer delete/update an entry 33 // due to keychain corruption. For backwards compatibility the initial slot 34 // just returns the accountName field. 35 func (a keychainSlottedAccount) String() string { 36 if a.slot == 0 { 37 return a.name.String() 38 } 39 return fmt.Sprintf("%s%s%d", a.name, slotSep, a.slot) 40 } 41 42 func parseSlottedAccount(account string) string { 43 parts := strings.Split(account, slotSep) 44 if len(parts) == 0 { 45 return account 46 } 47 return parts[0] 48 } 49 50 // NOTE There have been bug reports where we are unable to store a secret in 51 // the keychain since there is an existing corrupted entry that cannot be 52 // deleted (returns a keychain.ErrorItemNotFound) but can also not be written 53 // (return keychain.ErrorDuplicateItem). As a workaround we add a slot number 54 // to the accountName field to write the secret multiple times, using a new 55 // slot if an old one is corrupted. When reading the store we return the last 56 // secret we have written down. 57 type KeychainSecretStore struct{} 58 59 var _ SecretStoreAll = KeychainSecretStore{} 60 61 func (k KeychainSecretStore) serviceName(mctx MetaContext) string { 62 return mctx.G().GetStoredSecretServiceName() 63 } 64 65 func (k KeychainSecretStore) StoreSecret(mctx MetaContext, accountName NormalizedUsername, secret LKSecFullSecret) (err error) { 66 defer mctx.Trace(fmt.Sprintf("KeychainSecretStore.StoreSecret(%s)", accountName), &err)() 67 68 // Base64 encode to make it easy to work with Keychain Access (since we are 69 // using a password item and secret is not utf-8) 70 encodedSecret := base64.StdEncoding.EncodeToString(secret.Bytes()) 71 72 // Try until we successfully write the secret in the store and we are the 73 // last entry. 74 for i := 0; i < maxKeychainItemSlots; i++ { 75 account := newKeychainSlottedAccount(accountName, i) 76 if err = k.storeSecret(mctx, account, encodedSecret); err != nil { 77 mctx.Debug("KeychainSecretStore.StoreSecret(%s): unable to store secret %v, attempt %d, retrying", accountName, err, i) 78 continue 79 } 80 81 // look ahead, if we are the last entry in the keychain can break 82 // the loop, otherwise we should keep writing the down our secret 83 // since reads will only use the last entry. 84 if i < maxKeychainItemSlots-1 { 85 nextAccount := newKeychainSlottedAccount(accountName, i+1) 86 encodedSecret, err := keychain.GetGenericPassword(k.serviceName(mctx), nextAccount.String(), "", k.accessGroup(mctx)) 87 if err == nil && encodedSecret == nil { 88 mctx.Debug("KeychainSecretStore.StoreSecret(%s): successfully stored secret on attempt %d", accountName, i) 89 break 90 } 91 } 92 } 93 return err 94 } 95 96 func (k KeychainSecretStore) GetOptions(MetaContext) *SecretStoreOptions { return nil } 97 func (k KeychainSecretStore) SetOptions(MetaContext, *SecretStoreOptions) {} 98 99 func (k KeychainSecretStore) storeSecret(mctx MetaContext, account keychainSlottedAccount, encodedSecret string) (err error) { 100 // try to clear an old secret if present 101 if err = k.clearSecret(mctx, account); err != nil { 102 mctx.Debug("KeychainSecretStore.storeSecret(%s): unable to clearSecret error: %v", account, err) 103 } 104 105 item := keychain.NewGenericPassword(k.serviceName(mctx), account.String(), 106 "", []byte(encodedSecret), k.accessGroup(mctx)) 107 item.SetSynchronizable(k.synchronizable()) 108 item.SetAccessible(k.accessible()) 109 return keychain.AddItem(item) 110 } 111 112 func (k KeychainSecretStore) mobileKeychainPermissionDeniedCheck(mctx MetaContext, err error) { 113 mctx.G().Log.Debug("mobileKeychainPermissionDeniedCheck: checking for mobile permission denied") 114 if !(isIOS && mctx.G().IsMobileAppType()) { 115 mctx.G().Log.Debug("mobileKeychainPermissionDeniedCheck: not an iOS app") 116 return 117 } 118 if err != keychain.ErrorInteractionNotAllowed { 119 mctx.G().Log.Debug("mobileKeychainPermissionDeniedCheck: wrong kind of error: %s", err) 120 return 121 } 122 mctx.G().Log.Warning("mobileKeychainPermissionDeniedCheck: keychain permission denied, aborting: %s", err) 123 os.Exit(4) 124 } 125 126 func (k KeychainSecretStore) RetrieveSecret(mctx MetaContext, accountName NormalizedUsername) (secret LKSecFullSecret, err error) { 127 defer mctx.Trace(fmt.Sprintf("KeychainSecretStore.RetrieveSecret(%s)", accountName), &err)() 128 129 // find the last valid item we have stored in the keychain 130 var previousSecret LKSecFullSecret 131 for i := 0; i < maxKeychainItemSlots; i++ { 132 account := newKeychainSlottedAccount(accountName, i) 133 secret, err = k.retrieveSecret(mctx, account) 134 if err == nil { 135 previousSecret = secret 136 mctx.Debug("successfully retrieved secret on attempt: %d, checking if there is another filled slot", i) 137 } else if _, ok := err.(SecretStoreError); ok || err == keychain.ErrorItemNotFound { 138 // We've reached the end of the keychain entries so let's return 139 // the previous secret we found. 140 secret = previousSecret 141 err = nil 142 mctx.Debug("found last slot: %d, finished read", i) 143 break 144 } else { 145 mctx.Debug("unable to retrieve secret: %v, attempt: %d", err, i) 146 } 147 } 148 if err != nil { 149 return LKSecFullSecret{}, err 150 } else if secret.IsNil() { 151 return LKSecFullSecret{}, NewErrSecretForUserNotFound(accountName) 152 } 153 return secret, nil 154 } 155 156 func (k KeychainSecretStore) retrieveSecret(mctx MetaContext, account keychainSlottedAccount) (lk LKSecFullSecret, err error) { 157 encodedSecret, err := keychain.GetGenericPassword(k.serviceName(mctx), account.String(), 158 "", k.accessGroup(mctx)) 159 if err != nil { 160 k.mobileKeychainPermissionDeniedCheck(mctx, err) 161 return LKSecFullSecret{}, err 162 } else if encodedSecret == nil { 163 return LKSecFullSecret{}, NewErrSecretForUserNotFound(account.name) 164 } 165 166 secret, err := base64.StdEncoding.DecodeString(string(encodedSecret)) 167 if err != nil { 168 return LKSecFullSecret{}, err 169 } 170 171 return newLKSecFullSecretFromBytes(secret) 172 } 173 174 func (k KeychainSecretStore) ClearSecret(mctx MetaContext, accountName NormalizedUsername) (err error) { 175 defer mctx.Trace(fmt.Sprintf("KeychainSecretStore#ClearSecret: accountName: %s", accountName), 176 &err)() 177 178 if accountName.IsNil() { 179 mctx.Debug("NOOPing KeychainSecretStore#ClearSecret for empty username") 180 return nil 181 } 182 183 // Try all slots to fully clear any secrets for this user 184 epick := FirstErrorPicker{} 185 for i := 0; i < maxKeychainItemSlots; i++ { 186 account := newKeychainSlottedAccount(accountName, i) 187 err = k.clearSecret(mctx, account) 188 switch err { 189 case nil, keychain.ErrorItemNotFound: 190 default: 191 mctx.Debug("KeychainSecretStore#ClearSecret: accountName: %s, unable to clear secret: %v", accountName, err) 192 epick.Push(err) 193 } 194 } 195 return epick.Error() 196 } 197 198 func (k KeychainSecretStore) clearSecret(mctx MetaContext, account keychainSlottedAccount) (err error) { 199 query := keychain.NewGenericPassword(k.serviceName(mctx), account.String(), 200 "", nil, k.accessGroup(mctx)) 201 // iOS keychain returns `keychain.ErrorParam` if this is set so we skip it. 202 if !isIOS { 203 query.SetMatchLimit(keychain.MatchLimitAll) 204 } 205 return keychain.DeleteItem(query) 206 } 207 208 func NewSecretStoreAll(mctx MetaContext) SecretStoreAll { 209 if mctx.G().Env.ForceSecretStoreFile() { 210 // Allow use of file secret store for development/testing on MacOS. 211 return NewSecretStoreFile(mctx.G().Env.GetDataDir()) 212 } 213 return KeychainSecretStore{} 214 } 215 216 func HasSecretStore() bool { 217 return true 218 } 219 220 func (k KeychainSecretStore) GetUsersWithStoredSecrets(mctx MetaContext) ([]string, error) { 221 accounts, err := keychain.GetAccountsForService(k.serviceName(mctx)) 222 if err != nil { 223 mctx.Debug("KeychainSecretStore.GetUsersWithStoredSecrets() error: %s", err) 224 return nil, err 225 } 226 227 seen := map[string]bool{} 228 users := []string{} 229 for _, account := range accounts { 230 username := parseSlottedAccount(account) 231 if isPPSSecretStore(username) { 232 continue 233 } 234 if _, ok := seen[username]; !ok { 235 users = append(users, username) 236 seen[username] = true 237 } 238 } 239 240 mctx.Debug("KeychainSecretStore.GetUsersWithStoredSecrets() -> %d users, %d accounts", len(users), len(accounts)) 241 return users, nil 242 }