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