github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/engine/account_reset.go (about) 1 // This engine enters a user into the reset pipeline. 2 package engine 3 4 import ( 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. 13 type 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. 25 func 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. 33 func (e *AccountReset) Name() string { 34 return "AccountReset" 35 } 36 37 // Prereqs returns the engine prereqs. 38 func (e *AccountReset) Prereqs() Prereqs { 39 return Prereqs{} 40 } 41 42 // RequiredUIs returns the required UIs. 43 func (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 51 func (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. 57 func (e *AccountReset) SubConsumers() []libkb.UIConsumer { 58 return nil 59 } 60 61 // Run starts the engine. 62 func (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 206 type 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 214 func (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 223 func (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 241 func (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 316 func (e *AccountReset) ResetPending() bool { 317 return e.resetPending 318 } 319 func (e *AccountReset) ResetComplete() bool { 320 return e.resetComplete 321 }