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