github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/engine/login_state_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  	"errors"
     8  	"testing"
     9  
    10  	"github.com/keybase/client/go/libkb"
    11  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    12  	"github.com/stretchr/testify/require"
    13  	"golang.org/x/net/context"
    14  )
    15  
    16  // TODO: These tests should really be in libkb/. However, any test
    17  // that creates new users have to remain in engine/ for now. Fix this.
    18  
    19  // This mock (and the similar ones below) may be used from a goroutine
    20  // different from the main one, so don't mess with testing.T (which
    21  // isn't safe to use from a non-main goroutine) directly, and instead
    22  // have a LastErr field.
    23  type GetPassphraseMock struct {
    24  	Passphrase  string
    25  	StoreSecret bool
    26  	Called      bool
    27  	LastErr     error
    28  }
    29  
    30  func (m *GetPassphraseMock) GetPassphrase(p keybase1.GUIEntryArg, terminal *keybase1.SecretEntryArg) (res keybase1.GetPassphraseRes, err error) {
    31  	if m.Called {
    32  		m.LastErr = errors.New("GetPassphrase unexpectedly called more than once")
    33  		return res, m.LastErr
    34  	}
    35  	m.Called = true
    36  	return keybase1.GetPassphraseRes{Passphrase: m.Passphrase, StoreSecret: m.StoreSecret}, nil
    37  }
    38  
    39  func (m *GetPassphraseMock) CheckLastErr(t *testing.T) {
    40  	if m.LastErr != nil {
    41  		t.Fatal(m.LastErr)
    42  	}
    43  }
    44  
    45  // Test that login works while already logged in.
    46  func TestLoginWhileAlreadyLoggedIn(t *testing.T) {
    47  	tc := SetupEngineTest(t, "login while already logged in")
    48  	defer tc.Cleanup()
    49  
    50  	// Logs the user in.
    51  	fu := CreateAndSignupFakeUser(tc, "li")
    52  
    53  	// These should all work, since the username matches.
    54  	mctx := NewMetaContextForTest(tc)
    55  
    56  	_, err := libkb.GetPassphraseStreamStored(mctx)
    57  	require.NoError(t, err, "PassphraseLoginPrompt")
    58  	mctx = mctx.WithNewProvisionalLoginContext()
    59  	err = libkb.PassphraseLoginNoPrompt(mctx, fu.Username, fu.Passphrase)
    60  	mctx = mctx.CommitProvisionalLogin()
    61  	require.NoError(t, err, "PassphraseLoginNoPrompt")
    62  	_, err = libkb.GetPassphraseStreamStored(mctx)
    63  	require.NoError(t, err, "PassphraseLoginPrompt")
    64  }
    65  
    66  // Test that login works while already logged in and after a login
    67  // state reset (via service restart).
    68  func TestLoginAfterServiceRestart(t *testing.T) {
    69  	tc := SetupEngineTest(t, "login while already logged in")
    70  	defer tc.Cleanup()
    71  
    72  	// Logs the user in.
    73  	fu := SignupFakeUserStoreSecret(tc, "li")
    74  
    75  	simulateServiceRestart(t, tc, fu)
    76  	ok, _ := isLoggedIn(NewMetaContextForTest(tc))
    77  	require.True(t, ok, "we are logged in after a service restart")
    78  }
    79  
    80  // Test that login fails with a nonexistent user.
    81  func TestLoginNonexistent(t *testing.T) {
    82  	tc := SetupEngineTest(t, "login nonexistent")
    83  	defer tc.Cleanup()
    84  
    85  	_ = CreateAndSignupFakeUser(tc, "ln")
    86  
    87  	Logout(tc)
    88  
    89  	secretUI := &libkb.TestSecretUI{Passphrase: "XXXXXXXXXXXX"}
    90  	m := NewMetaContextForTest(tc)
    91  	m = m.WithNewProvisionalLoginContext().WithUIs(libkb.UIs{SecretUI: secretUI})
    92  	err := libkb.PassphraseLoginPrompt(m, "nonexistent", 1)
    93  	if _, ok := err.(libkb.NotFoundError); !ok {
    94  		t.Errorf("error type: %T, expected libkb.NotFoundError", err)
    95  	}
    96  }
    97  
    98  type GetUsernameMock struct {
    99  	Username string
   100  	Called   bool
   101  	LastErr  error
   102  }
   103  
   104  var _ libkb.LoginUI = (*GetUsernameMock)(nil)
   105  
   106  func (m *GetUsernameMock) GetEmailOrUsername(context.Context, int) (string, error) {
   107  	if m.Called {
   108  		m.LastErr = errors.New("GetEmailOrUsername unexpectedly called more than once")
   109  		return "invalid username", m.LastErr
   110  	}
   111  	m.Called = true
   112  	return m.Username, nil
   113  }
   114  
   115  func (m *GetUsernameMock) PromptRevokePaperKeys(_ context.Context, arg keybase1.PromptRevokePaperKeysArg) (bool, error) {
   116  	return false, nil
   117  }
   118  
   119  func (m *GetUsernameMock) DisplayPaperKeyPhrase(_ context.Context, arg keybase1.DisplayPaperKeyPhraseArg) error {
   120  	return nil
   121  }
   122  
   123  func (m *GetUsernameMock) DisplayPrimaryPaperKey(_ context.Context, arg keybase1.DisplayPrimaryPaperKeyArg) error {
   124  	return nil
   125  }
   126  
   127  func (m *GetUsernameMock) PromptResetAccount(_ context.Context,
   128  	arg keybase1.PromptResetAccountArg) (keybase1.ResetPromptResponse, error) {
   129  	return keybase1.ResetPromptResponse_NOTHING, nil
   130  }
   131  
   132  func (m *GetUsernameMock) DisplayResetProgress(_ context.Context, arg keybase1.DisplayResetProgressArg) error {
   133  	return nil
   134  }
   135  
   136  func (m *GetUsernameMock) CheckLastErr(t *testing.T) {
   137  	if m.LastErr != nil {
   138  		t.Fatal(m.LastErr)
   139  	}
   140  }
   141  
   142  func (m *GetUsernameMock) ExplainDeviceRecovery(_ context.Context, arg keybase1.ExplainDeviceRecoveryArg) error {
   143  	return nil
   144  }
   145  
   146  func (m *GetUsernameMock) PromptPassphraseRecovery(_ context.Context, arg keybase1.PromptPassphraseRecoveryArg) (bool, error) {
   147  	return false, nil
   148  }
   149  
   150  func (m *GetUsernameMock) ChooseDeviceToRecoverWith(_ context.Context, arg keybase1.ChooseDeviceToRecoverWithArg) (keybase1.DeviceID, error) {
   151  	return "", nil
   152  }
   153  
   154  func (m *GetUsernameMock) DisplayResetMessage(_ context.Context, arg keybase1.DisplayResetMessageArg) error {
   155  	return nil
   156  }
   157  
   158  // Test that the login falls back to a passphrase login if pubkey
   159  // login fails.
   160  func TestLoginWithPromptPassphrase(t *testing.T) {
   161  	tc := SetupEngineTest(t, "login with prompt (passphrase)")
   162  	defer tc.Cleanup()
   163  
   164  	fu := CreateAndSignupFakeUser(tc, "lwpp")
   165  
   166  	Logout(tc)
   167  
   168  	mockGetKeybasePassphrase := &GetPassphraseMock{
   169  		Passphrase: fu.Passphrase,
   170  	}
   171  
   172  	mctx := NewMetaContextForTest(tc).WithNewProvisionalLoginContext().WithUIs(libkb.UIs{SecretUI: mockGetKeybasePassphrase})
   173  	err := libkb.PassphraseLoginPrompt(mctx, fu.Username, 1)
   174  	require.NoError(t, err, "prompt with username")
   175  	mockGetKeybasePassphrase.CheckLastErr(t)
   176  	if !mockGetKeybasePassphrase.Called {
   177  		t.Fatalf("secretUI.GetKeybasePassphrase() unexpectedly not called")
   178  	}
   179  
   180  	Logout(tc)
   181  
   182  	// Clear out the username stored in G.Env.
   183  	err = tc.G.Env.GetConfigWriter().SetUserConfig(nil, true)
   184  	require.NoError(t, err)
   185  
   186  	mockGetUsername := &GetUsernameMock{
   187  		Username: fu.Username,
   188  	}
   189  	mctx = mctx.WithNewProvisionalLoginContext().WithUIs(libkb.UIs{SecretUI: mockGetKeybasePassphrase, LoginUI: mockGetUsername})
   190  	mockGetKeybasePassphrase.Called = false
   191  	err = libkb.PassphraseLoginPrompt(mctx, "", 1)
   192  	require.NoError(t, err, "prompt with username")
   193  
   194  	mockGetUsername.CheckLastErr(t)
   195  	mockGetKeybasePassphrase.CheckLastErr(t)
   196  
   197  	if !mockGetUsername.Called {
   198  		t.Fatalf("loginUI.GetEmailOrUsername() unexpectedly not called")
   199  	}
   200  	if !mockGetKeybasePassphrase.Called {
   201  		t.Fatalf("secretUI.GetKeybasePassphrase() unexpectedly not called")
   202  	}
   203  }
   204  
   205  func userHasStoredSecretViaConfiguredAccounts(tc *libkb.TestContext, username string) bool {
   206  	configuredAccounts, err := tc.G.GetConfiguredAccounts(context.TODO())
   207  	if err != nil {
   208  		tc.T.Error(err)
   209  		return false
   210  	}
   211  
   212  	for _, configuredAccount := range configuredAccounts {
   213  		if configuredAccount.Username == username {
   214  			return configuredAccount.HasStoredSecret
   215  		}
   216  	}
   217  	return false
   218  }
   219  
   220  func userHasStoredSecretViaSecretStore(tc *libkb.TestContext, username string) bool {
   221  	secret, err := tc.G.SecretStore().RetrieveSecret(NewMetaContextForTest(*tc), libkb.NewNormalizedUsername(username))
   222  	// TODO: Have RetrieveSecret return platform-independent errors
   223  	// so that we can make sure we got the right one.
   224  	return (!secret.IsNil() && err == nil)
   225  }
   226  
   227  func userHasStoredSecret(tc *libkb.TestContext, username string) bool {
   228  	hasStoredSecret1 := userHasStoredSecretViaConfiguredAccounts(tc, username)
   229  	hasStoredSecret2 := userHasStoredSecretViaSecretStore(tc, username)
   230  	if hasStoredSecret1 != hasStoredSecret2 {
   231  		tc.T.Errorf("user %s has stored secret via configured accounts = %t, but via secret store = %t", username, hasStoredSecret1, hasStoredSecret2)
   232  	}
   233  	return hasStoredSecret1
   234  }
   235  
   236  // Test that the login flow using the secret store works.
   237  func TestLoginWithStoredSecret(t *testing.T) {
   238  
   239  	tc := SetupEngineTest(t, "login with stored secret")
   240  	defer tc.Cleanup()
   241  
   242  	fu := CreateAndSignupFakeUser(tc, "lwss")
   243  	Logout(tc)
   244  
   245  	if userHasStoredSecret(&tc, fu.Username) {
   246  		t.Errorf("User %s unexpectedly has a stored secret", fu.Username)
   247  	}
   248  
   249  	mockGetPassphrase := &GetPassphraseMock{
   250  		Passphrase:  fu.Passphrase,
   251  		StoreSecret: true,
   252  	}
   253  	mctx := NewMetaContextForTest(tc).WithNewProvisionalLoginContext().WithUIs(libkb.UIs{SecretUI: mockGetPassphrase})
   254  	err := libkb.PassphraseLoginPromptThenSecretStore(mctx, fu.Username, 1, true)
   255  	require.NoError(t, err, "no error after prompt")
   256  
   257  	mockGetPassphrase.CheckLastErr(t)
   258  
   259  	if !mockGetPassphrase.Called {
   260  		t.Errorf("secretUI.GetKeybasePassphrase() unexpectedly not called")
   261  	}
   262  
   263  	if !userHasStoredSecret(&tc, fu.Username) {
   264  		t.Errorf("User %s unexpectedly does not have a stored secret", fu.Username)
   265  	}
   266  
   267  	mctx = mctx.CommitProvisionalLogin()
   268  
   269  	clearCaches(tc.G)
   270  	ili, _ := isLoggedIn(mctx)
   271  	require.True(t, ili, "still logged in after caches are cleared (via secret store)")
   272  
   273  	Logout(tc)
   274  
   275  	if err := libkb.ClearStoredSecret(mctx, fu.NormalizedUsername()); err != nil {
   276  		t.Error(err)
   277  	}
   278  
   279  	if userHasStoredSecret(&tc, fu.Username) {
   280  		t.Errorf("User %s unexpectedly has a stored secret", fu.Username)
   281  	}
   282  
   283  	ili, _ = isLoggedIn(mctx)
   284  	require.False(t, ili, "cannot finagle a login")
   285  
   286  	_ = CreateAndSignupFakeUser(tc, "lwss")
   287  	Logout(tc)
   288  
   289  	ili, _ = isLoggedIn(mctx)
   290  	require.False(t, ili, "cannot finagle a login")
   291  }
   292  
   293  // Test that the login flow with passphrase correctly denies bad
   294  // usernames/passphrases.
   295  func TestLoginWithPassphraseErrors(t *testing.T) {
   296  	tc := SetupEngineTest(t, "login with passphrase (errors)")
   297  	defer tc.Cleanup()
   298  
   299  	fu := CreateAndSignupFakeUser(tc, "lwpe")
   300  	Logout(tc)
   301  
   302  	mctx := NewMetaContextForTest(tc).WithNewProvisionalLoginContext()
   303  	err := libkb.PassphraseLoginNoPrompt(mctx, "", "")
   304  	if _, ok := err.(libkb.AppStatusError); !ok {
   305  		t.Error("Did not get expected AppStatusError")
   306  	}
   307  	mctx = mctx.WithNewProvisionalLoginContext()
   308  	err = libkb.PassphraseLoginNoPrompt(mctx, fu.Username, fu.Passphrase+"x")
   309  	if _, ok := err.(libkb.PassphraseError); !ok {
   310  		t.Error("Did not get expected PassphraseError")
   311  	}
   312  }
   313  
   314  // Test that the login flow with passphrase but without saving the
   315  // secret works.
   316  func TestLoginWithPassphraseNoStore(t *testing.T) {
   317  
   318  	tc := SetupEngineTest(t, "login with passphrase (no store)")
   319  	defer tc.Cleanup()
   320  
   321  	fu := CreateAndSignupFakeUser(tc, "lwpns")
   322  	Logout(tc)
   323  
   324  	mctx := NewMetaContextForTest(tc).WithNewProvisionalLoginContext()
   325  	err := libkb.PassphraseLoginNoPrompt(mctx, fu.Username, fu.Passphrase)
   326  	require.NoError(t, err, "login with passphrase worked")
   327  	mctx = mctx.CommitProvisionalLogin()
   328  	require.False(t, userHasStoredSecret(&tc, fu.Username), "no stored secret")
   329  	Logout(tc)
   330  	ili, _ := isLoggedIn(mctx)
   331  	require.False(t, ili, "not logged in, since no store")
   332  	require.False(t, userHasStoredSecret(&tc, fu.Username), "no stored secret")
   333  }
   334  
   335  // TODO: Test LoginWithPassphrase with pubkey login failing.
   336  
   337  // Signup followed by logout clears the stored secret
   338  func TestSignupWithStoreThenLogout(t *testing.T) {
   339  	tc := SetupEngineTest(t, "signup with store then logout")
   340  	defer tc.Cleanup()
   341  
   342  	fu := NewFakeUserOrBust(tc.T, "lssl")
   343  
   344  	if userHasStoredSecret(&tc, fu.Username) {
   345  		t.Errorf("User %s unexpectedly has a stored secret", fu.Username)
   346  	}
   347  
   348  	arg := MakeTestSignupEngineRunArg(fu)
   349  	arg.StoreSecret = true
   350  	_ = SignupFakeUserWithArg(tc, fu, arg)
   351  
   352  	Logout(tc)
   353  
   354  	if userHasStoredSecret(&tc, fu.Username) {
   355  		t.Errorf("User %s unexpectedly has a stored secret", fu.Username)
   356  	}
   357  }
   358  
   359  type timeoutAPI struct {
   360  	*libkb.APIArgRecorder
   361  }
   362  
   363  var errFakeNetworkTimeout = errors.New("fake network timeout in test")
   364  
   365  func (r *timeoutAPI) GetDecode(mctx libkb.MetaContext, arg libkb.APIArg, w libkb.APIResponseWrapper) error {
   366  	return libkb.APINetError{Err: errFakeNetworkTimeout}
   367  }
   368  func (r *timeoutAPI) PostDecode(mctx libkb.MetaContext, arg libkb.APIArg, w libkb.APIResponseWrapper) error {
   369  	return libkb.APINetError{Err: errFakeNetworkTimeout}
   370  }
   371  
   372  func (r *timeoutAPI) Get(mctx libkb.MetaContext, arg libkb.APIArg) (*libkb.APIRes, error) {
   373  	return nil, libkb.APINetError{Err: errFakeNetworkTimeout}
   374  }
   375  
   376  // Signup followed by logout clears the stored secret
   377  func TestSignupWithStoreThenOfflineLogout(t *testing.T) {
   378  	tc := SetupEngineTest(t, "signup with store then offline logout")
   379  	defer tc.Cleanup()
   380  
   381  	fu := NewFakeUserOrBust(tc.T, "lssol")
   382  
   383  	if userHasStoredSecret(&tc, fu.Username) {
   384  		t.Errorf("User %s unexpectedly has a stored secret", fu.Username)
   385  	}
   386  
   387  	arg := MakeTestSignupEngineRunArg(fu)
   388  	arg.StoreSecret = true
   389  	_ = SignupFakeUserWithArg(tc, fu, arg)
   390  
   391  	// Hack: log out and back in so passphrase state is stored. With a real user, this would happen
   392  	// when the passphrase is set, but the passphrase is set by signup instead of manually in test.
   393  	Logout(tc)
   394  	err := fu.Login(tc.G)
   395  	require.NoError(t, err)
   396  
   397  	// Go offline
   398  	tc.G.API = &timeoutAPI{}
   399  
   400  	Logout(tc)
   401  
   402  	if userHasStoredSecret(&tc, fu.Username) {
   403  		t.Errorf("User %s unexpectedly has a stored secret", fu.Username)
   404  	}
   405  }