1// Copyright 2019 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4// +build linux
5
6package libkb
7
8import (
9	cryptorand "crypto/rand"
10	"crypto/sha256"
11	"encoding/hex"
12	"fmt"
13	"io"
14	"path/filepath"
15	"strings"
16	"time"
17
18	secsrv "github.com/keybase/go-keychain/secretservice"
19	dbus "github.com/keybase/go.dbus"
20	"golang.org/x/crypto/hkdf"
21)
22
23const sessionOpenTimeout = 5 * time.Second
24
25type SecretStoreRevokableSecretService struct{}
26
27var _ SecretStoreAll = (*SecretStoreRevokableSecretService)(nil)
28
29func NewSecretStoreRevokableSecretService() *SecretStoreRevokableSecretService {
30	return &SecretStoreRevokableSecretService{}
31}
32
33func (s *SecretStoreRevokableSecretService) makeServiceAttributes(mctx MetaContext) secsrv.Attributes {
34	attrs := secsrv.Attributes{
35		"service": mctx.G().Env.GetStoredSecretServiceName(),
36	}
37	if mctx.G().Env.GetRunMode() == DevelRunMode {
38		attrs["service-base"] = mctx.G().Env.GetStoredSecretServiceBaseName()
39	}
40	return attrs
41}
42
43func (s *SecretStoreRevokableSecretService) makeAttributes(mctx MetaContext, username NormalizedUsername, instanceIdentifier []byte) secsrv.Attributes {
44	serviceAttributes := s.makeServiceAttributes(mctx)
45	serviceAttributes["username"] = string(username)
46	serviceAttributes["identifier"] = hex.EncodeToString(instanceIdentifier)
47	serviceAttributes["note"] = "https://keybase.io/docs/crypto/local-key-security"
48	serviceAttributes["info"] = "Do not delete this entry. Instead, log out or uncheck 'remember passphrase' in the app."
49	return serviceAttributes
50}
51
52func (s *SecretStoreRevokableSecretService) retrieveManyItems(mctx MetaContext, srv *secsrv.SecretService, username NormalizedUsername, instanceIdentifier []byte) ([]dbus.ObjectPath, error) {
53	if srv == nil {
54		return nil, fmt.Errorf("got nil d-bus secretservice")
55	}
56	attributes := s.makeAttributes(mctx, username, instanceIdentifier)
57	items, err := srv.SearchCollection(secsrv.DefaultCollection, attributes)
58	if err != nil {
59		return nil, err
60	}
61	return items, nil
62}
63
64func (s *SecretStoreRevokableSecretService) maybeRetrieveSingleItem(mctx MetaContext, srv *secsrv.SecretService, username NormalizedUsername, instanceIdentifier []byte) (*dbus.ObjectPath, error) {
65	items, err := s.retrieveManyItems(mctx, srv, username, instanceIdentifier)
66	if err != nil {
67		return nil, err
68	}
69
70	if len(items) < 1 {
71		return nil, nil
72	}
73	if len(items) > 1 {
74		mctx.Warning("found more than one match in keyring for query %+v", s.makeAttributes(mctx, username, instanceIdentifier))
75	}
76	item := items[0]
77	err = srv.Unlock([]dbus.ObjectPath{item})
78	if err != nil {
79		return nil, err
80	}
81	return &item, nil
82}
83
84func (s *SecretStoreRevokableSecretService) keystoreDir(mctx MetaContext, username string) string {
85	return fmt.Sprintf("ring%c%s", filepath.Separator, username)
86}
87
88func (s *SecretStoreRevokableSecretService) secretlessKeystore(mctx MetaContext, username string) SecretlessErasableKVStore {
89	return NewSecretlessFileErasableKVStore(mctx, s.keystoreDir(mctx, username))
90}
91
92func (s *SecretStoreRevokableSecretService) keystoreKey() string {
93	return "key"
94}
95
96func (s *SecretStoreRevokableSecretService) keystore(mctx MetaContext, username string, keyringSecret []byte) ErasableKVStore {
97	keygen := func(mctx MetaContext, noise NoiseBytes) (xs [32]byte, err error) {
98		// hkdf with salt=nil, info=context string, and using entropy from both
99		// the noise in the file and the secret in the keyring. Thus, when we
100		// try to erase this secret, as long as we are able to delete it from
101		// either the noise file or the keyring, we'll have succeeded in making
102		// the secret impossible to retrieve.
103		// See additional docs at https://keybase.io/docs/crypto/local-key-security.
104		h := hkdf.New(sha256.New, append(noise[:], keyringSecret...), nil, []byte(DeriveReasonLinuxRevokableKeyring))
105		_, err = io.ReadFull(h, xs[:])
106		if err != nil {
107			return [32]byte{}, err
108		}
109		return xs, nil
110	}
111	return NewFileErasableKVStore(mctx, s.keystoreDir(mctx, username), keygen)
112}
113
114const identifierKeystoreSuffix = ".user"
115
116func (s *SecretStoreRevokableSecretService) identifierKeystoreKey(username NormalizedUsername) string {
117	return string(username) + identifierKeystoreSuffix
118}
119
120func (s *SecretStoreRevokableSecretService) identifierKeystore(mctx MetaContext) ErasableKVStore {
121	plaintextKeygen := func(mctx MetaContext, noise NoiseBytes) (xs [32]byte, err error) {
122		return sha256.Sum256(noise[:]), nil
123	}
124	return NewFileErasableKVStore(mctx, "ring-identifiers", plaintextKeygen)
125}
126
127func (s *SecretStoreRevokableSecretService) RetrieveSecret(mctx MetaContext, username NormalizedUsername) (secret LKSecFullSecret, err error) {
128	defer mctx.Trace("SecretStoreRevokableSecretService.RetrieveSecret", &err)()
129
130	identifierKeystore := s.identifierKeystore(mctx)
131	var instanceIdentifier []byte
132	err = identifierKeystore.Get(mctx, s.identifierKeystoreKey(username), &instanceIdentifier)
133	if err != nil {
134		return LKSecFullSecret{}, err
135	}
136
137	srv, err := secsrv.NewService()
138	if err != nil {
139		return LKSecFullSecret{}, err
140	}
141	srv.SetSessionOpenTimeout(sessionOpenTimeout)
142	session, err := srv.OpenSession(secsrv.AuthenticationDHAES)
143	if err != nil {
144		return LKSecFullSecret{}, err
145	}
146	defer srv.CloseSession(session)
147
148	item, err := s.maybeRetrieveSingleItem(mctx, srv, username, instanceIdentifier)
149	if err != nil {
150		return LKSecFullSecret{}, err
151	}
152	if item == nil {
153		return LKSecFullSecret{}, fmt.Errorf("secret not found in secretstore")
154	}
155	keyringSecret, err := srv.GetSecret(*item, *session)
156	if err != nil {
157		return LKSecFullSecret{}, err
158	}
159
160	keystore := s.keystore(mctx, string(username), keyringSecret)
161	var secretBytes []byte
162	err = keystore.Get(mctx, s.keystoreKey(), &secretBytes)
163	if err != nil {
164		return LKSecFullSecret{}, err
165	}
166
167	return newLKSecFullSecretFromBytes(secretBytes)
168}
169
170func (s *SecretStoreRevokableSecretService) StoreSecret(mctx MetaContext, username NormalizedUsername, secret LKSecFullSecret) (err error) {
171	defer mctx.Trace("SecretStoreRevokableSecretService.StoreSecret", &err)()
172
173	// We add a public random identifier to the secret's properties in the
174	// Secret Service so if the same machine (with the same keyring) is storing
175	// passwords for the same user but in different home directories, they
176	// don't overwrite each others' keyring secrets (effectively logging the
177	// other one out after service restart).
178	instanceIdentifier := make([]byte, 32)
179	_, err = cryptorand.Read(instanceIdentifier)
180	if err != nil {
181		return err
182	}
183
184	keyringSecret := make([]byte, 32)
185	_, err = cryptorand.Read(keyringSecret)
186	if err != nil {
187		return err
188	}
189
190	srv, err := secsrv.NewService()
191	if err != nil {
192		return err
193	}
194	srv.SetSessionOpenTimeout(sessionOpenTimeout)
195	session, err := srv.OpenSession(secsrv.AuthenticationDHAES)
196	if err != nil {
197		return err
198	}
199	defer srv.CloseSession(session)
200	label := fmt.Sprintf("%s@%s", username, mctx.G().Env.GetStoredSecretServiceName())
201	properties := secsrv.NewSecretProperties(label, s.makeAttributes(mctx, username, instanceIdentifier))
202	srvSecret, err := session.NewSecret(keyringSecret)
203	if err != nil {
204		return err
205	}
206	err = srv.Unlock([]dbus.ObjectPath{secsrv.DefaultCollection})
207	if err != nil {
208		return err
209	}
210	_, err = srv.CreateItem(secsrv.DefaultCollection, properties, srvSecret, secsrv.ReplaceBehaviorReplace)
211	if err != nil {
212		return err
213	}
214
215	identifierKeystore := s.identifierKeystore(mctx)
216	err = identifierKeystore.Put(mctx, s.identifierKeystoreKey(username), instanceIdentifier)
217	if err != nil {
218		return err
219	}
220
221	keystore := s.keystore(mctx, string(username), keyringSecret)
222	err = keystore.Put(mctx, s.keystoreKey(), secret.Bytes())
223	if err != nil {
224		return err
225	}
226
227	return nil
228}
229
230func (s *SecretStoreRevokableSecretService) ClearSecret(mctx MetaContext, username NormalizedUsername) (err error) {
231	defer mctx.Trace("SecretStoreRevokableSecretService.ClearSecret", &err)()
232
233	// Delete file-based portion first. If it fails, we can still try to erase the keyring's portion.
234	secretlessKeystore := s.secretlessKeystore(mctx, string(username))
235	keystoreErr := secretlessKeystore.Erase(mctx, s.keystoreKey())
236	if keystoreErr != nil {
237		mctx.Warning("Failed to erase keystore half: %s; attempting to delete from keyring", keystoreErr)
238	}
239
240	identifierKeystore := s.identifierKeystore(mctx)
241	var instanceIdentifier []byte
242	err = identifierKeystore.Get(mctx, s.identifierKeystoreKey(username), &instanceIdentifier)
243	if err != nil {
244		// If we can't get the identifier, we can't delete it from the keyring, so bail out here.
245		return CombineErrors(keystoreErr, err)
246	}
247
248	err = identifierKeystore.Erase(mctx, s.identifierKeystoreKey(username))
249	if err != nil {
250		// We can continue even if we failed to erase the identifier, since we know it now.
251		mctx.Warning("Failed to erase identifier from identifier keystore %s; continuing to attempt to delete from keyring", err)
252	}
253
254	srv, err := secsrv.NewService()
255	if err != nil {
256		return CombineErrors(keystoreErr, err)
257	}
258	srv.SetSessionOpenTimeout(sessionOpenTimeout)
259	// Only delete the ones for the identifier we care about, so as not to erase
260	// other passwords for the same user in a different home directory on the
261	// same computer.
262	items, err := s.retrieveManyItems(mctx, srv, username, instanceIdentifier)
263	if err != nil {
264		return CombineErrors(keystoreErr, err)
265	}
266	for _, item := range items {
267		err = srv.DeleteItem(item)
268		if err != nil {
269			return CombineErrors(keystoreErr, err)
270		}
271	}
272
273	return keystoreErr
274}
275
276// Note that in the case of corruption, not all of these usernames may actually
277// be able to be logged in as due to the noise file being corrupted, the
278// keyring being uninstalled, etc.
279func (s *SecretStoreRevokableSecretService) GetUsersWithStoredSecrets(mctx MetaContext) (usernames []string, err error) {
280	defer mctx.Trace("SecretStoreRevokableSecretService.GetUsersWithStoredSecrets", &err)()
281	identifierKeystore := s.identifierKeystore(mctx)
282	suffixedUsernames, err := identifierKeystore.AllKeys(mctx, identifierKeystoreSuffix)
283	if err != nil {
284		return nil, err
285	}
286	for _, suffixedUsername := range suffixedUsernames {
287		usernames = append(usernames, strings.TrimSuffix(suffixedUsername, identifierKeystoreSuffix))
288	}
289	return usernames, nil
290}
291
292func (s *SecretStoreRevokableSecretService) GetOptions(MetaContext) *SecretStoreOptions  { return nil }
293func (s *SecretStoreRevokableSecretService) SetOptions(MetaContext, *SecretStoreOptions) {}
294