github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kvstore/cache.go (about)

     1  package kvstore
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"sync"
     8  
     9  	"github.com/keybase/client/go/libkb"
    10  	"github.com/keybase/client/go/protocol/keybase1"
    11  )
    12  
    13  const DeletedOrNonExistent = ""
    14  
    15  var _ libkb.KVRevisionCacher = (*KVRevisionCache)(nil)
    16  
    17  type kvCacheEntry struct {
    18  	Revision   int
    19  	EntryHash  string
    20  	TeamKeyGen keybase1.PerTeamKeyGeneration
    21  }
    22  
    23  type kvCacheData map[keybase1.TeamID]map[string] /*namespace*/ map[string] /*entry*/ kvCacheEntry
    24  
    25  type KVRevisionCache struct {
    26  	sync.Mutex
    27  	data kvCacheData
    28  }
    29  
    30  func NewKVRevisionCache(g *libkb.GlobalContext) *KVRevisionCache {
    31  	kvr := &KVRevisionCache{
    32  		data: make(kvCacheData),
    33  	}
    34  	g.AddLogoutHook(kvr, "kvstore revision cache")
    35  	g.AddDbNukeHook(kvr, "kvstore revision cache")
    36  	return kvr
    37  }
    38  
    39  // Hash is a sha256 on the input string. If the string is empty, then Hash will also be an
    40  // empty string for tracking deleted entries in perpetuity.
    41  func (k *KVRevisionCache) hash(ciphertext *string) string {
    42  	if ciphertext == nil || len(*ciphertext) == 0 || *ciphertext == DeletedOrNonExistent {
    43  		return DeletedOrNonExistent
    44  	}
    45  	b := sha256.Sum256([]byte(*ciphertext))
    46  	return hex.EncodeToString(b[:])
    47  }
    48  
    49  func (k *KVRevisionCache) checkLocked(mctx libkb.MetaContext, entryID keybase1.KVEntryID, ciphertext *string, teamKeyGen keybase1.PerTeamKeyGeneration, revision int) (err error) {
    50  	k.ensureIntermediateLocked(entryID)
    51  
    52  	entry, ok := k.data[entryID.TeamID][entryID.Namespace][entryID.EntryKey]
    53  	if !ok {
    54  		// this entry didn't exist in the cache, so there's nothing to check
    55  		return nil
    56  	}
    57  	entryHash := k.hash(ciphertext)
    58  	if revision < entry.Revision {
    59  		return KVCacheError{fmt.Sprintf("cache error: revision decreased from %d to %d", entry.Revision, revision)}
    60  	}
    61  	if teamKeyGen < entry.TeamKeyGen {
    62  		return KVCacheError{fmt.Sprintf("cache error: team key generation decreased from %d to %d", entry.TeamKeyGen, teamKeyGen)}
    63  	}
    64  	if revision == entry.Revision {
    65  		if teamKeyGen != entry.TeamKeyGen {
    66  			return KVCacheError{fmt.Sprintf("cache error: at the same revision (%d) team key gen cannot be different: %d -> %d", revision, entry.TeamKeyGen, teamKeyGen)}
    67  		}
    68  		if entryHash != entry.EntryHash {
    69  			return KVCacheError{fmt.Sprintf("cache error: at the same revision (%d) hash of entry cannot be different: %s -> %s", revision, entry.EntryHash, entryHash)}
    70  		}
    71  	}
    72  	return nil
    73  }
    74  
    75  func (k *KVRevisionCache) Check(mctx libkb.MetaContext, entryID keybase1.KVEntryID, ciphertext *string, teamKeyGen keybase1.PerTeamKeyGeneration, revision int) (err error) {
    76  	k.Lock()
    77  	defer k.Unlock()
    78  
    79  	return k.checkLocked(mctx, entryID, ciphertext, teamKeyGen, revision)
    80  }
    81  
    82  func (k *KVRevisionCache) Put(mctx libkb.MetaContext, entryID keybase1.KVEntryID, ciphertext *string, teamKeyGen keybase1.PerTeamKeyGeneration, revision int) (err error) {
    83  	k.Lock()
    84  	defer k.Unlock()
    85  
    86  	err = k.checkLocked(mctx, entryID, ciphertext, teamKeyGen, revision)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	entryHash := k.hash(ciphertext)
    92  	newEntry := kvCacheEntry{
    93  		EntryHash:  entryHash,
    94  		TeamKeyGen: teamKeyGen,
    95  		Revision:   revision,
    96  	}
    97  	k.data[entryID.TeamID][entryID.Namespace][entryID.EntryKey] = newEntry
    98  	return nil
    99  }
   100  
   101  func (k *KVRevisionCache) checkForUpdateLocked(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int) (err error) {
   102  	k.ensureIntermediateLocked(entryID)
   103  
   104  	entry, ok := k.data[entryID.TeamID][entryID.Namespace][entryID.EntryKey]
   105  	if !ok {
   106  		// this entry didn't exist in the cache, so there's nothing to check
   107  		return nil
   108  	}
   109  	if revision <= entry.Revision {
   110  		return NewKVRevisionError("" /* use the default out-of-date message */)
   111  	}
   112  	return nil
   113  }
   114  
   115  func (k *KVRevisionCache) CheckForUpdate(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int) (err error) {
   116  	k.Lock()
   117  	defer k.Unlock()
   118  
   119  	return k.checkForUpdateLocked(mctx, entryID, revision)
   120  }
   121  
   122  func (k *KVRevisionCache) MarkDeleted(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int) (err error) {
   123  	k.Lock()
   124  	defer k.Unlock()
   125  
   126  	err = k.checkForUpdateLocked(mctx, entryID, revision)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	existingEntry, ok := k.data[entryID.TeamID][entryID.Namespace][entryID.EntryKey]
   131  	if !ok {
   132  		// deleting an entry that's not been seen yet by the cache.
   133  		// being explicit here that it's ok to use an empty entry
   134  		existingEntry = kvCacheEntry{}
   135  	}
   136  	newEntry := kvCacheEntry{
   137  		EntryHash:  DeletedOrNonExistent,
   138  		TeamKeyGen: existingEntry.TeamKeyGen, // nothing gets encrypted here, so this should just roll forward or default to 0
   139  		Revision:   revision,
   140  	}
   141  	k.data[entryID.TeamID][entryID.Namespace][entryID.EntryKey] = newEntry
   142  
   143  	return nil
   144  }
   145  
   146  // Inspect is only really useful for testing
   147  func (k *KVRevisionCache) Inspect(entryID keybase1.KVEntryID) (entryHash string, generation keybase1.PerTeamKeyGeneration, revision int) {
   148  	entry := k.data[entryID.TeamID][entryID.Namespace][entryID.EntryKey]
   149  	return entry.EntryHash, entry.TeamKeyGen, entry.Revision
   150  }
   151  
   152  // ensure initialized maps exist for intermediate data structures
   153  func (k *KVRevisionCache) ensureIntermediateLocked(entryID keybase1.KVEntryID) {
   154  	// call this function inside a previously acquired lock
   155  	_, ok := k.data[entryID.TeamID]
   156  	if !ok {
   157  		// populate intermediate data structures to prevent panics
   158  		k.data[entryID.TeamID] = make(map[string]map[string]kvCacheEntry)
   159  	}
   160  	_, ok = k.data[entryID.TeamID][entryID.Namespace]
   161  	if !ok {
   162  		// populate intermediate data structures to prevent panics
   163  		k.data[entryID.TeamID][entryID.Namespace] = make(map[string]kvCacheEntry)
   164  	}
   165  }
   166  
   167  func (k *KVRevisionCache) OnLogout(m libkb.MetaContext) error {
   168  	k.data = make(kvCacheData)
   169  	return nil
   170  }
   171  
   172  func (k *KVRevisionCache) OnDbNuke(m libkb.MetaContext) error {
   173  	k.data = make(kvCacheData)
   174  	return nil
   175  }