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 }