Add TOTP/2FA support for SSH keyboard-interactive auth

Automatically generate and submit TOTP codes during SSH keyboard-interactive
authentication when a secret is configured for the profile.

Backend:
- Add TOTPSecret field to SSHConfig model
- Detect TOTP/2FA prompts in keyboard-interactive handler using keyword
  matching (verification code, otp, authenticator, 2fa, etc.)
- Generate 6-digit TOTP codes using pquerna/otp library
- Retrieve TOTP secret from vault during connection (keychain or file vault)
- API: SetTOTPSecret, GetTOTPCode, HasTOTPSecret, DeleteTOTPSecret

Frontend:
- Add TOTP/2FA section to SSH profile editor (collapsed by default)
- Password-masked secret input with show/hide toggle
- Save button: encrypts and stores in vault
- Test button: generates a live 6-digit code (shown for 5 seconds)
- Delete button: removes stored secret
- Auto-detect whether secret is already saved (placeholder indicator)
- CSS for TOTP input row, danger button style
This commit is contained in:
2026-04-17 00:00:00 +01:00
parent 1463394cbb
commit 2446e037ce
8 changed files with 334 additions and 5 deletions
+15
View File
@@ -508,6 +508,21 @@ func (m *Manager) connectSSH(sess *Session, getSecret func(prompt string) (strin
getPassword := m.buildSecretCallback(profile.ID, "password", sshConf.SecretMode, getSecret)
getPassphrase := m.buildSecretCallback(profile.ID, "passphrase", sshConf.KeySecretMode, getSecret)
// Resolve TOTP secret from vault if not already inline.
if sshConf.TOTPSecret == "" {
totpKey := fmt.Sprintf("profile:%s:totp", profile.ID)
if m.keychainStore != nil && m.keychainStore.Available() {
if val, err := m.keychainStore.GetSecret(totpKey); err == nil && val != "" {
sshConf.TOTPSecret = val
}
}
if sshConf.TOTPSecret == "" && m.secretStore != nil && m.secretStore.IsUnlocked() {
if val, err := m.secretStore.GetSecret(totpKey); err == nil && val != "" {
sshConf.TOTPSecret = val
}
}
}
if err := client.Connect(getPassword, getPassphrase); err != nil {
return err
}