github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/service/kvstore.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  // RPC handlers for kvstore operations
     5  
     6  package service
     7  
     8  import (
     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  
    21  type KVStoreHandler struct {
    22  	*BaseHandler
    23  	sync.Mutex
    24  	libkb.Contextified
    25  	Boxer kvstore.KVStoreBoxer
    26  }
    27  
    28  var _ keybase1.KvstoreInterface = (*KVStoreHandler)(nil)
    29  
    30  func 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  
    41  func (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  
    62  type 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  
    76  func (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  
   104  func (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  
   110  func (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  
   165  type putEntryAPIRes struct {
   166  	libkb.AppStatusEmbed
   167  	Revision int `json:"revision"`
   168  }
   169  
   170  func (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  
   176  func (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  
   260  func (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  
   266  func (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  
   346  type getListNamespacesAPIRes struct {
   347  	libkb.AppStatusEmbed
   348  	TeamID     keybase1.TeamID `json:"team_id"`
   349  	Namespaces []string        `json:"namespaces"`
   350  }
   351  
   352  func (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  
   358  func (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  
   393  type 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  
   400  type compressedEntryKey struct {
   401  	EntryKey string `json:"k"`
   402  	Revision int    `json:"r"`
   403  }
   404  
   405  func (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  
   411  func (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  }