github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/contacts/cache.go (about) 1 // Copyright 2019 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package contacts 5 6 import ( 7 "context" 8 "fmt" 9 "sync" 10 "time" 11 12 "github.com/keybase/client/go/encrypteddb" 13 "github.com/keybase/client/go/libkb" 14 "github.com/keybase/client/go/protocol/keybase1" 15 ) 16 17 // ContactCacheStore is used by CachedContactsProvider to store contact cache 18 // encrypted with device key. 19 type ContactCacheStore struct { 20 encryptedDB *encrypteddb.EncryptedDB 21 } 22 23 func (s *ContactCacheStore) dbKey(uid keybase1.UID) libkb.DbKey { 24 return libkb.DbKey{ 25 Typ: libkb.DBContactResolution, 26 Key: fmt.Sprintf("%v", uid), 27 } 28 } 29 30 // NewContactCacheStore creates new ContactCacheStore for global context. The 31 // store is used to securely store cached contact resolutions. 32 func NewContactCacheStore(g *libkb.GlobalContext) *ContactCacheStore { 33 keyFn := func(ctx context.Context) ([32]byte, error) { 34 return encrypteddb.GetSecretBoxKey(ctx, g, 35 libkb.EncryptionReasonContactsLocalStorage, "encrypting contact resolution cache") 36 } 37 dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb { 38 return g.LocalDb 39 } 40 return &ContactCacheStore{ 41 encryptedDB: encrypteddb.New(g, dbFn, keyFn), 42 } 43 } 44 45 type CachedContactsProvider struct { 46 lock sync.Mutex 47 48 Provider ContactsProvider 49 Store *ContactCacheStore 50 } 51 52 var _ ContactsProvider = (*CachedContactsProvider)(nil) 53 54 type cachedLookupResult struct { 55 ContactLookupResult 56 Resolved bool 57 ExpiresAt time.Time 58 } 59 60 type lookupResultCache struct { 61 Lookups map[ContactLookupKey]cachedLookupResult 62 Token Token 63 Version struct { 64 Major int 65 Minor int 66 } 67 } 68 69 func newLookupResultCache() (ret lookupResultCache) { 70 ret = lookupResultCache{ 71 Lookups: make(map[ContactLookupKey]cachedLookupResult), 72 } 73 ret.Version.Major = cacheCurrentMajorVersion 74 ret.Version.Minor = cacheCurrentMinorVersion 75 return ret 76 } 77 78 const cacheCurrentMajorVersion = 2 79 const cacheCurrentMinorVersion = 0 80 81 func cachedResultFromLookupResult(v ContactLookupResult, expires time.Time) cachedLookupResult { 82 return cachedLookupResult{ 83 ContactLookupResult: v, 84 Resolved: true, 85 ExpiresAt: expires, 86 } 87 } 88 89 // Time after we throw away the result and not return it anymore. When a cached 90 // entry expires, we will try to update it, but if we fail to do so, we will 91 // still return it - so user does not lose all their cache if they happen to be 92 // offline after expiration time. But if a cached entry has not been refreshed 93 // for duration of server provided expiration time plus cacheEvictionTime, it's 94 // discarded entirely. 95 const cacheEvictionTime = 45 * 24 * time.Hour // approx 45 days 96 97 func (c cachedLookupResult) getEvictionTime() time.Time { 98 return c.ExpiresAt.Add(cacheEvictionTime) 99 } 100 101 func (c *lookupResultCache) findFreshOrSetEmpty(mctx libkb.MetaContext, key ContactLookupKey) (res cachedLookupResult, stale bool, found bool) { 102 now := mctx.G().Clock().Now() 103 res, found = c.Lookups[key] 104 if !found || now.After(res.getEvictionTime()) { 105 // Pre-insert to the cache. If Provider.LookupAll does not find 106 // these, they will stay in the cache as unresolved, otherwise they 107 // are overwritten. 108 109 // Caller is supposed to set proper ExpiresAt value. 110 res = cachedLookupResult{Resolved: false, ExpiresAt: now} 111 c.Lookups[key] = res 112 return res, false, false 113 } 114 return res, now.After(res.ExpiresAt), true 115 } 116 117 func (c *lookupResultCache) cleanup(mctx libkb.MetaContext) { 118 now := mctx.G().Clock().Now() 119 for key, val := range c.Lookups { 120 if now.After(val.getEvictionTime()) { 121 delete(c.Lookups, key) 122 } 123 } 124 } 125 126 func (s *ContactCacheStore) getCache(mctx libkb.MetaContext) (obj lookupResultCache, created bool) { 127 var conCache lookupResultCache 128 var createCache bool 129 cacheKey := s.dbKey(mctx.CurrentUID()) 130 found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &conCache) 131 switch { 132 case err != nil: 133 mctx.Warning("Unable to pull contact lookup cache: %s", err) 134 createCache = true 135 case !found: 136 mctx.Debug("No contact lookup cache found, creating new cache object") 137 createCache = true 138 case conCache.Version.Major != cacheCurrentMajorVersion: 139 mctx.Debug("Found contact cache object but major version is %d (need %d)", conCache.Version.Major, cacheCurrentMajorVersion) 140 createCache = true 141 } 142 // NOTE: If we ever have a cache change that keeps major version same but 143 // increases minor version, do the object upgrade here. 144 145 if createCache { 146 conCache = newLookupResultCache() 147 } 148 return conCache, createCache 149 } 150 151 func (s *ContactCacheStore) putCache(mctx libkb.MetaContext, cacheObj lookupResultCache) error { 152 cacheKey := s.dbKey(mctx.CurrentUID()) 153 return s.encryptedDB.Put(mctx.Ctx(), cacheKey, cacheObj) 154 } 155 156 func (s *ContactCacheStore) ClearCache(mctx libkb.MetaContext) error { 157 cacheKey := s.dbKey(mctx.CurrentUID()) 158 return s.encryptedDB.Delete(mctx.Ctx(), cacheKey) 159 } 160 161 func (c *CachedContactsProvider) LookupAllWithToken(mctx libkb.MetaContext, emails []keybase1.EmailAddress, 162 numbers []keybase1.RawPhoneNumber, _ Token) (res ContactLookupResults, err error) { 163 return c.LookupAll(mctx, emails, numbers) 164 } 165 166 func (c *CachedContactsProvider) LookupAll(mctx libkb.MetaContext, emails []keybase1.EmailAddress, 167 numbers []keybase1.RawPhoneNumber) (res ContactLookupResults, err error) { 168 169 defer mctx.Trace(fmt.Sprintf("CachedContactsProvider#LookupAll(len=%d)", len(emails)+len(numbers)), 170 nil)() 171 172 res = NewContactLookupResults() 173 if len(emails)+len(numbers) == 0 { 174 return res, nil 175 } 176 177 // This is a rather long-lived lock, because normally it will be held 178 // through the entire duration of the lookup, but: 179 // - We don't expect this to be called concurrently, or repeatedly, without 180 // user's interaction. 181 // - We want to avoid looking up the same assertion multiple times (burning 182 // through the rate limit), while keeping the locking strategy simple. 183 c.lock.Lock() 184 defer c.lock.Unlock() 185 186 conCache, _ := c.Store.getCache(mctx) 187 188 var remainingEmails []keybase1.EmailAddress 189 var remainingNumbers []keybase1.RawPhoneNumber 190 191 mctx.Debug("Populating results from cache") 192 193 // List of keys of new or stale cache entries, to set ExpireAt value after 194 // we do parent provider LookupAll call. 195 newCacheEntries := make([]ContactLookupKey, 0, len(remainingEmails)+len(remainingNumbers)) 196 197 for _, email := range emails { 198 key := MakeEmailLookupKey(email) 199 cache, stale, found := conCache.findFreshOrSetEmpty(mctx, key) 200 if found && cache.Resolved { 201 // Store result even if stale, but may be overwritten by API query later. 202 res.Results[key] = cache.ContactLookupResult 203 } 204 if !found || stale { 205 remainingEmails = append(remainingEmails, email) 206 newCacheEntries = append(newCacheEntries, key) 207 } 208 } 209 210 for _, number := range numbers { 211 key := MakePhoneLookupKey(number) 212 cache, stale, found := conCache.findFreshOrSetEmpty(mctx, key) 213 if found && cache.Resolved { 214 // Store result even if stale, but may be overwritten by API query later. 215 res.Results[key] = cache.ContactLookupResult 216 } 217 if !found || stale { 218 remainingNumbers = append(remainingNumbers, number) 219 newCacheEntries = append(newCacheEntries, key) 220 } 221 } 222 223 mctx.Debug("After checking cache, %d emails and %d numbers left to be looked up", len(remainingEmails), len(remainingNumbers)) 224 225 if len(remainingEmails)+len(remainingNumbers) > 0 { 226 apiRes, err := c.Provider.LookupAllWithToken(mctx, remainingEmails, remainingNumbers, conCache.Token) 227 if err == nil { 228 conCache.Token = apiRes.Token 229 230 now := mctx.G().Clock().Now() 231 expiresAt := now.Add(apiRes.ResolvedFreshness) 232 for k, v := range apiRes.Results { 233 res.Results[k] = v 234 conCache.Lookups[k] = cachedResultFromLookupResult(v, expiresAt) 235 } 236 // Loop through entries that we asked for and find these we did not get 237 // resolutions for. Set ExpiresAt now that we know UnresolvedFreshness. 238 unresolvedExpiresAt := now.Add(apiRes.UnresolvedFreshness) 239 for _, key := range newCacheEntries { 240 val := conCache.Lookups[key] 241 if !val.Resolved { 242 val.ExpiresAt = unresolvedExpiresAt 243 conCache.Lookups[key] = val 244 } 245 } 246 } else { 247 mctx.Warning("Unable to call Provider.LookupAll, returning only cached results: %s", err) 248 } 249 250 conCache.cleanup(mctx) 251 cerr := c.Store.putCache(mctx, conCache) 252 if cerr != nil { 253 mctx.Warning("Unable to update cache: %s", cerr) 254 } 255 } 256 257 return res, nil 258 } 259 260 func (c *CachedContactsProvider) FindUsernames(mctx libkb.MetaContext, uids []keybase1.UID) (map[keybase1.UID]ContactUsernameAndFullName, error) { 261 return c.Provider.FindUsernames(mctx, uids) 262 } 263 264 func (c *CachedContactsProvider) FindFollowing(mctx libkb.MetaContext, uids []keybase1.UID) (map[keybase1.UID]bool, error) { 265 return c.Provider.FindFollowing(mctx, uids) 266 } 267 268 func (c *CachedContactsProvider) FindServiceMaps(mctx libkb.MetaContext, uids []keybase1.UID) (map[keybase1.UID]libkb.UserServiceSummary, error) { 269 return c.Provider.FindServiceMaps(mctx, uids) 270 } 271 272 // RemoveContactsCachePhoneEntry removes cached lookup for phone number. 273 func (s *ContactCacheStore) RemoveContactsCacheEntries(mctx libkb.MetaContext, 274 phone *keybase1.PhoneNumber, email *keybase1.EmailAddress) { 275 // TODO: Use a phoneNumber | email variant instead of two pointers. 276 cacheObj, created := s.getCache(mctx) 277 if created { 278 // There was no cache. 279 return 280 } 281 if phone != nil { 282 // TODO: this type conversion shouldn't have to be here, 283 // since this cache should take `PhoneNumber`s. 284 delete(cacheObj.Lookups, MakePhoneLookupKey(keybase1.RawPhoneNumber(*phone))) 285 mctx.Debug("ContactCacheStore: Removing phone number %q from lookup cache", *phone) 286 } 287 if email != nil { 288 delete(cacheObj.Lookups, MakeEmailLookupKey(*email)) 289 mctx.Debug("ContactCacheStore: Removing email %q from lookup cache", *email) 290 } 291 err := s.putCache(mctx, cacheObj) 292 if err != nil { 293 mctx.Warning("ContactCacheStore: Unable to update cache: %s", err) 294 } 295 }