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