github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/service/kvstore_test.go (about)

     1  package service
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  
    13  	"github.com/keybase/client/go/chat/signencrypt"
    14  	"github.com/keybase/client/go/kbtest"
    15  	"github.com/keybase/client/go/kvstore"
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/msgpack"
    18  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    19  	"github.com/keybase/client/go/teams"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  func kvTestSetup(t *testing.T) libkb.TestContext {
    24  	tc := libkb.SetupTest(t, "kvstore", 0)
    25  	teams.ServiceInit(tc.G)
    26  	newRevisionCache := kvstore.NewKVRevisionCache(tc.G)
    27  	tc.G.SetKVRevisionCache(newRevisionCache)
    28  	return tc
    29  }
    30  
    31  func TestKvStoreSelfTeamPutGet(t *testing.T) {
    32  	tc := kvTestSetup(t)
    33  	defer tc.Cleanup()
    34  
    35  	user, err := kbtest.CreateAndSignupFakeUser("kvs", tc.G)
    36  	require.NoError(t, err)
    37  	handler := NewKVStoreHandler(nil, tc.G)
    38  	ctx := context.Background()
    39  	teamName := fmt.Sprintf("%s,%s", user.Username, user.Username)
    40  	namespace := "ye-namespace"
    41  	entryKey := "lookmeup"
    42  
    43  	// fetch nonexistent
    44  	getArg := keybase1.GetKVEntryArg{
    45  		TeamName:  teamName,
    46  		Namespace: namespace,
    47  		EntryKey:  entryKey,
    48  	}
    49  	getRes, err := handler.GetKVEntry(ctx, getArg)
    50  	require.NoError(t, err)
    51  	require.Nil(t, getRes.EntryValue)
    52  	require.Equal(t, 0, getRes.Revision)
    53  	// and list
    54  	listNamespacesArg := keybase1.ListKVNamespacesArg{TeamName: teamName}
    55  	listNamespacesRes, err := handler.ListKVNamespaces(ctx, listNamespacesArg)
    56  	require.NoError(t, err)
    57  	require.EqualValues(t, listNamespacesRes.Namespaces, []string{})
    58  	listEntriesArg := keybase1.ListKVEntriesArg{TeamName: teamName, Namespace: namespace}
    59  	listEntriesRes, err := handler.ListKVEntries(ctx, listEntriesArg)
    60  	require.NoError(t, err)
    61  	require.EqualValues(t, []keybase1.KVListEntryKey{}, listEntriesRes.EntryKeys)
    62  
    63  	// put a secret
    64  	cleartextSecret := "lorem ipsum blah blah blah"
    65  	putArg := keybase1.PutKVEntryArg{
    66  		SessionID:  0,
    67  		TeamName:   teamName,
    68  		Namespace:  namespace,
    69  		EntryKey:   entryKey,
    70  		EntryValue: cleartextSecret,
    71  	}
    72  	putRes, err := handler.PutKVEntry(ctx, putArg)
    73  	require.NoError(t, err)
    74  	require.Equal(t, 1, putRes.Revision)
    75  
    76  	// fetch it and assert that it's now correct
    77  	getRes, err = handler.GetKVEntry(ctx, getArg)
    78  	require.NoError(t, err)
    79  	require.Equal(t, cleartextSecret, *getRes.EntryValue)
    80  
    81  	updatedSecret := `Contrary to popular belief, Lorem Ipsum is not simply
    82  		random text. It has roots in a piece of classical Latin literature
    83  		from 45 BC, making it over 2000 years old.`
    84  	putArg = keybase1.PutKVEntryArg{
    85  		SessionID:  0,
    86  		TeamName:   teamName,
    87  		Namespace:  namespace,
    88  		EntryKey:   entryKey,
    89  		EntryValue: updatedSecret,
    90  	}
    91  	putRes, err = handler.PutKVEntry(ctx, putArg)
    92  	require.NoError(t, err)
    93  	require.Equal(t, 2, putRes.Revision)
    94  	getRes, err = handler.GetKVEntry(ctx, getArg)
    95  	require.NoError(t, err)
    96  	require.Equal(t, updatedSecret, *getRes.EntryValue)
    97  
    98  	// another user cannot see or edit this entry
    99  	tcEve := kvTestSetup(t)
   100  	defer tcEve.Cleanup()
   101  	_, err = kbtest.CreateAndSignupFakeUser("kvs", tcEve.G)
   102  	require.NoError(t, err)
   103  	eveHandler := NewKVStoreHandler(nil, tcEve.G)
   104  	getRes, err = eveHandler.GetKVEntry(ctx, getArg)
   105  	require.Error(t, err)
   106  	require.Contains(t, err.Error(), "You are not a member of this team")
   107  
   108  	// put again
   109  	putRes, err = handler.PutKVEntry(ctx, putArg)
   110  	require.NoError(t, err)
   111  
   112  	// lists correctly
   113  	listNamespacesRes, err = handler.ListKVNamespaces(ctx, listNamespacesArg)
   114  	require.NoError(t, err)
   115  	require.EqualValues(t, listNamespacesRes.Namespaces, []string{namespace})
   116  
   117  	listEntriesRes, err = handler.ListKVEntries(ctx, listEntriesArg)
   118  	require.NoError(t, err)
   119  	expectedKey := keybase1.KVListEntryKey{EntryKey: entryKey, Revision: 3}
   120  	require.EqualValues(t, []keybase1.KVListEntryKey{expectedKey}, listEntriesRes.EntryKeys)
   121  }
   122  
   123  func TestKvStoreMultiUserTeam(t *testing.T) {
   124  	tcAlice := kvTestSetup(t)
   125  	defer tcAlice.Cleanup()
   126  	tcBob := kvTestSetup(t)
   127  	defer tcBob.Cleanup()
   128  	tcCharlie := kvTestSetup(t)
   129  	defer tcCharlie.Cleanup()
   130  	ctx := context.Background()
   131  
   132  	alice, err := kbtest.CreateAndSignupFakeUser("kvsA", tcAlice.G)
   133  	require.NoError(t, err)
   134  	aliceHandler := NewKVStoreHandler(nil, tcAlice.G)
   135  	bob, err := kbtest.CreateAndSignupFakeUser("kvsB", tcBob.G)
   136  	require.NoError(t, err)
   137  	bobHandler := NewKVStoreHandler(nil, tcBob.G)
   138  	charlie, err := kbtest.CreateAndSignupFakeUser("kvsB", tcCharlie.G)
   139  	require.NoError(t, err)
   140  	charlieHandler := NewKVStoreHandler(nil, tcCharlie.G)
   141  
   142  	teamName := alice.Username + "t"
   143  	teamID, err := teams.CreateRootTeam(context.Background(), tcAlice.G, teamName, keybase1.TeamSettings{})
   144  	require.NoError(t, err)
   145  	require.NotNil(t, teamID)
   146  	_, err = teams.AddMember(context.Background(), tcAlice.G, teamName, bob.Username, keybase1.TeamRole_WRITER, nil)
   147  	require.NoError(t, err)
   148  	t.Logf("%s created team %s:%s", alice.Username, teamName, teamID)
   149  
   150  	// Alice puts a secret
   151  	namespace := "myapp"
   152  	entryKey := "asdfasfeasef"
   153  	secretData := map[string]interface{}{
   154  		"username":      "hunter2",
   155  		"email":         "thereal@example.com",
   156  		"password":      "super random password",
   157  		"OTP":           "otp secret",
   158  		"twoFactorAuth": "not-really-anymore",
   159  	}
   160  	cleartextSecret, err := json.Marshal(secretData)
   161  	require.NoError(t, err)
   162  	putArg := keybase1.PutKVEntryArg{
   163  		SessionID:  0,
   164  		TeamName:   teamName,
   165  		Namespace:  namespace,
   166  		EntryKey:   entryKey,
   167  		EntryValue: string(cleartextSecret),
   168  	}
   169  	putRes, err := aliceHandler.PutKVEntry(ctx, putArg)
   170  	require.NoError(t, err)
   171  	require.Equal(t, 1, putRes.Revision)
   172  	t.Logf("alice successfully wrote an entry at revision 1")
   173  
   174  	// Bob can read it
   175  	getArg := keybase1.GetKVEntryArg{
   176  		TeamName:  teamName,
   177  		Namespace: namespace,
   178  		EntryKey:  entryKey,
   179  	}
   180  	getRes, err := bobHandler.GetKVEntry(ctx, getArg)
   181  	require.NoError(t, err)
   182  	require.Equal(t, string(cleartextSecret), *getRes.EntryValue)
   183  	require.Equal(t, 1, getRes.Revision)
   184  	listEntriesArg := keybase1.ListKVEntriesArg{TeamName: teamName, Namespace: namespace}
   185  	expectedKey := keybase1.KVListEntryKey{EntryKey: entryKey, Revision: 1}
   186  	listEntriesRes, err := bobHandler.ListKVEntries(ctx, listEntriesArg)
   187  	require.NoError(t, err)
   188  	require.EqualValues(t, listEntriesRes.EntryKeys, []keybase1.KVListEntryKey{expectedKey})
   189  	t.Logf("bob can GET and LIST it")
   190  
   191  	// Alice kicks bob out of the team.
   192  	err = teams.RemoveMember(ctx, tcAlice.G, teamName, bob.Username)
   193  	require.NoError(t, err)
   194  	err = teams.RotateKeyVisible(context.TODO(), tcAlice.G, *teamID)
   195  	require.NoError(t, err)
   196  	t.Logf("bob is no longer in the team")
   197  
   198  	// Bob cannot read the entry anymore.
   199  	getRes, err = bobHandler.GetKVEntry(ctx, getArg)
   200  	require.Error(t, err)
   201  	require.Contains(t, err.Error(), "You are not a member of this team (error 2623)")
   202  	require.IsType(t, err, libkb.AppStatusError{})
   203  	aerr, _ := err.(libkb.AppStatusError)
   204  	if aerr.Code != libkb.SCTeamReadError {
   205  		t.Fatalf("expected an SCTeamReadError error but got %v", err)
   206  	}
   207  	listNamespacesArg := keybase1.ListKVNamespacesArg{TeamName: teamName}
   208  	_, err = bobHandler.ListKVNamespaces(ctx, listNamespacesArg)
   209  	require.Error(t, err)
   210  	require.IsType(t, err, libkb.AppStatusError{})
   211  	aerr, _ = err.(libkb.AppStatusError)
   212  	if aerr.Code != libkb.SCTeamReadError {
   213  		t.Fatalf("expected an SCTeamReadError error but got %v", err)
   214  	}
   215  	t.Logf("bob can no longer GET or LIST the entry")
   216  
   217  	// New user to the team can overwrite the existing entry without specifying a revision
   218  	_, err = teams.AddMember(ctx, tcAlice.G, teamName, charlie.Username, keybase1.TeamRole_WRITER, nil)
   219  	require.NoError(t, err)
   220  	t.Logf("new user, charlie, is added to the team")
   221  	cleartextSecret = []byte("overwritten")
   222  	putArg = keybase1.PutKVEntryArg{
   223  		SessionID:  0,
   224  		TeamName:   teamName,
   225  		Namespace:  namespace,
   226  		EntryKey:   entryKey,
   227  		EntryValue: string(cleartextSecret),
   228  	}
   229  	putRes, err = charlieHandler.PutKVEntry(ctx, putArg)
   230  	require.NoError(t, err)
   231  	require.Equal(t, 2, putRes.Revision)
   232  	t.Logf("charlie can write to the entry")
   233  	getRes, err = charlieHandler.GetKVEntry(ctx, getArg)
   234  	require.NoError(t, err)
   235  	require.Equal(t, string(cleartextSecret), *getRes.EntryValue)
   236  	require.Equal(t, 2, getRes.Revision)
   237  	listNamespacesRes, err := charlieHandler.ListKVNamespaces(ctx, listNamespacesArg)
   238  	require.NoError(t, err)
   239  	require.EqualValues(t, listNamespacesRes.Namespaces, []string{namespace})
   240  	listEntriesRes, err = charlieHandler.ListKVEntries(ctx, listEntriesArg)
   241  	require.NoError(t, err)
   242  	expectedKey = keybase1.KVListEntryKey{EntryKey: entryKey, Revision: 2}
   243  	require.EqualValues(t, []keybase1.KVListEntryKey{expectedKey}, listEntriesRes.EntryKeys)
   244  	t.Logf("charlie can fetch and list the entry")
   245  }
   246  
   247  func TestKVDelete(t *testing.T) {
   248  	tc := kvTestSetup(t)
   249  	defer tc.Cleanup()
   250  	mctx := libkb.NewMetaContextForTest(tc)
   251  	ctx := context.Background()
   252  	user, err := kbtest.CreateAndSignupFakeUser("kv", tc.G)
   253  	require.NoError(t, err)
   254  	handler := NewKVStoreHandler(nil, tc.G)
   255  	teamName := user.Username + "t"
   256  	teamID, err := teams.CreateRootTeam(context.Background(), tc.G, teamName, keybase1.TeamSettings{})
   257  	require.NoError(t, err)
   258  	require.NotNil(t, teamID)
   259  
   260  	namespace := "myapp"
   261  	entryKey := "entry-key-whatever"
   262  	// delete a non-existent entry
   263  	delArg := keybase1.DelKVEntryArg{
   264  		SessionID: 0,
   265  		TeamName:  teamName,
   266  		Namespace: namespace,
   267  		EntryKey:  entryKey,
   268  	}
   269  	_, err = handler.DelKVEntry(mctx.Ctx(), delArg)
   270  	require.Error(t, err)
   271  	require.IsType(t, err, libkb.AppStatusError{})
   272  	aerr, _ := err.(libkb.AppStatusError)
   273  	if aerr.Code != libkb.SCTeamStorageNotFound {
   274  		t.Fatalf("expected an SCTeamStorageNotFound error but got %v", err)
   275  	}
   276  	t.Logf("attempting to delete a non-existent entry errors")
   277  
   278  	// create the new entry
   279  	putArg := keybase1.PutKVEntryArg{
   280  		SessionID:  0,
   281  		TeamName:   teamName,
   282  		Namespace:  namespace,
   283  		EntryKey:   entryKey,
   284  		EntryValue: "secret value",
   285  	}
   286  	putRes, err := handler.PutKVEntry(ctx, putArg)
   287  	require.NoError(t, err)
   288  	require.Equal(t, 1, putRes.Revision)
   289  
   290  	// delete it
   291  	delRes, err := handler.DelKVEntry(mctx.Ctx(), delArg)
   292  	require.NoError(t, err)
   293  	require.Equal(t, 2, delRes.Revision)
   294  
   295  	t.Logf("deleting an entry returns the next revision")
   296  	getArg := keybase1.GetKVEntryArg{
   297  		TeamName:  teamName,
   298  		Namespace: namespace,
   299  		EntryKey:  entryKey,
   300  	}
   301  	getRes, err := handler.GetKVEntry(ctx, getArg)
   302  	require.NoError(t, err)
   303  	require.Equal(t, 2, getRes.Revision)
   304  	require.Equal(t, teamName, getRes.TeamName)
   305  	require.Equal(t, namespace, getRes.Namespace)
   306  	require.Equal(t, entryKey, getRes.EntryKey)
   307  	require.Nil(t, getRes.EntryValue)
   308  	t.Logf("fetching a deleted entry has the correct revision and empty value")
   309  
   310  	// delete it again
   311  	delRes, err = handler.DelKVEntry(mctx.Ctx(), delArg)
   312  	require.Error(t, err)
   313  	require.IsType(t, err, libkb.AppStatusError{})
   314  	aerr, _ = err.(libkb.AppStatusError)
   315  	if aerr.Code != libkb.SCTeamStorageNotFound {
   316  		t.Fatalf("expected an SCTeamStorageNotFound error but got %v", err)
   317  	}
   318  	t.Logf("attempting to delete a deleted entry errors")
   319  
   320  	// recreate it after deletion
   321  	putRes, err = handler.PutKVEntry(ctx, putArg)
   322  	require.NoError(t, err)
   323  	require.Equal(t, 3, putRes.Revision)
   324  	getRes, err = handler.GetKVEntry(ctx, getArg)
   325  	require.NoError(t, err)
   326  	require.Equal(t, 3, getRes.Revision)
   327  	require.Equal(t, "secret value", *getRes.EntryValue)
   328  
   329  	// delete it again
   330  	delRes, err = handler.DelKVEntry(mctx.Ctx(), delArg)
   331  	require.NoError(t, err)
   332  	require.Equal(t, 4, delRes.Revision)
   333  }
   334  
   335  func assertRevisionError(t *testing.T, err error, expectedSource string) {
   336  	require.Error(t, err)
   337  	aerr, _ := err.(libkb.AppStatusError)
   338  	require.IsType(t, libkb.AppStatusError{}, aerr)
   339  	require.Equal(t, libkb.SCTeamStorageWrongRevision, aerr.Code)
   340  	require.Contains(t, err.Error(), "(error 2760)")
   341  	require.Contains(t, err.Error(), "revision")
   342  	switch expectedSource {
   343  	case "server":
   344  		possibleServerMessages := []string{
   345  			"expected revision [0-9]+ but got [0-9]+",
   346  			"revision [0-9]+ already exists for this entry",
   347  		}
   348  		regex := strings.Join(possibleServerMessages, "|")
   349  		require.Regexp(t, regexp.MustCompile(regex), err.Error())
   350  	case "cache":
   351  		require.Regexp(t, regexp.MustCompile("revision out of date"), err.Error())
   352  	default:
   353  		require.Fail(t, "revision error must come from the server or the cache")
   354  	}
   355  }
   356  
   357  func TestRevisionCache(t *testing.T) {
   358  	tc := kvTestSetup(t)
   359  	defer tc.Cleanup()
   360  	mctx := libkb.NewMetaContextForTest(tc)
   361  	ctx := mctx.Ctx()
   362  	user, err := kbtest.CreateAndSignupFakeUser("kv", tc.G)
   363  	require.NoError(t, err)
   364  	handler := NewKVStoreHandler(nil, tc.G)
   365  	teamName := user.Username + "t"
   366  	teamID, err := teams.CreateRootTeam(context.Background(), tc.G, teamName, keybase1.TeamSettings{})
   367  	require.NoError(t, err)
   368  	require.NotNil(t, teamID)
   369  
   370  	// create a new entry and other basic setup
   371  	namespace := "myapp"
   372  	entryKey := "messin-withtha-cache"
   373  	secretData := "supersecret"
   374  	putArg := keybase1.PutKVEntryArg{
   375  		SessionID:  0,
   376  		TeamName:   teamName,
   377  		Namespace:  namespace,
   378  		EntryKey:   entryKey,
   379  		EntryValue: secretData,
   380  	}
   381  	putRes, err := handler.PutKVEntry(ctx, putArg)
   382  	require.NoError(t, err)
   383  	require.Equal(t, 1, putRes.Revision)
   384  	entryID := keybase1.KVEntryID{
   385  		TeamID:    *teamID,
   386  		Namespace: namespace,
   387  		EntryKey:  entryKey,
   388  	}
   389  	getArg := keybase1.GetKVEntryArg{
   390  		TeamName:  teamName,
   391  		Namespace: namespace,
   392  		EntryKey:  entryKey,
   393  	}
   394  
   395  	// Mutate the revision cache to simulate the cases where the server
   396  	// is lying to the client about the next fetched entry. First, fetch
   397  	// and assert some basic stuff about the revision cache.
   398  	kvRevCache := tc.G.GetKVRevisionCache().(*kvstore.KVRevisionCache)
   399  	entryHash, generation, revision := kvRevCache.Inspect(entryID)
   400  	require.NotEmpty(t, entryHash)
   401  	require.EqualValues(t, 1, generation)
   402  	require.Equal(t, 1, revision)
   403  
   404  	// bump the revision in a new cache and verify error when going through the handler
   405  	tc.G.SetKVRevisionCache(kvstore.NewKVRevisionCache(tc.G))
   406  	revCache := tc.G.GetKVRevisionCache()
   407  	err = revCache.Check(mctx, entryID, &secretData, generation, 2)
   408  	require.NoError(t, err)
   409  	err = revCache.Put(mctx, entryID, &secretData, generation, 2)
   410  	require.NoError(t, err)
   411  	_, err = handler.GetKVEntry(ctx, getArg)
   412  	require.Error(t, err)
   413  	require.Contains(t, err.Error(), "revision")
   414  
   415  	// bump the team key generation in a new cache and verify error
   416  	tc.G.SetKVRevisionCache(kvstore.NewKVRevisionCache(tc.G))
   417  	revCache = tc.G.GetKVRevisionCache()
   418  	err = revCache.Check(mctx, entryID, &secretData, keybase1.PerTeamKeyGeneration(2), revision)
   419  	require.NoError(t, err)
   420  	err = revCache.Put(mctx, entryID, &secretData, keybase1.PerTeamKeyGeneration(2), revision)
   421  	require.NoError(t, err)
   422  	_, err = handler.GetKVEntry(ctx, getArg)
   423  	require.Error(t, err)
   424  	require.Contains(t, err.Error(), "team key generation")
   425  
   426  	// mutate the entry hash and verify error
   427  	tc.G.SetKVRevisionCache(kvstore.NewKVRevisionCache(tc.G))
   428  	revCache = tc.G.GetKVRevisionCache()
   429  	differentCiphertext := "this-is-wrong"
   430  	err = revCache.Check(mctx, entryID, &differentCiphertext, generation, revision)
   431  	require.NoError(t, err)
   432  	err = revCache.Put(mctx, entryID, &differentCiphertext, generation, revision)
   433  	require.NoError(t, err)
   434  	_, err = handler.GetKVEntry(ctx, getArg)
   435  	require.Error(t, err)
   436  	require.Contains(t, err.Error(), "hash of entry")
   437  	t.Logf("revision cache protects the client from the server lying in fetches")
   438  
   439  	// convenience checks for PUT and DELETE
   440  	// set a new cache and put an update
   441  	tc.G.SetKVRevisionCache(kvstore.NewKVRevisionCache(tc.G))
   442  	putArg = keybase1.PutKVEntryArg{
   443  		SessionID:  0,
   444  		TeamName:   teamName,
   445  		Namespace:  namespace,
   446  		EntryKey:   entryKey,
   447  		EntryValue: secretData,
   448  		Revision:   2,
   449  	}
   450  	putRes, err = handler.PutKVEntry(ctx, putArg)
   451  	require.NoError(t, err)
   452  	require.Equal(t, 2, putRes.Revision)
   453  	// assert the revision cache is what we think
   454  	kvRevCache = tc.G.GetKVRevisionCache().(*kvstore.KVRevisionCache)
   455  	entryHash, generation, revision = kvRevCache.Inspect(entryID)
   456  	require.NotEmpty(t, entryHash)
   457  	require.EqualValues(t, 1, generation)
   458  	require.Equal(t, 2, revision)
   459  
   460  	// attempt a put with a revision that the cache knows is too low
   461  	putArg.Revision = 2
   462  	_, err = handler.PutKVEntry(ctx, putArg)
   463  	assertRevisionError(t, err, "cache")
   464  	// attempt a put with a revision that's too high, but the cache can't know that (so it's a server error)
   465  	putArg.Revision = 4
   466  	_, err = handler.PutKVEntry(ctx, putArg)
   467  	assertRevisionError(t, err, "server")
   468  	t.Logf("revision cache provides convenience checks (the server also catches these things too) on updates")
   469  
   470  	// and the same for deletes
   471  	delArg := keybase1.DelKVEntryArg{
   472  		SessionID: 0,
   473  		TeamName:  teamName,
   474  		Namespace: namespace,
   475  		EntryKey:  entryKey,
   476  	}
   477  	delArg.Revision = 2
   478  	_, err = handler.DelKVEntry(ctx, delArg)
   479  	assertRevisionError(t, err, "cache")
   480  	delArg.Revision = 4
   481  	_, err = handler.DelKVEntry(ctx, delArg)
   482  	assertRevisionError(t, err, "server")
   483  	t.Logf("revision cache also provides the convenience checks for deletes")
   484  }
   485  
   486  var _ kvstore.KVStoreBoxer = (*KVStoreTestBoxer)(nil)
   487  
   488  type KVStoreTestBoxer struct {
   489  	libkb.Contextified
   490  	BoxMutateRevision     func(currentRevision int) (newRevision int)
   491  	UnboxMutateTeamGen    func(gen keybase1.PerTeamKeyGeneration) (newGen keybase1.PerTeamKeyGeneration)
   492  	UnboxMutateCiphertext func(ciphertext string) string
   493  }
   494  
   495  func (b *KVStoreTestBoxer) Box(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, cleartextValue string) (ciphertext string,
   496  	teamKeyGen keybase1.PerTeamKeyGeneration, ciphertextVersion int, err error) {
   497  	realBoxer := kvstore.NewKVStoreBoxer(mctx.G())
   498  	if b.BoxMutateRevision != nil {
   499  		revision = b.BoxMutateRevision(revision)
   500  	}
   501  	return realBoxer.Box(mctx, entryID, revision, cleartextValue)
   502  }
   503  
   504  func (b *KVStoreTestBoxer) Unbox(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, ciphertext string, teamKeyGen keybase1.PerTeamKeyGeneration,
   505  	formatVersion int, senderUID keybase1.UID, senderEldestSeqno keybase1.Seqno, senderDeviceID keybase1.DeviceID) (cleartext string, err error) {
   506  	realBoxer := kvstore.NewKVStoreBoxer(mctx.G())
   507  	if b.UnboxMutateTeamGen != nil {
   508  		teamKeyGen = b.UnboxMutateTeamGen(teamKeyGen)
   509  	}
   510  	if b.UnboxMutateCiphertext != nil {
   511  		ciphertext = b.UnboxMutateCiphertext(ciphertext)
   512  	}
   513  	return realBoxer.Unbox(mctx, entryID, revision, ciphertext, teamKeyGen, formatVersion, senderUID, senderEldestSeqno, senderDeviceID)
   514  }
   515  
   516  func TestKVEncryptionAndVerification(t *testing.T) {
   517  	ctx := context.TODO()
   518  	tc := kvTestSetup(t)
   519  	defer tc.Cleanup()
   520  	user, err := kbtest.CreateAndSignupFakeUser("kvs", tc.G)
   521  	require.NoError(t, err)
   522  	handler := NewKVStoreHandler(nil, tc.G)
   523  	// inject a test Boxer into the handler which, at this point,
   524  	// is just a passthrough to the real Boxer, but ensure that
   525  	// any expected errors later are not false negatives.
   526  	handler.Boxer = &KVStoreTestBoxer{
   527  		Contextified: libkb.NewContextified(tc.G),
   528  	}
   529  	teamName := user.Username + "t"
   530  	teamID, err := teams.CreateRootTeam(context.Background(), tc.G, teamName, keybase1.TeamSettings{})
   531  	require.NoError(t, err)
   532  	require.NotNil(t, teamID)
   533  	err = teams.RotateKeyVisible(context.TODO(), tc.G, *teamID)
   534  	require.NoError(t, err)
   535  	err = teams.RotateKeyVisible(context.TODO(), tc.G, *teamID)
   536  	require.NoError(t, err)
   537  	namespace := "myapp"
   538  	entryKey := "entry-key-entry-key"
   539  	secretData := "supersecret"
   540  	putArg := keybase1.PutKVEntryArg{
   541  		SessionID:  0,
   542  		TeamName:   teamName,
   543  		Namespace:  namespace,
   544  		EntryKey:   entryKey,
   545  		EntryValue: secretData,
   546  	}
   547  	_, err = handler.PutKVEntry(ctx, putArg)
   548  	require.NoError(t, err)
   549  	_, err = handler.PutKVEntry(ctx, putArg)
   550  	require.NoError(t, err)
   551  	putRes, err := handler.PutKVEntry(ctx, putArg)
   552  	require.NoError(t, err)
   553  	require.Equal(t, 3, putRes.Revision)
   554  	getArg := keybase1.GetKVEntryArg{
   555  		TeamName:  teamName,
   556  		Namespace: namespace,
   557  		EntryKey:  entryKey,
   558  	}
   559  	getRes, err := handler.GetKVEntry(ctx, getArg)
   560  	require.NoError(t, err)
   561  	require.Equal(t, secretData, *getRes.EntryValue)
   562  	require.Equal(t, 3, getRes.Revision)
   563  	t.Logf("entry exists for team at revision 3 and key generation 3")
   564  
   565  	// attempt to decrypt with the wrong team key generation
   566  	// should fail to decrypt
   567  	handler.Boxer = &KVStoreTestBoxer{
   568  		Contextified: libkb.NewContextified(tc.G),
   569  		UnboxMutateTeamGen: func(gen keybase1.PerTeamKeyGeneration) (newGen keybase1.PerTeamKeyGeneration) {
   570  			require.EqualValues(t, 3, gen, "generation should be 3 at this point")
   571  			return keybase1.PerTeamKeyGeneration(2)
   572  		},
   573  	}
   574  	_, err = handler.GetKVEntry(ctx, getArg)
   575  	require.Error(t, err)
   576  	require.IsType(t, signencrypt.Error{}, err)
   577  	require.Equal(t, err.(signencrypt.Error).Type, signencrypt.BadSecretbox)
   578  	t.Logf("attempting to decrypt with the wrong key generation fails")
   579  
   580  	// should fail to open if the server tells the client it's the wrong revision
   581  	handler.Boxer = &KVStoreTestBoxer{
   582  		Contextified:      libkb.NewContextified(tc.G),
   583  		BoxMutateRevision: func(currentRevision int) (newRevision int) { return 2 },
   584  	}
   585  	putRes, err = handler.PutKVEntry(ctx, putArg)
   586  	require.NoError(t, err)
   587  	_, err = handler.GetKVEntry(ctx, getArg)
   588  	require.Error(t, err)
   589  	require.IsType(t, signencrypt.Error{}, err)
   590  	require.Equal(t, err.(signencrypt.Error).Type, signencrypt.AssociatedDataMismatch)
   591  	t.Logf("verifying a signature with the wrong revision fails")
   592  
   593  	// should error if given the wrong nonce
   594  	handler.Boxer = &KVStoreTestBoxer{
   595  		Contextified: libkb.NewContextified(tc.G),
   596  		UnboxMutateCiphertext: func(currentCiphertext string) (newCiphertext string) {
   597  			decoded, err := base64.StdEncoding.DecodeString(currentCiphertext)
   598  			require.NoError(t, err)
   599  			var box keybase1.EncryptedKVEntry
   600  			err = msgpack.Decode(&box, decoded)
   601  			require.NoError(t, err)
   602  			require.Equal(t, len(box.N), 16, "there is an actual 16 byte nonce")
   603  			randBytes, err := libkb.RandBytes(16)
   604  			require.NoError(t, err)
   605  			box.N = randBytes
   606  			packed, err := msgpack.Encode(box)
   607  			require.NoError(t, err)
   608  			return base64.StdEncoding.EncodeToString(packed)
   609  		},
   610  	}
   611  	_, err = handler.GetKVEntry(ctx, getArg)
   612  	require.Error(t, err)
   613  	require.IsType(t, signencrypt.Error{}, err)
   614  	require.Equal(t, err.(signencrypt.Error).Type, signencrypt.BadSecretbox)
   615  	t.Logf("cannot decrypt with the wrong nonce")
   616  	// switch to a new, non-broken entry key to test that the nonce changes
   617  	putArg.EntryKey = "not-broken"
   618  	getArg.EntryKey = "not-broken"
   619  	var firstNonce, secondNonce [16]byte
   620  	handler.Boxer = &KVStoreTestBoxer{
   621  		Contextified: libkb.NewContextified(tc.G),
   622  		UnboxMutateCiphertext: func(currentCiphertext string) (newCiphertext string) {
   623  			decoded, err := base64.StdEncoding.DecodeString(currentCiphertext)
   624  			require.NoError(t, err)
   625  			var box keybase1.EncryptedKVEntry
   626  			err = msgpack.Decode(&box, decoded)
   627  			require.NoError(t, err)
   628  			require.Equal(t, len(box.N), 16, "there is an actual 16 byte nonce")
   629  			copy(firstNonce[:], box.N)
   630  			return currentCiphertext
   631  		},
   632  	}
   633  	_, err = handler.PutKVEntry(ctx, putArg)
   634  	require.NoError(t, err)
   635  	_, err = handler.GetKVEntry(ctx, getArg)
   636  	require.NoError(t, err)
   637  	require.Equal(t, len(firstNonce), 16, "firstNonce got populated")
   638  	handler.Boxer = &KVStoreTestBoxer{
   639  		Contextified: libkb.NewContextified(tc.G),
   640  		UnboxMutateCiphertext: func(ciphertext string) string {
   641  			decoded, err := base64.StdEncoding.DecodeString(ciphertext)
   642  			require.NoError(t, err)
   643  			var box keybase1.EncryptedKVEntry
   644  			err = msgpack.Decode(&box, decoded)
   645  			require.NoError(t, err)
   646  			require.Equal(t, len(box.N), 16, "there is an actual 16 byte nonce")
   647  			t.Logf("the nonce is 16 bytes")
   648  			copy(secondNonce[:], box.N)
   649  			return ciphertext
   650  		},
   651  	}
   652  	_, err = handler.PutKVEntry(ctx, putArg)
   653  	require.NoError(t, err)
   654  	_, err = handler.GetKVEntry(ctx, getArg)
   655  	require.NoError(t, err)
   656  	require.Equal(t, len(secondNonce), 16, "secondNonce got populated")
   657  	require.NotEqual(t, firstNonce, secondNonce)
   658  	t.Logf("two puts with identical data and keys use different nonces")
   659  }
   660  
   661  // TestManualControlOfRevisionWithoutCache tests the server erroring correctly
   662  // when fed the wrong revision, and that error bubbling up. This requires resetting
   663  // the cache before each request.
   664  func TestManualControlOfRevisionWithoutCache(t *testing.T) {
   665  	tc := kvTestSetup(t)
   666  	defer tc.Cleanup()
   667  	mctx := libkb.NewMetaContextForTest(tc)
   668  	ctx := mctx.Ctx()
   669  	user, err := kbtest.CreateAndSignupFakeUser("kv", tc.G)
   670  	require.NoError(t, err)
   671  	handler := NewKVStoreHandler(nil, tc.G)
   672  	teamName := user.Username + "t"
   673  	teamID, err := teams.CreateRootTeam(context.Background(), tc.G, teamName, keybase1.TeamSettings{})
   674  	require.NoError(t, err)
   675  	require.NotNil(t, teamID)
   676  
   677  	// create a new entry and other basic setup
   678  	namespace := "manually-controlled-namespace"
   679  	entryKey := "ye-new-key"
   680  	secretData := "supersecret"
   681  	basePutArg := keybase1.PutKVEntryArg{
   682  		SessionID:  0,
   683  		TeamName:   teamName,
   684  		Namespace:  namespace,
   685  		EntryKey:   entryKey,
   686  		EntryValue: secretData,
   687  	}
   688  
   689  	putItWithRev := func(revision int) (res keybase1.KVPutResult, err error) {
   690  		// reset the cache before each PUT to avoid additional errors from there
   691  		tc.G.SetKVRevisionCache(kvstore.NewKVRevisionCache(tc.G))
   692  		putArg := basePutArg
   693  		putArg.Revision = revision
   694  		return handler.PutKVEntry(ctx, putArg)
   695  	}
   696  
   697  	// errors if you specify a Revision > 1 for a new entry
   698  	_, err = putItWithRev(2)
   699  	assertRevisionError(t, err, "server")
   700  
   701  	// create it with revision 1
   702  	putRes, err := putItWithRev(1)
   703  	require.NoError(t, err)
   704  	require.Equal(t, putRes.Revision, 1)
   705  
   706  	// cannot update it again with revision 1
   707  	_, err = putItWithRev(1)
   708  	assertRevisionError(t, err, "server")
   709  
   710  	// updates correctly
   711  	putRes, err = putItWithRev(2)
   712  	require.NoError(t, err)
   713  	require.Equal(t, putRes.Revision, 2)
   714  
   715  	// cannot update with a revision that's too high
   716  	_, err = putItWithRev(4)
   717  	assertRevisionError(t, err, "server")
   718  
   719  	// cannot update with a revision that's too low
   720  	_, err = putItWithRev(2)
   721  	assertRevisionError(t, err, "server")
   722  
   723  	baseDelArg := keybase1.DelKVEntryArg{
   724  		SessionID: 0,
   725  		TeamName:  teamName,
   726  		Namespace: namespace,
   727  		EntryKey:  entryKey,
   728  	}
   729  
   730  	deleteItWithRev := func(revision int) (res keybase1.KVDeleteEntryResult, err error) {
   731  		// reset the cache before each request to avoid additional errors from there
   732  		tc.G.SetKVRevisionCache(kvstore.NewKVRevisionCache(tc.G))
   733  		delArg := baseDelArg
   734  		delArg.Revision = revision
   735  		return handler.DelKVEntry(ctx, delArg)
   736  	}
   737  
   738  	// cannot delete with a revision that's too low
   739  	_, err = deleteItWithRev(2)
   740  	assertRevisionError(t, err, "server")
   741  
   742  	// cannot delete with a revision that's too high
   743  	_, err = deleteItWithRev(4)
   744  	assertRevisionError(t, err, "server")
   745  
   746  	// deletes correctly
   747  	delRes, err := deleteItWithRev(3)
   748  	require.NoError(t, err)
   749  	require.Equal(t, delRes.Revision, 3)
   750  }
   751  
   752  // TestKVStoreRace tests that multiple requests by multiple users in rapid succession do not
   753  // cause unexpected errors or busted caches
   754  func TestKVStoreRace(t *testing.T) {
   755  	tc1 := kvTestSetup(t)
   756  	defer tc1.Cleanup()
   757  	mctx1 := libkb.NewMetaContextForTest(tc1)
   758  	ctx1 := mctx1.Ctx()
   759  	user1, err := kbtest.CreateAndSignupFakeUser("kv", tc1.G)
   760  	require.NoError(t, err)
   761  	handler1 := NewKVStoreHandler(nil, tc1.G)
   762  	teamName := user1.Username + "t"
   763  	teamID, err := teams.CreateRootTeam(ctx1, tc1.G, teamName, keybase1.TeamSettings{})
   764  	require.NoError(t, err)
   765  	require.NotNil(t, teamID)
   766  	// add a second user to the team
   767  	tc2 := kvTestSetup(t)
   768  	defer tc2.Cleanup()
   769  	mctx2 := libkb.NewMetaContextForTest(tc2)
   770  	ctx2 := mctx2.Ctx()
   771  	user2, err := kbtest.CreateAndSignupFakeUser("kv", tc2.G)
   772  	require.NoError(t, err)
   773  	handler2 := NewKVStoreHandler(nil, tc2.G)
   774  	_, err = teams.AddMember(ctx1, tc1.G, teamName, user2.Username, keybase1.TeamRole_WRITER, nil)
   775  	require.NoError(t, err)
   776  
   777  	namespace := "race-namespace"
   778  	entryKey := "race-key"
   779  	secretData := "supersecret"
   780  	putArg := keybase1.PutKVEntryArg{
   781  		SessionID:  0,
   782  		TeamName:   teamName,
   783  		Namespace:  namespace,
   784  		EntryKey:   entryKey,
   785  		EntryValue: secretData,
   786  	}
   787  	var wg sync.WaitGroup
   788  	errChan := make(chan error, 10)
   789  	for i := 0; i < 5; i++ {
   790  		wg.Add(1)
   791  		go func() {
   792  			defer wg.Done()
   793  			_, err = handler1.PutKVEntry(ctx1, putArg)
   794  			errChan <- err
   795  		}()
   796  		wg.Add(1)
   797  		go func() {
   798  			defer wg.Done()
   799  			_, err = handler2.PutKVEntry(ctx2, putArg)
   800  			errChan <- err
   801  		}()
   802  	}
   803  	wg.Wait()
   804  	close(errChan)
   805  	for err := range errChan {
   806  		// any errors should be server-thrown revision errors
   807  		if err != nil {
   808  			assertRevisionError(t, err, "server")
   809  		}
   810  	}
   811  
   812  	// and now a fetch should work from both users, confirming that neither
   813  	// has a busted revision cache
   814  	getArg := keybase1.GetKVEntryArg{
   815  		TeamName:  teamName,
   816  		Namespace: namespace,
   817  		EntryKey:  entryKey,
   818  	}
   819  	getRes, err := handler1.GetKVEntry(ctx1, getArg)
   820  	require.NoError(t, err)
   821  	require.Equal(t, secretData, *getRes.EntryValue)
   822  	getRes, err = handler2.GetKVEntry(ctx2, getArg)
   823  	require.NoError(t, err)
   824  	require.Equal(t, secretData, *getRes.EntryValue)
   825  }