github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/engine/passphrase_recover.go (about) 1 // Copyright 2019 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package engine 5 6 import ( 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. 18 type PassphraseRecover struct { 19 arg keybase1.RecoverPassphraseArg 20 libkb.Contextified 21 usernameFound bool 22 } 23 24 func 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 32 func (e *PassphraseRecover) Name() string { 33 return "PassphraseRecover" 34 } 35 36 // Prereqs returns engine prereqs 37 func (e *PassphraseRecover) Prereqs() Prereqs { 38 return Prereqs{} 39 } 40 41 // RequiredUIs returns the required UIs. 42 func (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 50 func (e *PassphraseRecover) SubConsumers() []libkb.UIConsumer { 51 return []libkb.UIConsumer{ 52 &LoginWithPaperKey{}, 53 } 54 } 55 56 // Run the engine 57 func (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 106 func (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 133 func (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 182 func (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 218 func (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 241 func (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 265 func (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 305 func (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 318 func (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 }