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

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package engine
     5  
     6  import (
     7  	"os"
     8  	"testing"
     9  
    10  	"github.com/keybase/client/go/libkb"
    11  	"github.com/stretchr/testify/require"
    12  	"golang.org/x/sync/errgroup"
    13  )
    14  
    15  func forceOpenDBs(tc libkb.TestContext) {
    16  	// We need to ensure these dbs are open since we test that we can delete
    17  	// them on deprovision
    18  	err := tc.G.LocalDb.ForceOpen()
    19  	require.NoError(tc.T, err)
    20  	err = tc.G.LocalChatDb.ForceOpen()
    21  	require.NoError(tc.T, err)
    22  }
    23  
    24  func assertFileExists(t libkb.TestingTB, path string) {
    25  	if _, err := os.Stat(path); os.IsNotExist(err) {
    26  		t.Fatalf("%s unexpectedly does not exist", path)
    27  	}
    28  }
    29  
    30  func assertFileDoesNotExist(t libkb.TestingTB, path string) {
    31  	if _, err := os.Stat(path); err == nil {
    32  		t.Fatalf("%s unexpectedly exists", path)
    33  	}
    34  }
    35  
    36  func isUserInConfigFile(tc libkb.TestContext, fu FakeUser) bool {
    37  	_, err := tc.G.Env.GetConfig().GetUserConfigForUsername(fu.NormalizedUsername())
    38  	return err == nil
    39  }
    40  
    41  func isUserConfigInMemory(tc libkb.TestContext) bool {
    42  	config, _ := tc.G.Env.GetConfig().GetUserConfig()
    43  	return config != nil
    44  }
    45  
    46  func getNumKeys(tc libkb.TestContext, fu FakeUser) int {
    47  	loaded, err := libkb.LoadUser(libkb.NewLoadUserArg(tc.G).WithName(fu.Username).WithForceReload())
    48  	if err != nil {
    49  		switch err.(type) {
    50  		case libkb.NoKeyError:
    51  			return 0
    52  		default:
    53  			require.NoError(tc.T, err)
    54  		}
    55  	}
    56  	ckf := loaded.GetComputedKeyFamily()
    57  	return len(ckf.GetAllActiveSibkeys()) + len(ckf.GetAllActiveSubkeys())
    58  }
    59  
    60  type assertDeprovisionWithSetupArg struct {
    61  	// create and then revoke one extra paper key
    62  	makeAndRevokePaperKey bool
    63  
    64  	// revoke the final paper key
    65  	revokePaperKey bool
    66  }
    67  
    68  func assertDeprovisionWithSetup(tc libkb.TestContext, targ assertDeprovisionWithSetupArg) *FakeUser {
    69  	// Sign up a new user and have it store its secret in the
    70  	// secret store (if possible).
    71  	fu := NewFakeUserOrBust(tc.T, "dpr")
    72  	arg := MakeTestSignupEngineRunArg(fu)
    73  	arg.SkipPaper = false
    74  	arg.StoreSecret = tc.G.SecretStore() != nil
    75  	uis := libkb.UIs{
    76  		LogUI:    tc.G.UI.GetLogUI(),
    77  		GPGUI:    &gpgtestui{},
    78  		SecretUI: fu.NewSecretUI(),
    79  		LoginUI:  &libkb.TestLoginUI{Username: fu.Username},
    80  	}
    81  	s := NewSignupEngine(tc.G, &arg)
    82  	err := RunEngine2(NewMetaContextForTest(tc).WithUIs(uis), s)
    83  	if err != nil {
    84  		tc.T.Fatal(err)
    85  	}
    86  
    87  	m := NewMetaContextForTest(tc)
    88  	if tc.G.SecretStore() != nil {
    89  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
    90  		_, err := secretStore.RetrieveSecret(m)
    91  		if err != nil {
    92  			tc.T.Fatal(err)
    93  		}
    94  	}
    95  
    96  	forceOpenDBs(tc)
    97  	dbPath := tc.G.Env.GetDbFilename()
    98  	chatDBPath := tc.G.Env.GetChatDbFilename()
    99  	secretKeysPath := tc.G.SKBFilenameForUser(fu.NormalizedUsername())
   100  	numKeys := getNumKeys(tc, *fu)
   101  	expectedNumKeys := numKeys
   102  
   103  	assertFileExists(tc.T, dbPath)
   104  	assertFileExists(tc.T, chatDBPath)
   105  	assertFileExists(tc.T, secretKeysPath)
   106  	if !isUserInConfigFile(tc, *fu) {
   107  		tc.T.Fatalf("User %s is not in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   108  	}
   109  	if !isUserConfigInMemory(tc) {
   110  		tc.T.Fatal("user config is not in memory")
   111  	}
   112  
   113  	if !LoggedIn(tc) {
   114  		tc.T.Fatal("Unexpectedly logged out")
   115  	}
   116  
   117  	if targ.makeAndRevokePaperKey {
   118  		t := tc.T
   119  		t.Logf("generate a paper key (targ)")
   120  		uis := libkb.UIs{
   121  			LogUI:    tc.G.UI.GetLogUI(),
   122  			LoginUI:  &libkb.TestLoginUI{},
   123  			SecretUI: &libkb.TestSecretUI{},
   124  		}
   125  		eng := NewPaperKey(tc.G)
   126  		m := NewMetaContextForTest(tc).WithUIs(uis)
   127  		err := RunEngine2(m, eng)
   128  		require.NoError(t, err)
   129  		require.NotEqual(t, 0, len(eng.Passphrase()), "empty passphrase")
   130  
   131  		revokeAnyPaperKey(tc, fu)
   132  	}
   133  
   134  	if targ.revokePaperKey {
   135  		tc.T.Logf("revoking paper key (targ)")
   136  		revokeAnyPaperKey(tc, fu)
   137  		expectedNumKeys -= 2
   138  	}
   139  
   140  	e := NewDeprovisionEngine(tc.G, fu.Username, true /* doRevoke */, libkb.LogoutOptions{})
   141  	uis = libkb.UIs{
   142  		LogUI:    tc.G.UI.GetLogUI(),
   143  		SecretUI: fu.NewSecretUI(),
   144  	}
   145  	m = m.WithUIs(uis)
   146  	if err := RunEngine2(m, e); err != nil {
   147  		tc.T.Fatal(err)
   148  	}
   149  	expectedNumKeys -= 2
   150  
   151  	if LoggedIn(tc) {
   152  		tc.T.Error("Unexpectedly still logged in")
   153  	}
   154  
   155  	if tc.G.SecretStore() != nil {
   156  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
   157  		secret, err := secretStore.RetrieveSecret(m)
   158  		if err == nil {
   159  			tc.T.Errorf("Unexpectedly got secret %v", secret)
   160  		}
   161  	}
   162  
   163  	assertFileDoesNotExist(tc.T, dbPath)
   164  	assertFileDoesNotExist(tc.T, chatDBPath)
   165  	assertFileDoesNotExist(tc.T, secretKeysPath)
   166  
   167  	if isUserInConfigFile(tc, *fu) {
   168  		tc.T.Fatalf("User %s is still in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   169  	}
   170  	if isUserConfigInMemory(tc) {
   171  		tc.T.Fatal("user config is still in memory")
   172  	}
   173  
   174  	newKeys := getNumKeys(tc, *fu)
   175  	require.Equal(tc.T, expectedNumKeys, newKeys, "unexpected number of keys (failed to revoke device keys)")
   176  
   177  	return fu
   178  }
   179  
   180  func TestDeprovision(t *testing.T) {
   181  	testDeprovision(t, false)
   182  }
   183  
   184  func TestDeprovisionPUK(t *testing.T) {
   185  	testDeprovision(t, true)
   186  }
   187  
   188  func testDeprovision(t *testing.T, upgradePerUserKey bool) {
   189  	tc := SetupEngineTest(t, "deprovision")
   190  	defer tc.Cleanup()
   191  	tc.Tp.DisableUpgradePerUserKey = !upgradePerUserKey
   192  	if tc.G.SecretStore() == nil {
   193  		t.Fatal("Need a secret store for this test")
   194  	}
   195  	assertDeprovisionWithSetup(tc, assertDeprovisionWithSetupArg{})
   196  }
   197  
   198  func TestDeprovisionAfterRevokePaper(t *testing.T) {
   199  	testDeprovisionAfterRevokePaper(t, false)
   200  }
   201  
   202  func TestDeprovisionAfterRevokePaperPUK(t *testing.T) {
   203  	testDeprovisionAfterRevokePaper(t, true)
   204  }
   205  
   206  func testDeprovisionAfterRevokePaper(t *testing.T, upgradePerUserKey bool) {
   207  	tc := SetupEngineTest(t, "deprovision")
   208  	defer tc.Cleanup()
   209  
   210  	tc.Tp.DisableUpgradePerUserKey = !upgradePerUserKey
   211  	if tc.G.SecretStore() == nil {
   212  		t.Fatal("Need a secret store for this test")
   213  	}
   214  	assertDeprovisionWithSetup(tc, assertDeprovisionWithSetupArg{
   215  		makeAndRevokePaperKey: true,
   216  	})
   217  }
   218  
   219  func assertDeprovisionLoggedOut(tc libkb.TestContext) {
   220  
   221  	// Sign up a new user and have it store its secret in the
   222  	// secret store (if possible).
   223  	fu := NewFakeUserOrBust(tc.T, "dpr")
   224  	arg := MakeTestSignupEngineRunArg(fu)
   225  
   226  	arg.StoreSecret = tc.G.SecretStore() != nil
   227  	uis := libkb.UIs{
   228  		LogUI:    tc.G.UI.GetLogUI(),
   229  		GPGUI:    &gpgtestui{},
   230  		SecretUI: fu.NewSecretUI(),
   231  		LoginUI:  &libkb.TestLoginUI{Username: fu.Username},
   232  	}
   233  	s := NewSignupEngine(tc.G, &arg)
   234  	err := RunEngine2(NewMetaContextForTest(tc).WithUIs(uis), s)
   235  	if err != nil {
   236  		tc.T.Fatal(err)
   237  	}
   238  
   239  	m := NewMetaContextForTest(tc)
   240  	if tc.G.SecretStore() != nil {
   241  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
   242  		_, err := secretStore.RetrieveSecret(m)
   243  		if err != nil {
   244  			tc.T.Fatal(err)
   245  		}
   246  	}
   247  
   248  	forceOpenDBs(tc)
   249  	dbPath := tc.G.Env.GetDbFilename()
   250  	chatDBPath := tc.G.Env.GetChatDbFilename()
   251  	secretKeysPath := tc.G.SKBFilenameForUser(fu.NormalizedUsername())
   252  	numKeys := getNumKeys(tc, *fu)
   253  
   254  	assertFileExists(tc.T, dbPath)
   255  	assertFileExists(tc.T, chatDBPath)
   256  	assertFileExists(tc.T, secretKeysPath)
   257  	if !isUserInConfigFile(tc, *fu) {
   258  		tc.T.Fatalf("User %s is not in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   259  	}
   260  	if !isUserConfigInMemory(tc) {
   261  		tc.T.Fatalf("user config is not in memory")
   262  	}
   263  
   264  	if !LoggedIn(tc) {
   265  		tc.T.Fatal("Unexpectedly logged out")
   266  	}
   267  
   268  	// Unlike the first test, this time we log out before we run the
   269  	// deprovision. We should be able to do a deprovision with revocation
   270  	// disabled.
   271  	if err := m.LogoutKillSecrets(); err != nil {
   272  		tc.T.Fatal(err)
   273  	}
   274  
   275  	e := NewDeprovisionEngine(tc.G, fu.Username, false /* doRevoke */, libkb.LogoutOptions{})
   276  	uis = libkb.UIs{
   277  		LogUI:    tc.G.UI.GetLogUI(),
   278  		SecretUI: fu.NewSecretUI(),
   279  	}
   280  	m = m.WithUIs(uis)
   281  	if err := RunEngine2(m, e); err != nil {
   282  		tc.T.Fatal(err)
   283  	}
   284  
   285  	if LoggedIn(tc) {
   286  		tc.T.Error("Unexpectedly still logged in")
   287  	}
   288  
   289  	if tc.G.SecretStore() != nil {
   290  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
   291  		secret, err := secretStore.RetrieveSecret(m)
   292  		if err == nil {
   293  			tc.T.Errorf("Unexpectedly got secret %v", secret)
   294  		}
   295  	}
   296  
   297  	assertFileDoesNotExist(tc.T, dbPath)
   298  	assertFileDoesNotExist(tc.T, chatDBPath)
   299  	assertFileDoesNotExist(tc.T, secretKeysPath)
   300  	if isUserInConfigFile(tc, *fu) {
   301  		tc.T.Fatalf("User %s is still in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   302  	}
   303  	if isUserConfigInMemory(tc) {
   304  		tc.T.Fatalf("user config is still in memory")
   305  	}
   306  
   307  	newNumKeys := getNumKeys(tc, *fu)
   308  	if newNumKeys != numKeys {
   309  		tc.T.Fatalf("expected the same number of device keys, before: %d, after: %d", numKeys, newNumKeys)
   310  	}
   311  }
   312  
   313  func TestDeprovisionLoggedOut(t *testing.T) {
   314  	tc := SetupEngineTest(t, "deprovision")
   315  	defer tc.Cleanup()
   316  	if tc.G.SecretStore() == nil {
   317  		t.Fatalf("Need a secret store for this test")
   318  	}
   319  	assertDeprovisionLoggedOut(tc)
   320  }
   321  
   322  func assertCurrentDeviceRevoked(tc libkb.TestContext) {
   323  
   324  	// Sign up a new user and have it store its secret in the
   325  	// secret store (if possible).
   326  	fu := NewFakeUserOrBust(tc.T, "dpr")
   327  	arg := MakeTestSignupEngineRunArg(fu)
   328  	arg.SkipPaper = false
   329  	arg.StoreSecret = tc.G.SecretStore() != nil
   330  	uis := libkb.UIs{
   331  		LogUI:    tc.G.UI.GetLogUI(),
   332  		GPGUI:    &gpgtestui{},
   333  		SecretUI: fu.NewSecretUI(),
   334  		LoginUI:  &libkb.TestLoginUI{Username: fu.Username},
   335  	}
   336  	s := NewSignupEngine(tc.G, &arg)
   337  	err := RunEngine2(NewMetaContextForTest(tc).WithUIs(uis), s)
   338  	if err != nil {
   339  		tc.T.Fatal(err)
   340  	}
   341  
   342  	if tc.G.SecretStore() != nil {
   343  		secretStore := libkb.NewSecretStore(tc.MetaContext(), fu.NormalizedUsername())
   344  		_, err := secretStore.RetrieveSecret(NewMetaContextForTest(tc))
   345  		if err != nil {
   346  			tc.T.Fatal(err)
   347  		}
   348  	}
   349  
   350  	forceOpenDBs(tc)
   351  	dbPath := tc.G.Env.GetDbFilename()
   352  	chatDBPath := tc.G.Env.GetChatDbFilename()
   353  	secretKeysPath := tc.G.SKBFilenameForUser(fu.NormalizedUsername())
   354  	numKeys := getNumKeys(tc, *fu)
   355  
   356  	assertFileExists(tc.T, dbPath)
   357  	assertFileExists(tc.T, chatDBPath)
   358  	assertFileExists(tc.T, secretKeysPath)
   359  	if !isUserInConfigFile(tc, *fu) {
   360  		tc.T.Fatalf("User %s is not in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   361  	}
   362  	if !isUserConfigInMemory(tc) {
   363  		tc.T.Fatal("user config is not in memory")
   364  	}
   365  
   366  	if !LoggedIn(tc) {
   367  		tc.T.Fatal("Unexpectedly logged out")
   368  	}
   369  
   370  	// Revoke the current device! This will cause an error when deprovision
   371  	// tries to revoke the device again, but deprovision should carry on.
   372  	err = doRevokeDevice(tc, fu, tc.G.Env.GetDeviceID(), true /* force */, false /* forceLast */)
   373  	if err != nil {
   374  		tc.T.Fatal(err)
   375  	}
   376  
   377  	e := NewDeprovisionEngine(tc.G, fu.Username, true /* doRevoke */, libkb.LogoutOptions{})
   378  	uis = libkb.UIs{
   379  		LogUI:    tc.G.UI.GetLogUI(),
   380  		SecretUI: fu.NewSecretUI(),
   381  	}
   382  	m := NewMetaContextForTest(tc).WithUIs(uis)
   383  	if err := RunEngine2(m, e); err != nil {
   384  		tc.T.Fatal(err)
   385  	}
   386  
   387  	if LoggedIn(tc) {
   388  		tc.T.Error("Unexpectedly still logged in")
   389  	}
   390  
   391  	if tc.G.SecretStore() != nil {
   392  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
   393  		secret, err := secretStore.RetrieveSecret(NewMetaContextForTest(tc))
   394  		if err == nil {
   395  			tc.T.Errorf("Unexpectedly got secret %v", secret)
   396  		}
   397  	}
   398  
   399  	assertFileDoesNotExist(tc.T, dbPath)
   400  	assertFileDoesNotExist(tc.T, chatDBPath)
   401  	assertFileDoesNotExist(tc.T, secretKeysPath)
   402  	if isUserInConfigFile(tc, *fu) {
   403  		tc.T.Fatalf("User %s is still in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   404  	}
   405  	if isUserConfigInMemory(tc) {
   406  		tc.T.Fatal("user config is still in memory")
   407  	}
   408  
   409  	newNumKeys := getNumKeys(tc, *fu)
   410  	if newNumKeys != numKeys-2 {
   411  		tc.T.Fatalf("failed to revoke device keys, before: %d, after: %d", numKeys, newNumKeys)
   412  	}
   413  }
   414  
   415  func TestCurrentDeviceRevoked(t *testing.T) {
   416  	tc := SetupEngineTest(t, "deprovision")
   417  	defer tc.Cleanup()
   418  
   419  	if tc.G.SecretStore() == nil {
   420  		t.Fatalf("Need a secret store for this test")
   421  	}
   422  	assertCurrentDeviceRevoked(tc)
   423  }
   424  
   425  func TestDeprovisionLastDevice(t *testing.T) {
   426  	testDeprovisionLastDevice(t, false)
   427  }
   428  
   429  func TestDeprovisionLastDevicePUK(t *testing.T) {
   430  	testDeprovisionLastDevice(t, true)
   431  }
   432  
   433  // A user should be able to revoke all of their devices.
   434  func testDeprovisionLastDevice(t *testing.T, upgradePerUserKey bool) {
   435  	tc := SetupEngineTest(t, "deprovision")
   436  	defer tc.Cleanup()
   437  
   438  	tc.Tp.DisableUpgradePerUserKey = !upgradePerUserKey
   439  	if tc.G.SecretStore() == nil {
   440  		t.Fatal("Need a secret store for this test")
   441  	}
   442  	fu := assertDeprovisionWithSetup(tc, assertDeprovisionWithSetupArg{
   443  		revokePaperKey: true,
   444  	})
   445  	assertNumDevicesAndKeys(tc, fu, 0, 0)
   446  }
   447  
   448  func TestConcurrentDeprovision(t *testing.T) {
   449  	tc := SetupEngineTest(t, "deprovision-concurrent")
   450  	defer tc.Cleanup()
   451  	if tc.G.SecretStore() == nil {
   452  		t.Fatal("Need a secret store for this test")
   453  	}
   454  
   455  	// Sign up a new user and have it store its secret in the
   456  	// secret store (if possible).
   457  	fu := NewFakeUserOrBust(tc.T, "dpr")
   458  	arg := MakeTestSignupEngineRunArg(fu)
   459  	arg.SkipPaper = false
   460  	arg.StoreSecret = tc.G.SecretStore() != nil
   461  	uis := libkb.UIs{
   462  		LogUI:    tc.G.UI.GetLogUI(),
   463  		GPGUI:    &gpgtestui{},
   464  		SecretUI: fu.NewSecretUI(),
   465  		LoginUI:  &libkb.TestLoginUI{Username: fu.Username},
   466  	}
   467  	s := NewSignupEngine(tc.G, &arg)
   468  	err := RunEngine2(NewMetaContextForTest(tc).WithUIs(uis), s)
   469  	if err != nil {
   470  		tc.T.Fatal(err)
   471  	}
   472  
   473  	m := NewMetaContextForTest(tc)
   474  	if tc.G.SecretStore() != nil {
   475  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
   476  		_, err := secretStore.RetrieveSecret(m)
   477  		if err != nil {
   478  			tc.T.Fatal(err)
   479  		}
   480  	}
   481  
   482  	forceOpenDBs(tc)
   483  	dbPath := tc.G.Env.GetDbFilename()
   484  	chatDBPath := tc.G.Env.GetChatDbFilename()
   485  	secretKeysPath := tc.G.SKBFilenameForUser(fu.NormalizedUsername())
   486  	numKeys := getNumKeys(tc, *fu)
   487  	expectedNumKeys := numKeys
   488  
   489  	assertFileExists(tc.T, dbPath)
   490  	assertFileExists(tc.T, chatDBPath)
   491  	assertFileExists(tc.T, secretKeysPath)
   492  	if !isUserInConfigFile(tc, *fu) {
   493  		tc.T.Fatalf("User %s is not in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   494  	}
   495  	if !isUserConfigInMemory(tc) {
   496  		tc.T.Fatal("user config is not in memory")
   497  	}
   498  
   499  	if !LoggedIn(tc) {
   500  		tc.T.Fatal("Unexpectedly logged out")
   501  	}
   502  
   503  	g := new(errgroup.Group)
   504  	for i := 0; i < 5; i++ {
   505  		g.Go(func() error {
   506  			e := NewDeprovisionEngine(tc.G, fu.Username, false, libkb.LogoutOptions{})
   507  			uis = libkb.UIs{
   508  				LogUI:    tc.G.UI.GetLogUI(),
   509  				SecretUI: fu.NewSecretUI(),
   510  			}
   511  			m = m.WithUIs(uis)
   512  			return RunEngine2(m, e)
   513  		})
   514  	}
   515  	require.NoError(t, g.Wait())
   516  
   517  	if tc.G.SecretStore() != nil {
   518  		secretStore := libkb.NewSecretStore(m, fu.NormalizedUsername())
   519  		secret, err := secretStore.RetrieveSecret(m)
   520  		if err == nil {
   521  			tc.T.Errorf("Unexpectedly got secret %v", secret)
   522  		}
   523  	}
   524  
   525  	assertFileDoesNotExist(tc.T, dbPath)
   526  	assertFileDoesNotExist(tc.T, chatDBPath)
   527  	assertFileDoesNotExist(tc.T, secretKeysPath)
   528  
   529  	if isUserInConfigFile(tc, *fu) {
   530  		tc.T.Fatalf("User %s is still in the config file %s", fu.Username, tc.G.Env.GetConfigFilename())
   531  	}
   532  	if isUserConfigInMemory(tc) {
   533  		tc.T.Fatal("user config is still in memory")
   534  	}
   535  
   536  	newKeys := getNumKeys(tc, *fu)
   537  	require.Equal(tc.T, expectedNumKeys, newKeys, "unexpected number of keys (failed to revoke device keys)")
   538  }