Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,30 @@ If you save it in a variable it can be exposed if your `ENV` is exposed somehow,
if you directly type in the password in the command line, it can end up in your
bash/zsh/whatevershell history.

### Using YubiKey for Password

You can use a YubiKey (or similar hardware token) to provide the password by
setting the `TOTP_YUBIKEY` environment variable. When enabled, the password
prompt will change to "Touch your YubiKey:", and you can use:

- **YubiKey Static Password**: Configure a YubiKey slot with a static password
- **YubiKey OTP**: Use a slot configured for Yubico OTP (press the button)
- **Challenge-Response**: Any hardware token that can output text via keyboard emulation

```shell
# Enable YubiKey mode
export TOTP_YUBIKEY=1

# Now when you run any command, it will prompt for YubiKey input
totp-cli list
```

**Important Notes:**
- The YubiKey must be configured before first use with totp-cli
- The same YubiKey configuration (same output) must be used consistently
- YubiKey Static Password or OTP slots work by emulating keyboard input
- This works with any USB security key that can output via keyboard emulation

Mostly to support CI/CD automation, there is an option to set the
password/passphrase as an environment variable. **Please use it only if you know
the system is safe to store passwords in environment variables.**
Expand Down
19 changes: 16 additions & 3 deletions internal/storage/filebackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,15 @@ func (s *FileBackend) Prepare() error {
password := os.Getenv("TOTP_PASS")

if password == "" {
if password, err = term.Hidden("Password:"); err != nil {
return BackendError{Message: err.Error()}
// Check if YubiKey mode is enabled
if os.Getenv("TOTP_YUBIKEY") == "1" || os.Getenv("TOTP_YUBIKEY") == "true" {
if password, err = term.Hidden("Touch your YubiKey:"); err != nil {
return BackendError{Message: err.Error()}
}
} else {
if password, err = term.Hidden("Password:"); err != nil {
return BackendError{Message: err.Error()}
}
}
}

Expand Down Expand Up @@ -288,7 +295,13 @@ func (s *FileBackend) initfileStorage() error {

var err error

password, err = term.Hidden("Your Password (do not forget it):")
// Check if YubiKey mode is enabled
if os.Getenv("TOTP_YUBIKEY") == "1" || os.Getenv("TOTP_YUBIKEY") == "true" {
password, err = term.Hidden("Touch your YubiKey (do not forget your configuration):")
} else {
password, err = term.Hidden("Your Password (do not forget it):")
}

if err != nil {
return BackendError{Message: err.Error()}
}
Expand Down
31 changes: 31 additions & 0 deletions internal/storage/filebackend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,34 @@ func (suite *FileBackendTestSuite) TestInvalidPassword() {
err = newStorage.Prepare()
suite.Require().ErrorIs(err, storage.BackendError{Message: "no identity matched any of the recipients"})
}

func (suite *FileBackendTestSuite) TestYubiKeyModeWithPassword() {
tmpDir, err := os.MkdirTemp("", "totp-cli-test-*")
if err != nil {
return
}
credsFilepath := path.Join(tmpDir, "credentials")

defer func() {
os.RemoveAll(tmpDir)
os.Unsetenv("TOTP_YUBIKEY")
}()

os.Setenv("TOTP_PASS", "yubikey-password")
os.Setenv("TOTP_YUBIKEY", "1")
os.Setenv("TOTP_CLI_CREDENTIAL_FILE", credsFilepath)

err = suite.storage.Prepare()
suite.Require().NoError(err)
suite.Empty(suite.storage.ListNamespaces())
suite.storage.AddNamespace(&storage.Namespace{Name: "ns1"})
suite.storage.AddNamespace(&storage.Namespace{Name: "ns2"})
suite.Len(suite.storage.ListNamespaces(), 2)
suite.storage.Save()

newStorage := storage.NewFileStorage()
newStorage.SetPassword("yubikey-password")
err = newStorage.Prepare()
suite.Require().NoError(err)
suite.Len(newStorage.ListNamespaces(), 2)
}