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