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  }