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	"sort"
9
10	"github.com/keybase/client/go/kbun"
11	"github.com/keybase/client/go/libkb"
12	keybase1 "github.com/keybase/client/go/protocol/keybase1"
13)
14
15// PassphraseRecover is an engine that implements the "password recovery" flow,
16// where the user is shown instructions on how to either change their password
17// on other devices or allows them to change the password using a paper key.
18type PassphraseRecover struct {
19	arg keybase1.RecoverPassphraseArg
20	libkb.Contextified
21	usernameFound bool
22}
23
24func NewPassphraseRecover(g *libkb.GlobalContext, arg keybase1.RecoverPassphraseArg) *PassphraseRecover {
25	return &PassphraseRecover{
26		arg:          arg,
27		Contextified: libkb.NewContextified(g),
28	}
29}
30
31// Name provides the name of the engine for the engine interface
32func (e *PassphraseRecover) Name() string {
33	return "PassphraseRecover"
34}
35
36// Prereqs returns engine prereqs
37func (e *PassphraseRecover) Prereqs() Prereqs {
38	return Prereqs{}
39}
40
41// RequiredUIs returns the required UIs.
42func (e *PassphraseRecover) RequiredUIs() []libkb.UIKind {
43	return []libkb.UIKind{
44		libkb.LoginUIKind,
45		libkb.SecretUIKind,
46	}
47}
48
49// SubConsumers requires the other UI consumers of this engine
50func (e *PassphraseRecover) SubConsumers() []libkb.UIConsumer {
51	return []libkb.UIConsumer{
52		&LoginWithPaperKey{},
53	}
54}
55
56// Run the engine
57func (e *PassphraseRecover) Run(mctx libkb.MetaContext) (err error) {
58	defer mctx.Trace("PassphraseRecover#Run", &err)()
59
60	// If no username was passed, ask for one
61	if e.arg.Username == "" {
62		res, err := mctx.UIs().LoginUI.GetEmailOrUsername(mctx.Ctx(), 0)
63		if err != nil {
64			return err
65		}
66		e.arg.Username = res
67	}
68
69	// Look up the passed username against the list of configured users
70	if err := e.processUsername(mctx); err != nil {
71		return err
72	}
73
74	// In the new flow we noop if we're already logged in
75	if loggedIn, _ := isLoggedIn(mctx); loggedIn {
76		mctx.Warning("Already logged in with unlocked device keys")
77		return libkb.LoggedInError{}
78	}
79	mctx.Debug("No device keys available, proceeding with recovery")
80
81	// Load the user by username
82	ueng := newLoginLoadUser(mctx.G(), e.arg.Username)
83	if err := RunEngine2(mctx, ueng); err != nil {
84		return err
85	}
86
87	// Now we're taking that user info and evaluating our options
88	ckf := ueng.User().GetComputedKeyFamily()
89	if ckf == nil {
90		return libkb.NewNotFoundError("Account missing key family")
91	}
92
93	// HasActiveKey rather than HasActiveDevice to handle PGP cases
94	if !ckf.HasActiveKey() {
95		// Go directly to password reset
96		return e.resetPassword(mctx)
97	}
98	if !ckf.HasActiveDevice() {
99		// No point in asking for device selection
100		return e.suggestReset(mctx)
101	}
102
103	return e.chooseDevice(mctx, ckf)
104}
105
106func (e *PassphraseRecover) processUsername(mctx libkb.MetaContext) error {
107	// Fetch usernames from user configs
108	currentUsername, otherUsernames, err := mctx.G().GetAllUserNames()
109	if err != nil {
110		return err
111	}
112	usernamesMap := map[libkb.NormalizedUsername]struct{}{
113		currentUsername: {},
114	}
115	for _, username := range otherUsernames {
116		usernamesMap[username] = struct{}{}
117	}
118
119	var normalized kbun.NormalizedUsername
120	if e.arg.Username != "" {
121		normalized = libkb.NewNormalizedUsername(e.arg.Username)
122	} else {
123		normalized = currentUsername
124	}
125	e.arg.Username = normalized.String()
126
127	// Check if the passed username is in the map
128	_, ok := usernamesMap[normalized]
129	e.usernameFound = ok
130	return nil
131}
132
133func (e *PassphraseRecover) chooseDevice(mctx libkb.MetaContext, ckf *libkb.ComputedKeyFamily) (err error) {
134	defer mctx.Trace("PassphraseRecover#chooseDevice", &err)()
135
136	// Reorder the devices for the list
137	devices := partitionDeviceList(ckf.GetAllActiveDevices())
138	sort.Sort(devices)
139
140	// Choose an existing device
141	expDevices := make([]keybase1.Device, 0, len(devices))
142	idMap := make(map[keybase1.DeviceID]libkb.DeviceWithDeviceNumber)
143	for _, d := range devices {
144		// Don't show paper keys if the user has not provisioned on this device
145		if !e.usernameFound && d.Type == keybase1.DeviceTypeV2_PAPER {
146			continue
147		}
148		expDevices = append(expDevices, *d.ProtExportWithDeviceNum())
149		idMap[d.ID] = d
150	}
151	id, err := mctx.UIs().LoginUI.ChooseDeviceToRecoverWith(mctx.Ctx(), keybase1.ChooseDeviceToRecoverWithArg{
152		Devices: expDevices,
153	})
154	if err != nil {
155		return err
156	}
157
158	// No device chosen, we're going into the reset flow
159	if len(id) == 0 {
160		// Go directly to reset
161		return e.suggestReset(mctx)
162	}
163
164	mctx.Debug("user selected device %s", id)
165	selected, ok := idMap[id]
166	if !ok {
167		return fmt.Errorf("selected device %s not in local device map", id)
168	}
169	mctx.Debug("device details: %+v", selected)
170
171	// Roughly the same flow as in provisioning
172	switch selected.Type {
173	case keybase1.DeviceTypeV2_PAPER:
174		return e.loginWithPaperKey(mctx)
175	case keybase1.DeviceTypeV2_DESKTOP, keybase1.DeviceTypeV2_MOBILE:
176		return e.explainChange(mctx, selected)
177	default:
178		return fmt.Errorf("unknown device type: %v", selected.Type)
179	}
180}
181
182func (e *PassphraseRecover) resetPassword(mctx libkb.MetaContext) (err error) {
183	enterReset, err := mctx.UIs().LoginUI.PromptResetAccount(mctx.Ctx(), keybase1.PromptResetAccountArg{
184		Prompt: keybase1.NewResetPromptDefault(keybase1.ResetPromptType_ENTER_RESET_PW),
185	})
186	if err != nil {
187		return err
188	}
189	if enterReset != keybase1.ResetPromptResponse_CONFIRM_RESET {
190		// Flow cancelled
191		return nil
192	}
193
194	// User wants a reset password email
195	res, err := mctx.G().API.Post(mctx, libkb.APIArg{
196		Endpoint:    "send-reset-pw",
197		SessionType: libkb.APISessionTypeNONE,
198		Args: libkb.HTTPArgs{
199			"email_or_username": libkb.S{Val: e.arg.Username},
200		},
201		AppStatusCodes: []int{libkb.SCOk, libkb.SCBadLoginUserNotFound},
202	})
203	if err != nil {
204		return err
205	}
206	if res.AppStatus.Code == libkb.SCBadLoginUserNotFound {
207		return libkb.NotFoundError{Msg: "User not found"}
208	}
209	// done
210	if err := mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
211		Kind: keybase1.ResetMessage_RESET_LINK_SENT,
212	}); err != nil {
213		return err
214	}
215	return nil
216}
217
218func (e *PassphraseRecover) suggestReset(mctx libkb.MetaContext) (err error) {
219	enterReset, err := mctx.UIs().LoginUI.PromptResetAccount(mctx.Ctx(), keybase1.PromptResetAccountArg{
220		Prompt: keybase1.NewResetPromptDefault(keybase1.ResetPromptType_ENTER_FORGOT_PW),
221	})
222	if err != nil {
223		return err
224	}
225	if enterReset != keybase1.ResetPromptResponse_CONFIRM_RESET {
226		// Cancel the engine as the user elected not to reset their account
227		return nil
228	}
229
230	// We are certain the user will not know their password, so we can disable that prompt.
231	eng := NewAccountReset(mctx.G(), e.arg.Username)
232	eng.skipPasswordPrompt = true
233	if err := eng.Run(mctx); err != nil {
234		return err
235	}
236
237	// We're ignoring eng.ResetPending() as we've disabled reset completion
238	return nil
239}
240
241func (e *PassphraseRecover) loginWithPaperKey(mctx libkb.MetaContext) (err error) {
242	// First log in using the paper key
243	loginEng := NewLoginWithPaperKey(mctx.G(), e.arg.Username)
244	if err := RunEngine2(mctx, loginEng); err != nil {
245		return err
246	}
247
248	if err := e.changePassword(mctx); err != nil {
249		// Log out before returning
250		if err2 := RunEngine2(mctx, NewLogout(libkb.LogoutOptions{KeepSecrets: false, Force: true})); err2 != nil {
251			mctx.Warning("Unable to log out after password change failed: %v", err2)
252		}
253
254		return err
255	}
256
257	mctx.Debug("PassphraseRecover with paper key success, sending login notification")
258	mctx.G().NotifyRouter.HandleLogin(mctx.Ctx(), e.arg.Username)
259	mctx.Debug("PassphraseRecover with paper key success, calling login hooks")
260	mctx.G().CallLoginHooks(mctx)
261
262	return nil
263}
264
265func (e *PassphraseRecover) changePassword(mctx libkb.MetaContext) (err error) {
266	// Once logged in, check if there are any server keys
267	hskEng := NewHasServerKeys(mctx.G())
268	if err := RunEngine2(mctx, hskEng); err != nil {
269		return err
270	}
271	if hskEng.GetResult().HasServerKeys {
272		// Prompt the user explaining that they'll lose server keys
273		proceed, err := mctx.UIs().LoginUI.PromptPassphraseRecovery(mctx.Ctx(), keybase1.PromptPassphraseRecoveryArg{
274			Kind: keybase1.PassphraseRecoveryPromptType_ENCRYPTED_PGP_KEYS,
275		})
276		if err != nil {
277			return err
278		}
279		if !proceed {
280			return libkb.NewCanceledError("Password recovery canceled")
281		}
282	}
283
284	// We either have no server keys or the user is OK with resetting them
285	// Prompt the user for a new passphrase.
286	passphrase, err := e.promptPassphrase(mctx)
287	if err != nil {
288		return err
289	}
290
291	// ppres.Passphrase contains our new password
292	// Run passphrase change to finish the flow
293	changeEng := NewPassphraseChange(mctx.G(), &keybase1.PassphraseChangeArg{
294		Passphrase: passphrase,
295		Force:      true,
296	})
297	if err := RunEngine2(mctx, changeEng); err != nil {
298		return err
299	}
300
301	// We have a new passphrase!
302	return nil
303}
304
305func (e *PassphraseRecover) explainChange(mctx libkb.MetaContext, device libkb.DeviceWithDeviceNumber) (err error) {
306	var name string
307	if device.Description != nil {
308		name = *device.Description
309	}
310
311	// The actual contents of the shown prompt will depend on the UI impl
312	return mctx.UIs().LoginUI.ExplainDeviceRecovery(mctx.Ctx(), keybase1.ExplainDeviceRecoveryArg{
313		Name: name,
314		Kind: device.Type.ToDeviceType(),
315	})
316}
317
318func (e *PassphraseRecover) promptPassphrase(mctx libkb.MetaContext) (string, error) {
319	arg := libkb.DefaultPassphraseArg(mctx)
320	arg.WindowTitle = "Pick a new passphrase"
321	arg.Prompt = fmt.Sprintf("Pick a new strong passphrase (%d+ characters)", libkb.MinPassphraseLength)
322	arg.Type = keybase1.PassphraseType_VERIFY_PASS_PHRASE
323
324	ppres, err := libkb.GetKeybasePassphrase(mctx, mctx.UIs().SecretUI, arg)
325	if err != nil {
326		return "", err
327	}
328	return ppres.Passphrase, nil
329}
330