Fixes an issue with ContextReader on `bash` where abandoned password reads cause
the terminal to remain "locked" even after `tsh` exits. This happens on
interrupts but also on regular exit if the user picks a security key in the dual
security key/OTP login prompt.
Doesn't seem to affect shells like `zsh` or `fish`.
Repro steps:
```shell
$ bash
# repro #1
$ ./tsh login --proxy=example.com
> Enter password for Teleport user llama: <CTRL-C>
# shell is now locked
# repro #2
# (Use an account with security key and OTP MFA.)
$ ./tsh login --proxy=example.com
> Enter password for Teleport user llama: <enters password>
> Tap any security key or enter a code from a OTP device <taps security key>
# shell is now locked
```
* Restore terminal state on interrupt or exit
* Notify prompt on tsh exit
* Review: context.WithTimeout and Notify on os.Exit
Introduce the prompt.Password function and use it consistently whenever we read
a password, OTP or PIN.
The PR makes it easy to support PINs during MFA authentication, so now we do so.
It also adds the capability to mock prompt.Stdin() for tests, adding a uniform
way to fake user input in tests without having to swap functions.
Complements #10953 and #9160.
* Prompt for OTPs as passwords (take 1), read PINs on MFA authentication
* Add the prompt.Password method
* Add Stdin mocking capabilities to the prompt package
* Delegate password reads to prompt.Stdin().ReadPassword()
* Remove stdinHijack from PromptMFAChallenge
* Change api_login_test.go to FakeReader
* Change tsh_test.go to FakeReader
* Retire client.ReadPassword in favor of prompt.Password
This changes prompt.ContextReader in the following ways:
Reads only happen as a response to Read methods being called. This allows
ContextReader to coexist with other readers as long as no reads are abandoned.
ReadPassword is now available, the underlying implementation being
term.ReadPassword. An abandoned password read may be turned into a clean read.
This gives us some UX flexibility when callers abandon password reads (looking
at you, PromptMFAChallenge). Turning clean reads into password reads is not
supported. It's tricky and I have a few ideas, but it's not paramount at this
moment.
This solves the woes caused by abandoned OTP reads followed by PIN reads in
different packages, such as client.PromptMFAChallenge followed by tsh mfa add's
implementation.
#9160
* Move ContextReader to its own file
* Refactor ContextReader and implement ReadPassword
* Test ReadPassword
* Fix typos
* Remove prompt.StdinSync()
prompt.Stdin() has the same behavior for non-abandoned reads.
* Group /x/term methods under a type
Implement context-based cancellation in `/lib/utils/prompt`, for MFA
prompts.
This fixes the following scenario:
```sh
User has both OTP and U2F devices registered.
$ tsh mfa ls
Name Type Added at Last used
----- ---- ----------------------------- -----------------------------
otp TOTP Wed, 21 Apr 2021 19:41:44 UTC Wed, 21 Apr 2021 19:44:32 UTC
usb-a U2F Wed, 21 Apr 2021 19:44:34 UTC Wed, 21 Apr 2021 19:44:34 UTC
Add a new OTP device, using existing U2F device:
$ tsh mfa add
Choose device type [TOTP, U2F]: totp
Enter device name: otp2
Tap any *registered* security key or enter a code from a *registered* OTP device: <tap> # <- First OTP prompt here
Open your TOTP app and create a new manual entry with these fields:
Name: awly@localhost:3080
Issuer: Teleport
Algorithm: SHA1
Number of digits: 6
Period: 30s
Secret: 3UD42X2NN7EEZ6LUPG6NFMNOLDY6AJTS
Once created, enter an OTP code generated by the app: 607738 # <- Second OTP prompt here
MFA device "otp2" added.
```
Before this PR, the first OTP prompt (for `*registered* device`) would
hang in the background. The OTP code from the newly-registered device is
prompted later, but any text written ends up going to the first prompt.
After this PR, the first prompt is canceled and the code from a new
device goes to the second prompt as intended.
Note: this is implemented using pure Go code (background goroutine
consuming `os.Stdin`) rather than syscalls (e.g. `poll` or `select`)
for portability.