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