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  }