github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/secret_store_file.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 package libkb 5 6 import ( 7 "fmt" 8 "os" 9 "path/filepath" 10 "runtime" 11 ) 12 13 type secretBytes [LKSecLen]byte 14 15 func NewErrSecretForUserNotFound(username NormalizedUsername) SecretStoreError { 16 return SecretStoreError{ 17 Msg: fmt.Sprintf("No secret found for %s", username), 18 } 19 } 20 21 type SecretStoreFile struct { 22 dir string 23 notifyCreate func(NormalizedUsername) 24 } 25 26 var _ SecretStoreAll = (*SecretStoreFile)(nil) 27 28 func NewSecretStoreFile(dir string) *SecretStoreFile { 29 return &SecretStoreFile{dir: dir} 30 } 31 32 func (s *SecretStoreFile) RetrieveSecret(mctx MetaContext, username NormalizedUsername) (secret LKSecFullSecret, err error) { 33 defer mctx.Trace(fmt.Sprintf("SecretStoreFile.RetrieveSecret(%s)", username), &err)() 34 mctx.Debug("Retrieving secret V2 from file") 35 secret, err = s.retrieveSecretV2(mctx, username) 36 switch err.(type) { 37 case nil: 38 return secret, nil 39 case SecretStoreError: 40 default: 41 return LKSecFullSecret{}, err 42 } 43 44 // check for v1 45 mctx.Debug("Retrieving secret V1 from file") 46 secret, err = s.retrieveSecretV1(mctx, username) 47 if err != nil { 48 return LKSecFullSecret{}, err 49 } 50 51 // upgrade to v2 52 if err = s.StoreSecret(mctx, username, secret); err != nil { 53 return secret, err 54 } 55 if err = s.clearSecretV1(username); err != nil { 56 return secret, err 57 } 58 return secret, nil 59 } 60 61 func (s *SecretStoreFile) retrieveSecretV1(mctx MetaContext, username NormalizedUsername) (LKSecFullSecret, error) { 62 userpath := s.userpath(username) 63 mctx.Debug("SecretStoreFile.retrieveSecretV1: checking path: %s", userpath) 64 secret, err := os.ReadFile(userpath) 65 if err != nil { 66 if os.IsNotExist(err) { 67 return LKSecFullSecret{}, NewErrSecretForUserNotFound(username) 68 } 69 70 return LKSecFullSecret{}, err 71 } 72 73 return newLKSecFullSecretFromBytes(secret) 74 } 75 76 func (s *SecretStoreFile) retrieveSecretV2(mctx MetaContext, username NormalizedUsername) (LKSecFullSecret, error) { 77 userpath := s.userpathV2(username) 78 mctx.Debug("SecretStoreFile.retrieveSecretV2: checking path: %s", userpath) 79 xor, err := os.ReadFile(userpath) 80 if err != nil { 81 if os.IsNotExist(err) { 82 return LKSecFullSecret{}, NewErrSecretForUserNotFound(username) 83 } 84 85 return LKSecFullSecret{}, err 86 } 87 88 noise, err := os.ReadFile(s.noisepathV2(username)) 89 if err != nil { 90 if os.IsNotExist(err) { 91 return LKSecFullSecret{}, NewErrSecretForUserNotFound(username) 92 } 93 94 return LKSecFullSecret{}, err 95 } 96 97 var xorFixed secretBytes 98 copy(xorFixed[:], xor) 99 var noiseFixed NoiseBytes 100 copy(noiseFixed[:], noise) 101 secret, err := NoiseXOR(xorFixed, noiseFixed) 102 if err != nil { 103 return LKSecFullSecret{}, err 104 } 105 106 return newLKSecFullSecretFromBytes(secret) 107 } 108 109 func (s *SecretStoreFile) StoreSecret(mctx MetaContext, username NormalizedUsername, secret LKSecFullSecret) (err error) { 110 defer mctx.Trace(fmt.Sprintf("SecretStoreFile.StoreSecret(%s)", username), &err)() 111 noise, err := MakeNoise() 112 if err != nil { 113 return err 114 } 115 var secretFixed secretBytes 116 copy(secretFixed[:], secret.Bytes()) 117 xor, err := NoiseXOR(secretFixed, noise) 118 if err != nil { 119 return err 120 } 121 122 if err := os.MkdirAll(s.dir, PermDir); err != nil { 123 return err 124 } 125 126 fsec, err := os.CreateTemp(s.dir, username.String()) 127 if err != nil { 128 return err 129 } 130 fnoise, err := os.CreateTemp(s.dir, username.String()) 131 if err != nil { 132 return err 133 } 134 135 // remove the temp file if it still exists at the end of this function 136 defer func() { _ = ShredFile(fsec.Name()) }() 137 defer func() { _ = ShredFile(fnoise.Name()) }() 138 139 if runtime.GOOS != "windows" { 140 // os.Fchmod not supported on windows 141 if err := fsec.Chmod(PermFile); err != nil { 142 return err 143 } 144 if err := fnoise.Chmod(PermFile); err != nil { 145 return err 146 } 147 } 148 if _, err := fsec.Write(xor); err != nil { 149 return err 150 } 151 if err := fsec.Close(); err != nil { 152 return err 153 } 154 if _, err := fnoise.Write(noise[:]); err != nil { 155 return err 156 } 157 if err := fnoise.Close(); err != nil { 158 return err 159 } 160 161 finalSec := s.userpathV2(username) 162 finalNoise := s.noisepathV2(username) 163 164 exists, err := FileExists(finalSec) 165 if err != nil { 166 return err 167 } 168 169 // NOTE: Pre-existing maybe-bug: I think this step breaks atomicity. It's 170 // possible that the rename below fails, in which case we'll have already 171 // destroyed the previous value. 172 173 // On Unix we could solve this by hard linking the old file to a new tmp 174 // location, and then shredding it after the rename. On Windows, I think 175 // we'd need to somehow call the ReplaceFile Win32 function (which Go 176 // doesn't expose anywhere as far as I know, so this would require CGO) to 177 // take advantage of its lpBackupFileName param. 178 if exists { 179 // shred the existing secret 180 if err := s.clearSecretV2(username); err != nil { 181 return err 182 } 183 } 184 185 if err := os.Rename(fsec.Name(), finalSec); err != nil { 186 return err 187 } 188 if err := os.Rename(fnoise.Name(), finalNoise); err != nil { 189 return err 190 } 191 192 if err := os.Chmod(finalSec, PermFile); err != nil { 193 return err 194 } 195 if err := os.Chmod(finalNoise, PermFile); err != nil { 196 return err 197 } 198 199 // if we just created the secret store file for the 200 // first time, notify anyone interested. 201 if !exists && s.notifyCreate != nil { 202 s.notifyCreate(username) 203 } 204 205 return nil 206 } 207 208 func (s *SecretStoreFile) ClearSecret(mctx MetaContext, username NormalizedUsername) (err error) { 209 defer mctx.Trace(fmt.Sprintf("SecretStoreFile.ClearSecret(%s)", username), &err)() 210 // try both 211 212 if username.IsNil() { 213 mctx.Debug("NOOPing SecretStoreFile#ClearSecret for empty username") 214 return nil 215 } 216 217 errV1 := s.clearSecretV1(username) 218 errV2 := s.clearSecretV2(username) 219 220 err = CombineErrors(errV1, errV2) 221 if err != nil { 222 mctx.Debug("Failed to clear secret in at least one version: %s", err) 223 return err 224 } 225 return nil 226 } 227 228 func (s *SecretStoreFile) clearSecretV1(username NormalizedUsername) error { 229 if err := ShredFile(s.userpath(username)); err != nil { 230 if os.IsNotExist(err) { 231 return nil 232 } 233 return err 234 } 235 236 return nil 237 } 238 239 func (s *SecretStoreFile) clearSecretV2(username NormalizedUsername) error { 240 exists, err := FileExists(s.noisepathV2(username)) 241 if err != nil { 242 return err 243 } 244 if !exists { 245 return nil 246 } 247 248 nerr := ShredFile(s.noisepathV2(username)) 249 uerr := ShredFile(s.userpathV2(username)) 250 if nerr != nil { 251 return nerr 252 } 253 if uerr != nil { 254 return uerr 255 } 256 return nil 257 } 258 259 func (s *SecretStoreFile) GetUsersWithStoredSecrets(mctx MetaContext) (users []string, err error) { 260 defer mctx.Trace("SecretStoreFile.GetUsersWithStoredSecrets", &err)() 261 files, err := filepath.Glob(filepath.Join(s.dir, "*.ss*")) 262 if err != nil { 263 return nil, err 264 } 265 users = make([]string, 0, len(files)) 266 for _, f := range files { 267 uname := stripExt(filepath.Base(f)) 268 if !isPPSSecretStore(uname) { 269 users = append(users, uname) 270 } 271 } 272 return users, nil 273 } 274 275 func (s *SecretStoreFile) userpath(username NormalizedUsername) string { 276 return filepath.Join(s.dir, fmt.Sprintf("%s.ss", username)) 277 } 278 279 func (s *SecretStoreFile) userpathV2(username NormalizedUsername) string { 280 return filepath.Join(s.dir, fmt.Sprintf("%s.ss2", username)) 281 } 282 283 func (s *SecretStoreFile) noisepathV2(username NormalizedUsername) string { 284 return filepath.Join(s.dir, fmt.Sprintf("%s.ns2", username)) 285 } 286 287 func (s *SecretStoreFile) GetOptions(MetaContext) *SecretStoreOptions { return nil } 288 func (s *SecretStoreFile) SetOptions(MetaContext, *SecretStoreOptions) {} 289 290 func stripExt(path string) string { 291 for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- { 292 if path[i] == '.' { 293 return path[:i] 294 } 295 } 296 return "" 297 }