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  }