1// Copyright 2019 Keybase, Inc. All rights reserved. Use of 2// this source code is governed by the included BSD license. 3 4// RPC handlers for kvstore operations 5 6package service 7 8import ( 9 "fmt" 10 "strings" 11 "sync" 12 13 "github.com/keybase/client/go/kvstore" 14 "github.com/keybase/client/go/libkb" 15 keybase1 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/keybase/client/go/teams" 17 "github.com/keybase/go-framed-msgpack-rpc/rpc" 18 "golang.org/x/net/context" 19) 20 21type KVStoreHandler struct { 22 *BaseHandler 23 sync.Mutex 24 libkb.Contextified 25 Boxer kvstore.KVStoreBoxer 26} 27 28var _ keybase1.KvstoreInterface = (*KVStoreHandler)(nil) 29 30func NewKVStoreHandler(xp rpc.Transporter, g *libkb.GlobalContext) *KVStoreHandler { 31 if g.GetKVRevisionCache() == nil { 32 g.SetKVRevisionCache(kvstore.NewKVRevisionCache(g)) 33 } 34 return &KVStoreHandler{ 35 BaseHandler: NewBaseHandler(g, xp), 36 Contextified: libkb.NewContextified(g), 37 Boxer: kvstore.NewKVStoreBoxer(g), 38 } 39} 40 41func (h *KVStoreHandler) resolveTeam(mctx libkb.MetaContext, userInputTeamName string) (teamID keybase1.TeamID, err error) { 42 if strings.Contains(userInputTeamName, ",") { 43 // it's an implicit team that might not exist yet 44 team, _, _, err := teams.LookupOrCreateImplicitTeam(mctx.Ctx(), mctx.G(), userInputTeamName, false /*public*/) 45 if err != nil { 46 mctx.Debug("error loading implicit team %s: %v", userInputTeamName, err) 47 err = libkb.AppStatusError{ 48 Code: libkb.SCTeamReadError, 49 Desc: "You are not a member of this team", 50 } 51 return teamID, err 52 } 53 return team.ID, nil 54 } 55 teamID, err = teams.GetTeamIDByNameRPC(mctx, userInputTeamName) 56 if err != nil { 57 mctx.Debug("error resolving team with name %s: %v", userInputTeamName, err) 58 } 59 return teamID, err 60} 61 62type getEntryAPIRes struct { 63 libkb.AppStatusEmbed 64 TeamID keybase1.TeamID `json:"team_id"` 65 Namespace string `json:"namespace"` 66 EntryKey string `json:"entry_key"` 67 TeamKeyGen keybase1.PerTeamKeyGeneration `json:"team_key_gen"` 68 Revision int `json:"revision"` 69 Ciphertext *string `json:"ciphertext"` 70 FormatVersion int `json:"format_version"` 71 WriterUID keybase1.UID `json:"uid"` 72 WriterEldestSeqno keybase1.Seqno `json:"eldest_seqno"` 73 WriterDeviceID keybase1.DeviceID `json:"device_id"` 74} 75 76func (h *KVStoreHandler) serverFetch(mctx libkb.MetaContext, entryID keybase1.KVEntryID) (emptyRes getEntryAPIRes, err error) { 77 var apiRes getEntryAPIRes 78 apiArg := libkb.APIArg{ 79 Endpoint: "team/storage", 80 SessionType: libkb.APISessionTypeREQUIRED, 81 Args: libkb.HTTPArgs{ 82 "team_id": libkb.S{Val: entryID.TeamID.String()}, 83 "namespace": libkb.S{Val: entryID.Namespace}, 84 "entry_key": libkb.S{Val: entryID.EntryKey}, 85 }, 86 } 87 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 88 if err != nil { 89 mctx.Debug("error fetching %+v from server: %v", entryID, err) 90 return emptyRes, err 91 } 92 if apiRes.TeamID != entryID.TeamID { 93 return emptyRes, fmt.Errorf("api returned an unexpected teamID: %s isn't %s", apiRes.TeamID, entryID.TeamID) 94 } 95 if apiRes.Namespace != entryID.Namespace { 96 return emptyRes, fmt.Errorf("api returned an unexpected namespace: %s isn't %s", apiRes.Namespace, entryID.Namespace) 97 } 98 if apiRes.EntryKey != entryID.EntryKey { 99 return emptyRes, fmt.Errorf("api returned an unexpected entryKey: %s isn't %s", apiRes.EntryKey, entryID.EntryKey) 100 } 101 return apiRes, nil 102} 103 104func (h *KVStoreHandler) GetKVEntry(ctx context.Context, arg keybase1.GetKVEntryArg) (res keybase1.KVGetResult, err error) { 105 h.Lock() 106 defer h.Unlock() 107 return h.getKVEntryLocked(ctx, arg) 108} 109 110func (h *KVStoreHandler) getKVEntryLocked(ctx context.Context, arg keybase1.GetKVEntryArg) (res keybase1.KVGetResult, err error) { 111 ctx = libkb.WithLogTag(ctx, "KV") 112 mctx := libkb.NewMetaContext(ctx, h.G()) 113 defer mctx.Trace(fmt.Sprintf("KVStoreHandler#GetKVEntry: t:%s, n:%s, k:%s", arg.TeamName, arg.Namespace, arg.EntryKey), &err)() 114 115 if err := assertLoggedIn(ctx, h.G()); err != nil { 116 mctx.Debug("not logged in err: %v", err) 117 return res, err 118 } 119 teamID, err := h.resolveTeam(mctx, arg.TeamName) 120 if err != nil { 121 return res, err 122 } 123 entryID := keybase1.KVEntryID{ 124 TeamID: teamID, 125 Namespace: arg.Namespace, 126 EntryKey: arg.EntryKey, 127 } 128 apiRes, err := h.serverFetch(mctx, entryID) 129 if err != nil { 130 mctx.Debug("error fetching %+v from server: %v", entryID, err) 131 return res, err 132 } 133 // check the server response against the local cache 134 err = mctx.G().GetKVRevisionCache().Check(mctx, entryID, apiRes.Ciphertext, apiRes.TeamKeyGen, apiRes.Revision) 135 if err != nil { 136 err = fmt.Errorf("error comparing the entry from the server to what's in the local cache: %s", err) 137 mctx.Debug("%+v: %s", entryID, err) 138 return res, err 139 } 140 var entryValue *string 141 if apiRes.Ciphertext != nil && len(*apiRes.Ciphertext) > 0 { 142 // ciphertext coming back from the server is available to be unboxed (has previously been set, and was not previously deleted) 143 cleartext, err := h.Boxer.Unbox(mctx, entryID, apiRes.Revision, *apiRes.Ciphertext, apiRes.TeamKeyGen, apiRes.FormatVersion, apiRes.WriterUID, apiRes.WriterEldestSeqno, apiRes.WriterDeviceID) 144 if err != nil { 145 mctx.Debug("error unboxing %+v: %v", entryID, err) 146 return res, err 147 } 148 entryValue = &cleartext 149 } 150 err = mctx.G().GetKVRevisionCache().Put(mctx, entryID, apiRes.Ciphertext, apiRes.TeamKeyGen, apiRes.Revision) 151 if err != nil { 152 err = fmt.Errorf("error putting newly fetched values into the local cache: %s", err) 153 mctx.Debug("%+v: %s", entryID, err) 154 return res, err 155 } 156 return keybase1.KVGetResult{ 157 TeamName: arg.TeamName, 158 Namespace: arg.Namespace, 159 EntryKey: arg.EntryKey, 160 EntryValue: entryValue, 161 Revision: apiRes.Revision, 162 }, nil 163} 164 165type putEntryAPIRes struct { 166 libkb.AppStatusEmbed 167 Revision int `json:"revision"` 168} 169 170func (h *KVStoreHandler) PutKVEntry(ctx context.Context, arg keybase1.PutKVEntryArg) (res keybase1.KVPutResult, err error) { 171 h.Lock() 172 defer h.Unlock() 173 return h.putKVEntryLocked(ctx, arg) 174} 175 176func (h *KVStoreHandler) putKVEntryLocked(ctx context.Context, arg keybase1.PutKVEntryArg) (res keybase1.KVPutResult, err error) { 177 ctx = libkb.WithLogTag(ctx, "KV") 178 mctx := libkb.NewMetaContext(ctx, h.G()) 179 defer mctx.Trace(fmt.Sprintf("KVStoreHandler#PutKVEntry: t:%s, n:%s, k:%s, r:%d", arg.TeamName, arg.Namespace, arg.EntryKey, arg.Revision), &err)() 180 if err := assertLoggedIn(ctx, h.G()); err != nil { 181 mctx.Debug("not logged in err: %v", err) 182 return res, err 183 } 184 teamID, err := h.resolveTeam(mctx, arg.TeamName) 185 if err != nil { 186 return res, err 187 } 188 entryID := keybase1.KVEntryID{ 189 TeamID: teamID, 190 Namespace: arg.Namespace, 191 EntryKey: arg.EntryKey, 192 } 193 194 revision := arg.Revision 195 if revision == 0 { 196 // fetch to get the correct revision when it's not specified 197 getRes, err := h.getKVEntryLocked(ctx, keybase1.GetKVEntryArg{ 198 SessionID: arg.SessionID, 199 TeamName: arg.TeamName, 200 Namespace: arg.Namespace, 201 EntryKey: arg.EntryKey, 202 }) 203 if err != nil { 204 err = fmt.Errorf("error fetching the revision before writing this entry: %s", err) 205 mctx.Debug("%+v: %s", entryID, err) 206 return res, err 207 } 208 revision = getRes.Revision + 1 209 } 210 211 mctx.Debug("updating %+v to revision %d", entryID, revision) 212 ciphertext, teamKeyGen, ciphertextVersion, err := h.Boxer.Box(mctx, entryID, revision, arg.EntryValue) 213 if err != nil { 214 mctx.Debug("error boxing %+v: %v", entryID, err) 215 return res, err 216 } 217 err = mctx.G().GetKVRevisionCache().CheckForUpdate(mctx, entryID, revision) 218 if err != nil { 219 mctx.Debug("error from cache for updating %+v: %s", entryID, err) 220 return res, err 221 } 222 223 apiArg := libkb.APIArg{ 224 Endpoint: "team/storage", 225 SessionType: libkb.APISessionTypeREQUIRED, 226 Args: libkb.HTTPArgs{ 227 "team_id": libkb.S{Val: entryID.TeamID.String()}, 228 "team_key_gen": libkb.I{Val: int(teamKeyGen)}, 229 "namespace": libkb.S{Val: entryID.Namespace}, 230 "entry_key": libkb.S{Val: entryID.EntryKey}, 231 "ciphertext": libkb.S{Val: ciphertext}, 232 "ciphertext_version": libkb.I{Val: ciphertextVersion}, 233 "revision": libkb.I{Val: revision}, 234 }, 235 } 236 var apiRes putEntryAPIRes 237 err = mctx.G().API.PostDecode(mctx, apiArg, &apiRes) 238 if err != nil { 239 mctx.Debug("error posting update for %+v to the server: %v", entryID, err) 240 return res, err 241 } 242 if apiRes.Revision != revision { 243 mctx.Debug("expected the server to return revision %d but got %d for %+v", revision, apiRes.Revision, entryID) 244 return res, fmt.Errorf("kvstore PUT revision error. expected %d, got %d", revision, apiRes.Revision) 245 } 246 err = mctx.G().GetKVRevisionCache().Put(mctx, entryID, &ciphertext, teamKeyGen, revision) 247 if err != nil { 248 err = fmt.Errorf("error caching this new entry (try fetching it again): %s", err) 249 mctx.Debug("%+v: %s", entryID, err) 250 return res, err 251 } 252 return keybase1.KVPutResult{ 253 TeamName: arg.TeamName, 254 Namespace: arg.Namespace, 255 EntryKey: arg.EntryKey, 256 Revision: apiRes.Revision, 257 }, nil 258} 259 260func (h *KVStoreHandler) DelKVEntry(ctx context.Context, arg keybase1.DelKVEntryArg) (res keybase1.KVDeleteEntryResult, err error) { 261 h.Lock() 262 defer h.Unlock() 263 return h.delKVEntryLocked(ctx, arg) 264} 265 266func (h *KVStoreHandler) delKVEntryLocked(ctx context.Context, arg keybase1.DelKVEntryArg) (res keybase1.KVDeleteEntryResult, err error) { 267 ctx = libkb.WithLogTag(ctx, "KV") 268 mctx := libkb.NewMetaContext(ctx, h.G()) 269 defer mctx.Trace(fmt.Sprintf("KVStoreHandler#DeleteKVEntry: t:%s, n:%s, k:%s, r:%d", arg.TeamName, arg.Namespace, arg.EntryKey, arg.Revision), &err)() 270 if err := assertLoggedIn(ctx, h.G()); err != nil { 271 mctx.Debug("not logged in err: %v", err) 272 return res, err 273 } 274 teamID, err := h.resolveTeam(mctx, arg.TeamName) 275 if err != nil { 276 return res, err 277 } 278 entryID := keybase1.KVEntryID{ 279 TeamID: teamID, 280 Namespace: arg.Namespace, 281 EntryKey: arg.EntryKey, 282 } 283 284 revision := arg.Revision 285 if revision == 0 { 286 getArg := keybase1.GetKVEntryArg{ 287 SessionID: arg.SessionID, 288 TeamName: arg.TeamName, 289 Namespace: arg.Namespace, 290 EntryKey: arg.EntryKey, 291 } 292 getRes, err := h.getKVEntryLocked(ctx, getArg) 293 if err != nil { 294 err = fmt.Errorf("error fetching the revision before deleting this entry: %s", err) 295 mctx.Debug("%+v: %s", entryID, err) 296 return res, err 297 } 298 revision = getRes.Revision + 1 299 } 300 301 mctx.Debug("deleting %+v at revision %d", entryID, revision) 302 err = mctx.G().GetKVRevisionCache().CheckForUpdate(mctx, entryID, revision) 303 if err != nil { 304 mctx.Debug("error from cache for deleting %+v: %s", entryID, err) 305 return res, err 306 } 307 apiArg := libkb.APIArg{ 308 Endpoint: "team/storage", 309 SessionType: libkb.APISessionTypeREQUIRED, 310 Args: libkb.HTTPArgs{ 311 "team_id": libkb.S{Val: entryID.TeamID.String()}, 312 "namespace": libkb.S{Val: entryID.Namespace}, 313 "entry_key": libkb.S{Val: entryID.EntryKey}, 314 "revision": libkb.I{Val: revision}, 315 }, 316 } 317 apiRes, err := mctx.G().API.Delete(mctx, apiArg) 318 if err != nil { 319 mctx.Debug("error making delete request for entry %v: %v", entryID, err) 320 return res, err 321 } 322 responseRevision, err := apiRes.Body.AtKey("revision").GetInt() 323 if err != nil { 324 mctx.Debug("error getting the revision from the server response: %v", err) 325 err = fmt.Errorf("server response doesnt have a revision field: %s", err) 326 return res, err 327 } 328 if responseRevision != revision { 329 mctx.Debug("expected the server to return revision %d but got %d for %+v", revision, responseRevision, entryID) 330 return res, fmt.Errorf("kvstore DEL revision error. expected %d, got %d", revision, responseRevision) 331 } 332 err = mctx.G().GetKVRevisionCache().MarkDeleted(mctx, entryID, revision) 333 if err != nil { 334 err = fmt.Errorf("error caching this now-deleted entry (try fetching it): %s", err) 335 mctx.Debug("%+v: %s", entryID, err) 336 return res, err 337 } 338 return keybase1.KVDeleteEntryResult{ 339 TeamName: arg.TeamName, 340 Namespace: arg.Namespace, 341 EntryKey: arg.EntryKey, 342 Revision: revision, 343 }, nil 344} 345 346type getListNamespacesAPIRes struct { 347 libkb.AppStatusEmbed 348 TeamID keybase1.TeamID `json:"team_id"` 349 Namespaces []string `json:"namespaces"` 350} 351 352func (h *KVStoreHandler) ListKVNamespaces(ctx context.Context, arg keybase1.ListKVNamespacesArg) (res keybase1.KVListNamespaceResult, err error) { 353 h.Lock() 354 defer h.Unlock() 355 return h.listKVNamespaceLocked(ctx, arg) 356} 357 358func (h *KVStoreHandler) listKVNamespaceLocked(ctx context.Context, arg keybase1.ListKVNamespacesArg) (res keybase1.KVListNamespaceResult, err error) { 359 ctx = libkb.WithLogTag(ctx, "KV") 360 mctx := libkb.NewMetaContext(ctx, h.G()) 361 defer mctx.Trace(fmt.Sprintf("KVStoreHandler#ListKVNamespaces: t:%s", arg.TeamName), &err)() 362 if err := assertLoggedIn(ctx, h.G()); err != nil { 363 mctx.Debug("not logged in err: %v", err) 364 return res, err 365 } 366 teamID, err := h.resolveTeam(mctx, arg.TeamName) 367 if err != nil { 368 return res, err 369 } 370 371 var apiRes getListNamespacesAPIRes 372 apiArg := libkb.APIArg{ 373 Endpoint: "team/storage/list", 374 SessionType: libkb.APISessionTypeREQUIRED, 375 Args: libkb.HTTPArgs{ 376 "team_id": libkb.S{Val: teamID.String()}, 377 }, 378 } 379 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 380 if err != nil { 381 return res, err 382 } 383 if apiRes.TeamID != teamID { 384 mctx.Debug("list KV Namespaces server returned an unexpected, mismatching teamID") 385 return res, fmt.Errorf("expected teamID %s from the server, got %s", teamID, apiRes.TeamID) 386 } 387 return keybase1.KVListNamespaceResult{ 388 TeamName: arg.TeamName, 389 Namespaces: apiRes.Namespaces, 390 }, nil 391} 392 393type getListEntriesAPIRes struct { 394 libkb.AppStatusEmbed 395 TeamID keybase1.TeamID `json:"team_id"` 396 Namespace string `json:"namespace"` 397 EntryKeys []compressedEntryKey `json:"entry_keys"` 398} 399 400type compressedEntryKey struct { 401 EntryKey string `json:"k"` 402 Revision int `json:"r"` 403} 404 405func (h *KVStoreHandler) ListKVEntries(ctx context.Context, arg keybase1.ListKVEntriesArg) (res keybase1.KVListEntryResult, err error) { 406 h.Lock() 407 defer h.Unlock() 408 return h.listKVEntriesLocked(ctx, arg) 409} 410 411func (h *KVStoreHandler) listKVEntriesLocked(ctx context.Context, arg keybase1.ListKVEntriesArg) (res keybase1.KVListEntryResult, err error) { 412 ctx = libkb.WithLogTag(ctx, "KV") 413 mctx := libkb.NewMetaContext(ctx, h.G()) 414 defer mctx.Trace(fmt.Sprintf("KVStoreHandler#ListKVEntries: t:%s, n:%s", arg.TeamName, arg.Namespace), &err)() 415 if err := assertLoggedIn(ctx, h.G()); err != nil { 416 mctx.Debug("not logged in err: %v", err) 417 return res, err 418 } 419 teamID, err := h.resolveTeam(mctx, arg.TeamName) 420 if err != nil { 421 return res, err 422 } 423 var apiRes getListEntriesAPIRes 424 apiArg := libkb.APIArg{ 425 Endpoint: "team/storage/list", 426 SessionType: libkb.APISessionTypeREQUIRED, 427 Args: libkb.HTTPArgs{ 428 "team_id": libkb.S{Val: teamID.String()}, 429 "namespace": libkb.S{Val: arg.Namespace}, 430 }, 431 } 432 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 433 if err != nil { 434 return res, err 435 } 436 if apiRes.TeamID != teamID { 437 mctx.Debug("list KV Namespaces server returned an unexpected, mismatching teamID") 438 return res, fmt.Errorf("expected teamID %s from the server, got %s", teamID, apiRes.TeamID) 439 } 440 if apiRes.Namespace != arg.Namespace { 441 mctx.Debug("list KV EntryKeys server returned an unexpected, mismatching namespace") 442 return res, fmt.Errorf("expected namespace %s from the server, got %s", arg.Namespace, apiRes.Namespace) 443 } 444 resKeys := []keybase1.KVListEntryKey{} 445 for _, ek := range apiRes.EntryKeys { 446 k := keybase1.KVListEntryKey{EntryKey: ek.EntryKey, Revision: ek.Revision} 447 resKeys = append(resKeys, k) 448 } 449 return keybase1.KVListEntryResult{ 450 TeamName: arg.TeamName, 451 Namespace: arg.Namespace, 452 EntryKeys: resKeys, 453 }, nil 454} 455