1// This engine enters a user into the reset pipeline.
2package engine
3
4import (
5	"fmt"
6	"time"
7
8	"github.com/keybase/client/go/libkb"
9	"github.com/keybase/client/go/protocol/keybase1"
10)
11
12// AccountReset is an engine.
13type AccountReset struct {
14	libkb.Contextified
15	username           string
16	passphrase         string
17	skipPasswordPrompt bool
18	completeReset      bool
19
20	resetPending  bool
21	resetComplete bool
22}
23
24// NewAccountReset creates a AccountReset engine.
25func NewAccountReset(g *libkb.GlobalContext, username string) *AccountReset {
26	return &AccountReset{
27		Contextified: libkb.NewContextified(g),
28		username:     username,
29	}
30}
31
32// Name is the unique engine name.
33func (e *AccountReset) Name() string {
34	return "AccountReset"
35}
36
37// Prereqs returns the engine prereqs.
38func (e *AccountReset) Prereqs() Prereqs {
39	return Prereqs{}
40}
41
42// RequiredUIs returns the required UIs.
43func (e *AccountReset) RequiredUIs() []libkb.UIKind {
44	return []libkb.UIKind{
45		libkb.LoginUIKind,
46		libkb.SecretUIKind,
47	}
48}
49
50// SetPassphrase lets a caller add a passphrase
51func (e *AccountReset) SetPassphrase(passphrase string) {
52	e.passphrase = passphrase
53	e.skipPasswordPrompt = true
54}
55
56// SubConsumers returns the other UI consumers for this engine.
57func (e *AccountReset) SubConsumers() []libkb.UIConsumer {
58	return nil
59}
60
61// Run starts the engine.
62func (e *AccountReset) Run(mctx libkb.MetaContext) (err error) {
63	mctx = mctx.WithLogTag("RST")
64	defer mctx.Trace("Account#Run", &err)()
65
66	// User's with active devices cannot reset at all
67	if mctx.ActiveDevice().Valid() {
68		return libkb.ResetWithActiveDeviceError{}
69	}
70
71	// We can enter the reset pipeline with exactly one of these parameters
72	// set. We first attempt to establish a session for the user. Otherwise we
73	// send up a username. If the user only has an email,
74	// they should go through the "recover username" flow first.
75	var self bool
76	var username string
77
78	arg := keybase1.GUIEntryArg{
79		SubmitLabel: "Submit",
80		CancelLabel: "I don't know",
81		WindowTitle: "Keybase passphrase",
82		Type:        keybase1.PassphraseType_PASS_PHRASE,
83		Username:    e.username,
84		Prompt: fmt.Sprintf("Please enter the Keybase password for %s ("+
85			"%d+ characters) if you know it (if not, cancel this prompt)", e.username, libkb.MinPassphraseLength),
86		Features: keybase1.GUIEntryFeatures{
87			ShowTyping: keybase1.Feature{
88				Allow:        true,
89				DefaultValue: false,
90				Readonly:     true,
91				Label:        "Show typing",
92			},
93		},
94	}
95
96	// Reuse the existing login context whenever possible to prevent duplicate password prompts
97	if mctx.LoginContext() == nil {
98		mctx = mctx.WithNewProvisionalLoginContext()
99	}
100
101	if len(e.username) == 0 {
102		err = libkb.NewResetMissingParamsError("Unable to start reset process, no username provided")
103		return
104	}
105
106	if e.passphrase != "" {
107		err = libkb.PassphraseLoginNoPrompt(mctx, e.username, e.passphrase)
108		if err != nil {
109			return err
110		}
111		self = true
112	} else if !e.skipPasswordPrompt {
113		err = libkb.PassphraseLoginPromptWithArg(mctx, 3, arg)
114		switch err.(type) {
115		case nil:
116			self = true
117		case
118			// ignore these errors since we can verify the reset process from username
119			libkb.NoUIError,
120			libkb.PassphraseError,
121			libkb.RetryExhaustedError,
122			libkb.InputCanceledError,
123			libkb.SkipSecretPromptError:
124			mctx.Debug("unable to authenticate a session: %v, charging forward without it", err)
125			username = e.username
126		default:
127			return err
128		}
129	} else {
130		username = e.username
131	}
132
133	willVerifyUnverifiedState := false
134	if self {
135		status, err := e.loadResetStatus(mctx)
136		if err != nil {
137			return err
138		}
139		if status.ResetID != nil {
140			if status.EventType == libkb.AutoresetEventStart {
141				willVerifyUnverifiedState = true
142			} else {
143				return e.resetPrompt(mctx, status)
144			}
145		}
146	}
147
148	res, err := mctx.G().API.Post(mctx, libkb.APIArg{
149		Endpoint:    "autoreset/enter",
150		SessionType: libkb.APISessionTypeOPTIONAL,
151		Args: libkb.HTTPArgs{
152			"username": libkb.S{Val: username},
153			"self":     libkb.B{Val: self},
154		},
155	})
156	if err != nil {
157		return err
158	}
159	mctx.G().Log.Debug("autoreset/enter result: %s", res.Body.MarshalToDebug())
160	if willVerifyUnverifiedState {
161		// If we got here, then we supplied a correct passphrase thus verifying
162		// a pipeline that was previously in START.
163		if err := mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
164			Kind: keybase1.ResetMessage_REQUEST_VERIFIED,
165		}); err != nil {
166			return err
167		}
168	} else {
169		if self {
170			if err := mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
171				Kind: keybase1.ResetMessage_ENTERED_PASSWORDLESS,
172			}); err != nil {
173				return err
174			}
175		} else {
176			if err := mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
177				Kind: keybase1.ResetMessage_ENTERED_VERIFIED,
178			}); err != nil {
179				return err
180			}
181		}
182	}
183	e.resetPending = true
184
185	// Ask the server, so that we have the correct reset time to tell the UI
186	if self {
187		status, err := e.loadResetStatus(mctx)
188		if err != nil {
189			return err
190		}
191		if status.ResetID != nil {
192			return e.resetPrompt(mctx, status)
193		}
194	} else {
195		if err := mctx.UIs().LoginUI.DisplayResetProgress(mctx.Ctx(), keybase1.DisplayResetProgressArg{
196			Text:       "Please verify your phone number or email address to proceed with the reset.",
197			NeedVerify: true,
198		}); err != nil {
199			return err
200		}
201	}
202
203	return nil
204}
205
206type accountResetStatusResponse struct {
207	ResetID   *string `json:"reset_id"`
208	EventTime string  `json:"event_time"`
209	DelaySecs int     `json:"delay_secs"`
210	EventType int     `json:"event_type"`
211	HasWallet bool    `json:"has_wallet"`
212}
213
214func (a *accountResetStatusResponse) ReadyTime() (time.Time, error) {
215	eventTime, err := time.Parse(time.RFC3339, a.EventTime)
216	if err != nil {
217		return time.Time{}, err
218	}
219
220	return eventTime.Add(time.Second * time.Duration(a.DelaySecs)), nil
221}
222
223func (e *AccountReset) loadResetStatus(mctx libkb.MetaContext) (*accountResetStatusResponse, error) {
224	// Check the status first
225	res, err := mctx.G().API.Get(mctx, libkb.APIArg{
226		Endpoint:    "autoreset/status",
227		SessionType: libkb.APISessionTypeREQUIRED,
228	})
229	if err != nil {
230		return nil, err
231	}
232
233	parsedResponse := accountResetStatusResponse{}
234	if err := res.Body.UnmarshalAgain(&parsedResponse); err != nil {
235		return nil, err
236	}
237
238	return &parsedResponse, nil
239}
240
241func (e *AccountReset) resetPrompt(mctx libkb.MetaContext, status *accountResetStatusResponse) error {
242	if status.EventType == libkb.AutoresetEventReady {
243		// Ask the user if they'd like to reset if we're in login + it's ready
244		response, err := mctx.UIs().LoginUI.PromptResetAccount(mctx.
245			Ctx(), keybase1.PromptResetAccountArg{
246			Prompt: keybase1.NewResetPromptWithComplete(keybase1.ResetPromptInfo{HasWallet: status.HasWallet}),
247		})
248		if err != nil {
249			return err
250		}
251		if response == keybase1.ResetPromptResponse_NOTHING {
252			// noop
253			return mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
254				Kind: keybase1.ResetMessage_NOT_COMPLETED,
255			})
256		} else if response == keybase1.ResetPromptResponse_CANCEL_RESET {
257			// noop
258			if err := mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
259				Kind: keybase1.ResetMessage_CANCELED,
260			}); err != nil {
261				return err
262			}
263			return libkb.CancelResetPipeline(mctx)
264		}
265
266		arg := libkb.NewAPIArg("autoreset/reset")
267		arg.SessionType = libkb.APISessionTypeREQUIRED
268		payload := libkb.JSONPayload{
269			"src": "app",
270		}
271		arg.JSONPayload = payload
272		if _, err := mctx.G().API.Post(mctx, arg); err != nil {
273			return err
274		}
275		if err := mctx.UIs().LoginUI.DisplayResetMessage(mctx.Ctx(), keybase1.DisplayResetMessageArg{
276			Kind: keybase1.ResetMessage_COMPLETED,
277		}); err != nil {
278			return err
279		}
280
281		e.resetComplete = true
282		return nil
283	}
284
285	if status.EventType != libkb.AutoresetEventVerify {
286		// Race condition against autoresetd. We've probably just canceled or reset.
287		return nil
288	}
289
290	readyTime, err := status.ReadyTime()
291	if err != nil {
292		return err
293	}
294
295	// Notify the user how much time is left / if they can reset
296	var notificationText string
297	switch status.EventType {
298	case libkb.AutoresetEventReady:
299		notificationText = "Please log in to finish resetting your account."
300	default:
301		notificationText = fmt.Sprintf(
302			"You will be able to reset your account %s.",
303			libkb.HumanizeResetTime(readyTime),
304		)
305	}
306	if err := mctx.UIs().LoginUI.DisplayResetProgress(mctx.Ctx(), keybase1.DisplayResetProgressArg{
307		EndTime: keybase1.Time(readyTime.Unix()),
308		Text:    notificationText,
309	}); err != nil {
310		return err
311	}
312	e.resetPending = true
313	return nil
314}
315
316func (e *AccountReset) ResetPending() bool {
317	return e.resetPending
318}
319func (e *AccountReset) ResetComplete() bool {
320	return e.resetComplete
321}
322