1// Copyright 2015 Keybase, Inc. All rights reserved. Use of 2// this source code is governed by the included BSD license. 3 4package engine 5 6import ( 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. 23type GetPassphraseMock struct { 24 Passphrase string 25 StoreSecret bool 26 Called bool 27 LastErr error 28} 29 30func (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 39func (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. 46func 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). 68func 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. 81func 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 98type GetUsernameMock struct { 99 Username string 100 Called bool 101 LastErr error 102} 103 104var _ libkb.LoginUI = (*GetUsernameMock)(nil) 105 106func (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 115func (m *GetUsernameMock) PromptRevokePaperKeys(_ context.Context, arg keybase1.PromptRevokePaperKeysArg) (bool, error) { 116 return false, nil 117} 118 119func (m *GetUsernameMock) DisplayPaperKeyPhrase(_ context.Context, arg keybase1.DisplayPaperKeyPhraseArg) error { 120 return nil 121} 122 123func (m *GetUsernameMock) DisplayPrimaryPaperKey(_ context.Context, arg keybase1.DisplayPrimaryPaperKeyArg) error { 124 return nil 125} 126 127func (m *GetUsernameMock) PromptResetAccount(_ context.Context, 128 arg keybase1.PromptResetAccountArg) (keybase1.ResetPromptResponse, error) { 129 return keybase1.ResetPromptResponse_NOTHING, nil 130} 131 132func (m *GetUsernameMock) DisplayResetProgress(_ context.Context, arg keybase1.DisplayResetProgressArg) error { 133 return nil 134} 135 136func (m *GetUsernameMock) CheckLastErr(t *testing.T) { 137 if m.LastErr != nil { 138 t.Fatal(m.LastErr) 139 } 140} 141 142func (m *GetUsernameMock) ExplainDeviceRecovery(_ context.Context, arg keybase1.ExplainDeviceRecoveryArg) error { 143 return nil 144} 145 146func (m *GetUsernameMock) PromptPassphraseRecovery(_ context.Context, arg keybase1.PromptPassphraseRecoveryArg) (bool, error) { 147 return false, nil 148} 149 150func (m *GetUsernameMock) ChooseDeviceToRecoverWith(_ context.Context, arg keybase1.ChooseDeviceToRecoverWithArg) (keybase1.DeviceID, error) { 151 return "", nil 152} 153 154func (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. 160func 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 205func 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 220func 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 227func 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. 237func 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. 295func 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. 316func 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 338func 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 359type timeoutAPI struct { 360 *libkb.APIArgRecorder 361} 362 363var errFakeNetworkTimeout = errors.New("fake network timeout in test") 364 365func (r *timeoutAPI) GetDecode(mctx libkb.MetaContext, arg libkb.APIArg, w libkb.APIResponseWrapper) error { 366 return libkb.APINetError{Err: errFakeNetworkTimeout} 367} 368func (r *timeoutAPI) PostDecode(mctx libkb.MetaContext, arg libkb.APIArg, w libkb.APIResponseWrapper) error { 369 return libkb.APINetError{Err: errFakeNetworkTimeout} 370} 371 372func (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 377func 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} 406