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