1package contacts 2 3import ( 4 "context" 5 "fmt" 6 7 "github.com/keybase/client/go/encrypteddb" 8 9 "github.com/keybase/client/go/libkb" 10 "github.com/keybase/client/go/protocol/keybase1" 11) 12 13// Saving contact list into encrypted db. 14 15// Cache resolutions of a lookup ran on entire contact list provided by the 16// frontend. Assume every time SaveContacts is called, entire contact list is 17// passed as an argument. Always cache the result of last resolution, do not do 18// any result merging. 19 20type SavedContactsStore struct { 21 encryptedDB *encrypteddb.EncryptedDB 22} 23 24var _ libkb.SyncedContactListProvider = (*SavedContactsStore)(nil) 25 26// NewSavedContactsStore creates a new SavedContactsStore for global context. 27// The store is used to securely store list of resolved contacts. 28func NewSavedContactsStore(g *libkb.GlobalContext) *SavedContactsStore { 29 keyFn := func(ctx context.Context) ([32]byte, error) { 30 return encrypteddb.GetSecretBoxKey(ctx, g, 31 libkb.EncryptionReasonContactsLocalStorage, "encrypting local contact list") 32 } 33 dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb { 34 return g.LocalDb 35 } 36 return &SavedContactsStore{ 37 encryptedDB: encrypteddb.New(g, dbFn, keyFn), 38 } 39} 40 41func ServiceInit(g *libkb.GlobalContext) { 42 g.SyncedContactList = NewSavedContactsStore(g) 43} 44 45func savedContactsDbKey(uid keybase1.UID) libkb.DbKey { 46 return libkb.DbKey{ 47 Typ: libkb.DBSavedContacts, 48 Key: fmt.Sprintf("%v", uid), 49 } 50} 51 52type savedContactsCache struct { 53 Contacts []keybase1.ProcessedContact 54 Version int 55} 56 57const savedContactsCurrentVer = 1 58 59func assertionToNameDbKey(uid keybase1.UID) libkb.DbKey { 60 return libkb.DbKey{ 61 Typ: libkb.DBSavedContacts, 62 Key: fmt.Sprintf("lookup:%v", uid), 63 } 64} 65 66type assertionToNameCache struct { 67 AssertionToName map[string]string 68 Version int 69} 70 71const assertionToNameCurrentVer = 1 72 73func ResolveAndSaveContacts(mctx libkb.MetaContext, provider ContactsProvider, contacts []keybase1.Contact) (res keybase1.ContactListResolutionResult, err error) { 74 resolveResults, err := ResolveContacts(mctx, provider, contacts) 75 if err != nil { 76 return res, err 77 } 78 79 // find resolved contacts 80 for _, contact := range resolveResults { 81 // Strip out the user and anyone they follow. 82 if contact.Resolved && !contact.Following && 83 libkb.NewNormalizedUsername(contact.Username) != mctx.CurrentUsername() { 84 res.Resolved = append(res.Resolved, contact) 85 } 86 } 87 88 // find newly resolved 89 s := mctx.G().SyncedContactList 90 currentContacts, err := s.RetrieveContacts(mctx) 91 92 newlyResolvedMap := make(map[string]keybase1.ProcessedContact) 93 if err == nil { 94 unresolved := make(map[string]bool) 95 resolved := make(map[string]bool) 96 for _, contact := range currentContacts { 97 if contact.Resolved { 98 resolved[contact.ContactName] = true 99 } 100 if resolved[contact.ContactName] { 101 // If any contact by this name is resolved, we dedupe. 102 delete(unresolved, contact.ContactName) 103 } else { 104 // We resolve based on display name, not assertion, so we don't 105 // duplicate multiple assertions for the same contact. 106 unresolved[contact.ContactName] = true 107 } 108 } 109 110 for _, resolution := range resolveResults { 111 if unresolved[resolution.ContactName] && resolution.Resolved && !resolution.Following { 112 // We only want to show one resolution per username. 113 newlyResolvedMap[resolution.Username] = resolution 114 } 115 } 116 } else { 117 mctx.Warning("error retrieving synced contacts; continuing: %s", err) 118 } 119 120 if len(newlyResolvedMap) == 0 { 121 return res, s.SaveProcessedContacts(mctx, resolveResults) 122 } 123 124 resolutionsForPeoplePage := make([]ContactResolution, 0, len(newlyResolvedMap)) 125 for _, contact := range newlyResolvedMap { 126 contactDisplay := contact.ContactName 127 if contactDisplay == "" { 128 contactDisplay = contact.Component.ValueString() 129 } 130 resolutionsForPeoplePage = append(resolutionsForPeoplePage, ContactResolution{ 131 Description: contactDisplay, 132 ResolvedUser: keybase1.User{ 133 Uid: contact.Uid, 134 Username: contact.Username, 135 }, 136 }) 137 res.NewlyResolved = append(res.NewlyResolved, contact) 138 } 139 err = SendEncryptedContactResolutionToServer(mctx, resolutionsForPeoplePage) 140 if err != nil { 141 mctx.Warning("Could not add resolved contacts to people page: %v; returning contacts anyway", err) 142 } 143 144 return res, s.SaveProcessedContacts(mctx, resolveResults) 145} 146 147func makeAssertionToName(contacts []keybase1.ProcessedContact) (res map[string]string) { 148 res = make(map[string]string) 149 toRemove := make(map[string]struct{}) 150 for _, contact := range contacts { 151 if _, ok := res[contact.Assertion]; ok { 152 // multiple contacts match this assertion, remove once we're done 153 toRemove[contact.Assertion] = struct{}{} 154 continue 155 } 156 res[contact.Assertion] = contact.ContactName 157 } 158 for remove := range toRemove { 159 delete(res, remove) 160 } 161 return res 162} 163 164func (s *SavedContactsStore) SaveProcessedContacts(mctx libkb.MetaContext, contacts []keybase1.ProcessedContact) (err error) { 165 val := savedContactsCache{ 166 Contacts: contacts, 167 Version: savedContactsCurrentVer, 168 } 169 170 cacheKey := savedContactsDbKey(mctx.CurrentUID()) 171 err = s.encryptedDB.Put(mctx.Ctx(), cacheKey, val) 172 if err != nil { 173 return err 174 } 175 176 assertionToName := makeAssertionToName(contacts) 177 lookupVal := assertionToNameCache{ 178 AssertionToName: assertionToName, 179 Version: assertionToNameCurrentVer, 180 } 181 182 cacheKey = assertionToNameDbKey(mctx.CurrentUID()) 183 err = s.encryptedDB.Put(mctx.Ctx(), cacheKey, lookupVal) 184 return err 185} 186 187func (s *SavedContactsStore) RetrieveContacts(mctx libkb.MetaContext) (ret []keybase1.ProcessedContact, err error) { 188 cacheKey := savedContactsDbKey(mctx.CurrentUID()) 189 var cache savedContactsCache 190 found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &cache) 191 if err != nil { 192 return nil, err 193 } 194 if !found { 195 return ret, nil 196 } 197 if cache.Version != savedContactsCurrentVer { 198 mctx.Warning("synced contact list found but had an old version (found: %d, need: %d), returning empty list", 199 cache.Version, savedContactsCurrentVer) 200 return ret, nil 201 } 202 return cache.Contacts, nil 203} 204 205func (s *SavedContactsStore) RetrieveAssertionToName(mctx libkb.MetaContext) (ret map[string]string, err error) { 206 cacheKey := assertionToNameDbKey(mctx.CurrentUID()) 207 var cache assertionToNameCache 208 found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &cache) 209 if err != nil { 210 return nil, err 211 } 212 if !found { 213 return ret, nil 214 } 215 if cache.Version != assertionToNameCurrentVer { 216 mctx.Warning("assertion to name found but had an old version (found: %d, need: %d), returning empty map", 217 cache.Version, assertionToNameCurrentVer) 218 return ret, nil 219 } 220 return cache.AssertionToName, nil 221} 222 223func (s *SavedContactsStore) UnresolveContactsWithComponent(mctx libkb.MetaContext, 224 phoneNumber *keybase1.PhoneNumber, email *keybase1.EmailAddress) { 225 // TODO: Use a phoneNumber | email variant instead of two pointers. 226 contactList, err := s.RetrieveContacts(mctx) 227 if err != nil { 228 mctx.Warning("Failed to get cached contact list: %x", err) 229 return 230 } 231 for i, con := range contactList { 232 var unresolve bool 233 switch { 234 case phoneNumber != nil && con.Component.PhoneNumber != nil: 235 unresolve = *con.Component.PhoneNumber == keybase1.RawPhoneNumber(*phoneNumber) 236 case email != nil && con.Component.Email != nil: 237 unresolve = *con.Component.Email == *email 238 } 239 240 if unresolve { 241 // Unresolve contact. 242 con.Resolved = false 243 con.Username = "" 244 con.Uid = "" 245 con.Following = false 246 con.FullName = "" 247 // TODO: DisplayName/DisplayLabel logic infects yet another file / 248 // module. But it will sort itself out once we get rid of both. 249 con.DisplayName = con.ContactName 250 con.DisplayLabel = con.Component.FormatDisplayLabel(false /* addLabel */) 251 contactList[i] = con 252 } 253 } 254 err = s.SaveProcessedContacts(mctx, contactList) 255 if err != nil { 256 mctx.Warning("Failed to put cached contact list: %x", err) 257 } 258} 259