1// Copyright 2019 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package engine
5
6import (
7	"fmt"
8	"testing"
9
10	"github.com/stretchr/testify/require"
11	context "golang.org/x/net/context"
12
13	"github.com/keybase/client/go/libkb"
14	"github.com/keybase/client/go/protocol/keybase1"
15)
16
17func TestPassphraseRecoverLoggedIn(t *testing.T) {
18	tc := SetupEngineTest(t, "PassphraseRecoverGuideAndReset")
19	defer tc.Cleanup()
20	u := CreateAndSignupFakeUser(tc, "pprec")
21
22	loginUI := &TestLoginUIRecover{}
23	uis := libkb.UIs{
24		LogUI:       tc.G.UI.GetLogUI(),
25		LoginUI:     loginUI,
26		SecretUI:    u.NewSecretUI(),
27		ProvisionUI: newTestProvisionUINoSecret(),
28	}
29	m := NewMetaContextForTest(tc).WithUIs(uis)
30
31	args := []keybase1.RecoverPassphraseArg{
32		// 1) Invalid username
33		{Username: "doesntexist"},
34		// 2) No username (last configured device)
35		{},
36		// 3) Valid username
37		{Username: u.Username},
38	}
39
40	for _, arg := range args {
41		// The args don't matter - passphrase recover does not work when you
42		// are logged in.
43		err := NewPassphraseRecover(tc.G, arg).Run(m)
44		require.Error(t, err)
45		require.IsType(t, err, libkb.LoggedInError{})
46	}
47}
48
49func TestPassphraseRecoverGuideAndReset(t *testing.T) {
50	tc := SetupEngineTest(t, "PassphraseRecoverGuideAndReset")
51	defer tc.Cleanup()
52	u := CreateAndSignupFakeUser(tc, "pprec")
53	Logout(tc)
54
55	// Here we're exploring a bunch of flows where the engine will explain to
56	// the user how to change their password. Eventually we'll enter the reset
57	// pipeline.
58
59	// We're starting off with all the required UIs
60	loginUI := &TestLoginUIRecover{}
61	uis := libkb.UIs{
62		LogUI:       tc.G.UI.GetLogUI(),
63		LoginUI:     loginUI,
64		SecretUI:    u.NewSecretUI(),
65		ProvisionUI: newTestProvisionUINoSecret(),
66	}
67	m := NewMetaContextForTest(tc).WithUIs(uis)
68
69	// With autoreset enabled we don't necessarily require the device to be
70	// preconfigured with an account, so instead of "NotProvisioned" we expect
71	// a "NotFound" here.
72	arg := keybase1.RecoverPassphraseArg{
73		Username: "doesntexist",
74	}
75	require.Equal(t, libkb.NotFoundError{},
76		NewPassphraseRecover(tc.G, arg).Run(m))
77
78	// Make sure that empty username shows the correct devices
79	arg.Username = ""
80	loginUI.chooseDevice = keybase1.DeviceTypeV2_DESKTOP
81	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
82	require.Equal(t, keybase1.DeviceType_DESKTOP, loginUI.lastExplain.Kind)
83	require.Equal(t, defaultDeviceName, loginUI.lastExplain.Name)
84
85	// Expect same behaviour for an existing username
86	arg.Username = u.Username
87	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
88	require.Equal(t, keybase1.DeviceType_DESKTOP, loginUI.lastExplain.Kind)
89	require.Equal(t, defaultDeviceName, loginUI.lastExplain.Name)
90
91	// Should work even for a user that isnt configured on the device
92	tc2 := SetupEngineTest(t, "PassphraseRecoverGuideAndReset2")
93	defer tc2.Cleanup()
94	u2 := CreateAndSignupFakeUser(tc2, "pprec")
95	arg.Username = u2.Username
96	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
97	require.Equal(t, keybase1.DeviceType_DESKTOP, loginUI.lastExplain.Kind)
98	require.Equal(t, defaultDeviceName, loginUI.lastExplain.Name)
99
100	// You should be able to enter the pipeline without a password on both
101	// accounts.
102	loginUI.Reset()
103	loginUI.PassphraseRecovery = true
104	loginUI.ResetAccount = keybase1.ResetPromptResponse_CONFIRM_RESET
105	loginUI.chooseDevice = keybase1.DeviceTypeV2_NONE
106	m = NewMetaContextForTest(tc).WithUIs(uis)
107
108	arg.Username = u.Username
109	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
110	require.Nil(t, loginUI.lastExplain)
111	require.Nil(t, assertAutoreset(tc, u.UID(), libkb.AutoresetEventStart))
112
113	arg.Username = u2.Username
114	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
115	require.Nil(t, loginUI.lastExplain)
116	require.Nil(t, assertAutoreset(tc, u2.UID(), libkb.AutoresetEventStart))
117}
118
119func TestPassphraseRecoverPGPOnly(t *testing.T) {
120	tc := SetupEngineTest(t, "PassphraseRecoverPGPOnly")
121	defer tc.Cleanup()
122	u := createFakeUserWithPGPOnly(t, tc)
123
124	// If the only way to provision the account is to do it with a password,
125	// the flow should immediately go to autoreset.
126	loginUI := &TestLoginUIRecover{
127		TestLoginUI: libkb.TestLoginUI{
128			PassphraseRecovery: true,
129			ResetAccount:       keybase1.ResetPromptResponse_CONFIRM_RESET,
130		},
131	}
132	uis := libkb.UIs{
133		LogUI:       tc.G.UI.GetLogUI(),
134		LoginUI:     loginUI,
135		SecretUI:    u.NewSecretUI(),
136		ProvisionUI: newTestProvisionUINoSecret(),
137	}
138	m := NewMetaContextForTest(tc).WithUIs(uis)
139
140	arg := keybase1.RecoverPassphraseArg{
141		Username: u.Username,
142	}
143	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
144	require.Nil(t, loginUI.lastExplain)
145
146	// Should be pending verification
147	require.Nil(t, assertAutoreset(tc, u.UID(), libkb.AutoresetEventStart))
148}
149
150func TestPassphraseRecoverNoDevices(t *testing.T) {
151	tc := SetupEngineTest(t, "PassphraseRecoverNoDevices")
152	defer tc.Cleanup()
153	username, passphrase := createFakeUserWithNoKeys(tc)
154
155	// If the only way to provision the account is to do it with a password,
156	// the flow should immediately go to autoreset.
157	loginUI := &TestLoginUIRecover{
158		TestLoginUI: libkb.TestLoginUI{
159			PassphraseRecovery: true,
160			ResetAccount:       keybase1.ResetPromptResponse_CONFIRM_RESET,
161		},
162	}
163	uis := libkb.UIs{
164		LogUI:       tc.G.UI.GetLogUI(),
165		LoginUI:     loginUI,
166		SecretUI:    &libkb.TestSecretUI{Passphrase: passphrase},
167		ProvisionUI: newTestProvisionUINoSecret(),
168	}
169	m := NewMetaContextForTest(tc).WithUIs(uis)
170
171	arg := keybase1.RecoverPassphraseArg{
172		Username: username,
173	}
174	require.NoError(t, NewPassphraseRecover(tc.G, arg).Run(m))
175	require.Nil(t, loginUI.lastExplain)
176
177	// Should not be in the reset queue
178	require.Nil(t, assertAutoreset(tc, libkb.UsernameToUID(username), -1))
179}
180
181func TestPassphraseRecoverChangeWithPaper(t *testing.T) {
182	tc1 := SetupEngineTest(t, "PassphraseRecoverChangeWithPaper")
183	defer tc1.Cleanup()
184
185	// Prepare two accounts on the same device
186	u1, paperkey1 := CreateAndSignupLPK(tc1, "pprec")
187	Logout(tc1)
188	u2, paperkey2 := CreateAndSignupLPK(tc1, "pprec")
189	Logout(tc1)
190
191	// And a third one on another one
192	tc2 := SetupEngineTest(t, "PassphraseRecoverChangeWithPaper")
193	defer tc2.Cleanup()
194	u3, paperkey3 := CreateAndSignupLPK(tc2, "pprec")
195	Logout(tc2)
196
197	loginUI := &TestLoginUIRecover{}
198	uis := libkb.UIs{
199		LogUI:   tc1.G.UI.GetLogUI(),
200		LoginUI: loginUI,
201		SecretUI: &TestSecretUIRecover{
202			T:        t,
203			PaperKey: paperkey2,
204			Password: "test1234",
205		},
206		ProvisionUI: newTestProvisionUI(),
207	}
208	m := NewMetaContextForTest(tc1).WithUIs(uis)
209	arg := keybase1.RecoverPassphraseArg{}
210
211	// should work with no username passed on tc1
212	arg.Username = ""
213	loginUI.chooseDevice = keybase1.DeviceTypeV2_PAPER
214	loginUI.Username = u2.Username
215	require.NoError(t, NewPassphraseRecover(tc1.G, arg).Run(m))
216	require.NoError(t, AssertLoggedIn(tc1))
217	require.NoError(t, AssertProvisioned(tc1))
218	Logout(tc1)
219
220	// should work the same way with a username passed
221	uis.SecretUI = &TestSecretUIRecover{
222		T:        t,
223		PaperKey: paperkey1,
224		Password: "test1234",
225	}
226	m = m.WithUIs(uis)
227	arg.Username = u1.Username
228	loginUI.Username = ""
229	require.NoError(t, NewPassphraseRecover(tc1.G, arg).Run(m))
230	require.NoError(t, AssertLoggedIn(tc1))
231	require.NoError(t, AssertProvisioned(tc1))
232	Logout(tc1)
233
234	// (3) should fail
235	uis.SecretUI = &TestSecretUIRecover{
236		T:        t,
237		PaperKey: paperkey3,
238		Password: "test1234",
239	}
240	loginUI = &TestLoginUIRecover{
241		chooseDevice: keybase1.DeviceTypeV2_PAPER,
242	}
243	uis.LoginUI = loginUI
244	m = m.WithUIs(uis)
245	arg.Username = u3.Username
246
247	require.NoError(t, NewPassphraseRecover(tc1.G, arg).Run(m))
248	require.Equal(t, 1, loginUI.calledChooseDevice)
249	for _, device := range loginUI.lastDevices {
250		require.NotEqual(t, keybase1.DeviceTypeV2_PAPER, device.Type)
251	}
252	require.Error(t, AssertLoggedIn(tc1))
253	require.Error(t, AssertProvisioned(tc1))
254	require.Nil(t, assertAutoreset(tc1, u3.UID(), -1))
255}
256
257type TestSecretUIRecover struct {
258	T                  *testing.T
259	PaperKey           string
260	Password           string
261	GetPassphraseCalls int
262}
263
264func (t *TestSecretUIRecover) GetPassphrase(p keybase1.GUIEntryArg, terminal *keybase1.SecretEntryArg) (keybase1.GetPassphraseRes, error) {
265	t.GetPassphraseCalls++
266
267	switch p.Type {
268	case keybase1.PassphraseType_PAPER_KEY:
269		return keybase1.GetPassphraseRes{
270			Passphrase:  t.PaperKey,
271			StoreSecret: false,
272		}, nil
273	case keybase1.PassphraseType_PASS_PHRASE,
274		keybase1.PassphraseType_VERIFY_PASS_PHRASE:
275		return keybase1.GetPassphraseRes{
276			Passphrase:  t.Password,
277			StoreSecret: false,
278		}, nil
279	default:
280		return keybase1.GetPassphraseRes{}, fmt.Errorf("Invalid passphrase type, got %v", p.Type)
281	}
282}
283
284type TestLoginUIRecover struct {
285	libkb.TestLoginUI
286
287	calledChooseDevice int
288	chooseDevice       keybase1.DeviceTypeV2
289	lastDevices        []keybase1.Device
290
291	lastExplain *keybase1.ExplainDeviceRecoveryArg
292}
293
294func (t *TestLoginUIRecover) ExplainDeviceRecovery(_ context.Context, arg keybase1.ExplainDeviceRecoveryArg) error {
295	t.lastExplain = &arg
296	return nil
297}
298
299func (t *TestLoginUIRecover) Reset() {
300	t.lastExplain = nil
301}
302
303func (t *TestLoginUIRecover) ChooseDeviceToRecoverWith(_ context.Context, arg keybase1.ChooseDeviceToRecoverWithArg) (keybase1.DeviceID, error) {
304	t.calledChooseDevice++
305	t.lastDevices = arg.Devices
306
307	if len(arg.Devices) == 0 || t.chooseDevice == keybase1.DeviceTypeV2_NONE {
308		return "", nil
309	}
310	for _, d := range arg.Devices {
311		if d.Type == t.chooseDevice {
312			return d.DeviceID, nil
313		}
314	}
315	return "", nil
316}
317