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  }