github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/erasable_kv_store.go (about) 1 package libkb 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "fmt" 7 8 "net/url" 9 "os" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strings" 14 "sync" 15 16 "golang.org/x/crypto/nacl/secretbox" 17 ) 18 19 type UnboxError struct { 20 inner error 21 info string 22 } 23 24 func NewUnboxError(inner error) UnboxError { 25 return UnboxError{inner: inner} 26 } 27 28 func NewUnboxErrorWithInfo(inner error, info string) UnboxError { 29 return UnboxError{inner: inner, info: info} 30 } 31 32 func (e UnboxError) Error() string { 33 return fmt.Sprintf("ErasableKVStore UnboxError (info=%s): %v", e.info, e.inner.Error()) 34 } 35 36 func (e UnboxError) Info() string { 37 return e.info 38 } 39 40 type boxedData struct { 41 V int 42 N [NaclDHNonceSize]byte 43 E []byte 44 H []byte // sha256 has of noise used for encryption 45 } 46 47 // *** 48 // If we change this, make sure to update the key derivation reason for all 49 // callers of ErasableKVStore! 50 // *** 51 const cryptoVersion = 1 52 const noiseSuffix = ".ns" 53 const storageSubDir = "eraseablekvstore" 54 55 func getStorageDir(mctx MetaContext, subDir string) string { 56 base := mctx.G().Env.GetDataDir() 57 // check for iOS 58 if runtime.GOOS == "ios" { 59 base = mctx.G().Env.GetConfigDir() 60 } 61 return filepath.Join(base, storageSubDir, subDir) 62 } 63 64 type ErasableKVStore interface { 65 Put(mctx MetaContext, key string, val interface{}) error 66 Get(mctx MetaContext, key string, val interface{}) error 67 Erase(mctx MetaContext, key string) error 68 AllKeys(mctx MetaContext, keySuffix string) ([]string, error) 69 } 70 71 type SecretlessErasableKVStore interface { 72 Erase(mctx MetaContext, key string) error 73 AllKeys(mctx MetaContext, keySuffix string) ([]string, error) 74 } 75 76 type keygenFunc = func(mctx MetaContext, noise NoiseBytes) ([32]byte, error) 77 78 // File based erasable kv store. Thread safe. 79 // We encrypt all data stored here with a mix of the device long term key and a 80 // large random noise file. We use the random noise file to make it more 81 // difficult to recover the encrypted data once the noise file is wiped from 82 // the filesystem as is done for the secret_store_file. 83 type FileErasableKVStore struct { 84 sync.Mutex 85 storageDir string 86 keygen keygenFunc 87 } 88 89 var _ ErasableKVStore = (*FileErasableKVStore)(nil) 90 91 func NewFileErasableKVStore(mctx MetaContext, subDir string, keygen keygenFunc) *FileErasableKVStore { 92 return &FileErasableKVStore{ 93 storageDir: getStorageDir(mctx, subDir), 94 keygen: keygen, 95 } 96 } 97 98 func NewSecretlessFileErasableKVStore(mctx MetaContext, subDir string) SecretlessErasableKVStore { 99 return &FileErasableKVStore{ 100 storageDir: getStorageDir(mctx, subDir), 101 } 102 } 103 104 func (s *FileErasableKVStore) filepath(key string) string { 105 return filepath.Join(s.storageDir, url.QueryEscape(key)) 106 } 107 108 func (s *FileErasableKVStore) noiseKey(key string) string { 109 return fmt.Sprintf("%s%s", url.QueryEscape(key), noiseSuffix) 110 } 111 112 func (s *FileErasableKVStore) unbox(mctx MetaContext, data []byte, noiseBytes NoiseBytes, val interface{}) (err error) { 113 defer mctx.Trace("FileErasableKVStore#unbox", &err)() 114 // Decode encrypted box 115 var boxed boxedData 116 if err := MPackDecode(data, &boxed); err != nil { 117 return NewUnboxError(err) 118 } 119 if boxed.V > cryptoVersion { 120 return NewUnboxError(fmt.Errorf("unexpected crypto version: %d current: %d", boxed.V, cryptoVersion)) 121 } 122 enckey, err := s.keygen(mctx, noiseBytes) 123 if err != nil { 124 return err 125 } 126 pt, ok := secretbox.Open(nil, boxed.E, &boxed.N, &enckey) 127 if !ok { 128 // If this fails, let's see if our noise file was corrupted somehow. 129 originalNoise := boxed.H 130 currentNoise := s.noiseHash(noiseBytes[:]) 131 eq := bytes.Equal(originalNoise, currentNoise) 132 err = fmt.Errorf("secretbox.Open failure. Stored noise hash: %x, current noise hash: %x, equal: %v", originalNoise, currentNoise, eq) 133 if eq { 134 return NewUnboxErrorWithInfo(err, "noise hashes match") 135 } 136 return NewUnboxErrorWithInfo(err, "noise hashes do not match") 137 } 138 139 err = MPackDecode(pt, val) 140 if err != nil { 141 return NewUnboxError(err) 142 } 143 return nil 144 } 145 146 func (s *FileErasableKVStore) box(mctx MetaContext, val interface{}, 147 noiseBytes NoiseBytes) (data []byte, err error) { 148 defer mctx.Trace("FileErasableKVStore#box", &err)() 149 data, err = MPackEncode(val) 150 if err != nil { 151 return data, err 152 } 153 154 enckey, err := s.keygen(mctx, noiseBytes) 155 if err != nil { 156 return data, err 157 } 158 var nonce []byte 159 nonce, err = RandBytes(NaclDHNonceSize) 160 if err != nil { 161 return data, err 162 } 163 var fnonce [NaclDHNonceSize]byte 164 copy(fnonce[:], nonce) 165 sealed := secretbox.Seal(nil, data, &fnonce, &enckey) 166 boxed := boxedData{ 167 V: cryptoVersion, 168 E: sealed, 169 N: fnonce, 170 H: s.noiseHash(noiseBytes[:]), 171 } 172 173 // Encode encrypted box 174 return MPackEncode(boxed) 175 } 176 177 func (s *FileErasableKVStore) Put(mctx MetaContext, key string, val interface{}) (err error) { 178 defer mctx.Trace(fmt.Sprintf("FileErasableKVStore#Put: %v", key), &err)() 179 s.Lock() 180 defer s.Unlock() 181 182 noiseBytes, err := MakeNoise() 183 data, err := s.box(mctx, val, noiseBytes) 184 if err != nil { 185 return err 186 } 187 if err = s.write(mctx, key, data); err != nil { 188 return err 189 } 190 noiseKey := s.noiseKey(key) 191 return s.write(mctx, noiseKey, noiseBytes[:]) 192 } 193 194 func (s *FileErasableKVStore) write(mctx MetaContext, key string, data []byte) (err error) { 195 defer mctx.Trace(fmt.Sprintf("FileErasableKVStore#write: %v", key), &err)() 196 filepath := s.filepath(key) 197 if err := MakeParentDirs(mctx.G().Log, filepath); err != nil { 198 return err 199 } 200 201 tmp, err := os.CreateTemp(s.storageDir, key) 202 if err != nil { 203 return err 204 } 205 // remove the temp file if it still exists at the end of this function 206 defer func() { _ = ShredFile(tmp.Name()) }() 207 208 if err := SetDisableBackup(mctx, tmp.Name()); err != nil { 209 return err 210 } 211 212 if runtime.GOOS != "windows" { 213 // os.Fchmod not supported on windows 214 if err := tmp.Chmod(PermFile); err != nil { 215 return err 216 } 217 } 218 if _, err := tmp.Write(data); err != nil { 219 return err 220 } 221 if err := tmp.Close(); err != nil { 222 return err 223 } 224 225 // NOTE: Pre-existing maybe-bug: I think this step breaks atomicity. It's 226 // possible that the rename below fails, in which case we'll have already 227 // destroyed the previous value. 228 229 // On Unix we could solve this by hard linking the old file to a new tmp 230 // location, and then shredding it after the rename. On Windows, I think 231 // we'd need to somehow call the ReplaceFile Win32 function (which Go 232 // doesn't expose anywhere as far as I know, so this would require CGO) to 233 // take advantage of its lpBackupFileName param. 234 err = s.erase(mctx, key) 235 if err != nil { 236 return err 237 } 238 239 if err := os.Rename(tmp.Name(), filepath); err != nil { 240 return err 241 } 242 if err := SetDisableBackup(mctx, filepath); err != nil { 243 return err 244 } 245 246 if runtime.GOOS != "windows" { 247 // os.Fchmod not supported on windows 248 if err := os.Chmod(filepath, PermFile); err != nil { 249 return err 250 } 251 } 252 return nil 253 } 254 255 func (s *FileErasableKVStore) Get(mctx MetaContext, key string, val interface{}) (err error) { 256 defer mctx.Trace(fmt.Sprintf("FileErasableKVStore#Get: %v", key), &err)() 257 s.Lock() 258 defer s.Unlock() 259 return s.get(mctx, key, val) 260 } 261 262 func (s *FileErasableKVStore) get(mctx MetaContext, key string, val interface{}) (err error) { 263 noiseKey := s.noiseKey(key) 264 noise, err := s.read(mctx, noiseKey) 265 if err != nil { 266 if IsTooManyFilesError(err) { 267 return err 268 } 269 return NewUnboxError(err) 270 } 271 var noiseBytes NoiseBytes 272 copy(noiseBytes[:], noise) 273 274 data, err := s.read(mctx, key) 275 if err != nil { 276 if IsTooManyFilesError(err) { 277 return err 278 } 279 return NewUnboxError(err) 280 } 281 282 return s.unbox(mctx, data, noiseBytes, val) 283 } 284 285 func (s *FileErasableKVStore) read(mctx MetaContext, key string) (data []byte, err error) { 286 defer mctx.Trace(fmt.Sprintf("FileErasableKVStore#read: %v", key), &err)() 287 filepath := s.filepath(key) 288 return os.ReadFile(filepath) 289 } 290 291 func (s *FileErasableKVStore) noiseHash(noiseBytes []byte) []byte { 292 h := sha256.New() 293 _, _ = h.Write(noiseBytes) 294 return h.Sum(nil) 295 } 296 297 func (s *FileErasableKVStore) Erase(mctx MetaContext, key string) (err error) { 298 defer mctx.Trace(fmt.Sprintf("FileErasableKVStore#Erase: %s", key), &err)() 299 s.Lock() 300 defer s.Unlock() 301 noiseKey := s.noiseKey(key) 302 epick := FirstErrorPicker{} 303 epick.Push(s.erase(mctx, noiseKey)) 304 epick.Push(s.erase(mctx, key)) 305 err = epick.Error() 306 return err 307 } 308 309 func (s *FileErasableKVStore) erase(mctx MetaContext, key string) (err error) { 310 defer mctx.Trace(fmt.Sprintf("FileErasableKVStore#erase: %s", key), &err)() 311 filepath := s.filepath(key) 312 if exists, err := FileExists(filepath); err != nil { 313 return err 314 } else if exists { 315 if err := ShredFile(filepath); err != nil { 316 return err 317 } 318 } 319 return nil 320 } 321 322 func (s *FileErasableKVStore) AllKeys(mctx MetaContext, keySuffix string) (keys []string, err error) { 323 defer mctx.Trace("FileErasableKVStore#AllKeys", &err)() 324 s.Lock() 325 defer s.Unlock() 326 if err := os.MkdirAll(s.storageDir, PermDir); err != nil { 327 return nil, err 328 } 329 files, err := os.ReadDir(s.storageDir) 330 if err != nil { 331 return nil, err 332 } 333 tmpFileExp, err := regexp.Compile(fmt.Sprintf(`(%s|%s)[\d]+`, keySuffix, noiseSuffix)) 334 if err != nil { 335 return nil, err 336 } 337 for _, file := range files { 338 filename := filepath.Base(file.Name()) 339 if tmpFileExp.MatchString(filename) { 340 if err := ShredFile(s.filepath(filename)); err != nil { 341 mctx.Debug("FileErasableKVStore#AllKeys: unable to remove temp file: %v, %v", file.Name(), err) 342 } 343 continue 344 } 345 if strings.HasSuffix(filename, noiseSuffix) { 346 continue 347 } 348 key, err := url.QueryUnescape(filename) 349 if err != nil { 350 return nil, err 351 } 352 keys = append(keys, key) 353 } 354 return keys, nil 355 }