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