github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }