Skip to content

Commit cdc8b4f

Browse files
fix(ssh): handle encrypted key passphrase prompts when editing comments
- Add TUI suspension for encrypted SSH keys to allow passphrase input - Connect stdin/stdout/stderr to ssh-keygen command for interactive prompts - Improve encryption detection by checking for "bcrypt" cipher indicator - Propagate encryption and public key status when merging agent keys - Update help text formatting in server details view - Change warning icon for missing public keys
1 parent 1436c04 commit cdc8b4f

File tree

4 files changed

+57
-16
lines changed

4 files changed

+57
-16
lines changed

internal/adapters/ui/handlers.go

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,14 +1016,29 @@ func (t *tui) handleEditKeyComment() {
10161016
editor := NewEditKeyComment(t.app).
10171017
SetKey(key.Name, key.Path, key.Comment).
10181018
OnSave(func(newComment string) {
1019-
// Update the key comment using gitService
1020-
if err := t.gitService.UpdateKeyComment(key.Path, newComment); err != nil {
1021-
t.storeErrorf("[Edit SSH Key Comment] Failed to update comment for '%s': %v", key.Name, err)
1022-
t.showStatusTempColor(fmt.Sprintf("Failed to update comment: %v", err), "#FF6B6B")
1023-
} else {
1024-
t.showStatusTemp("SSH key comment updated successfully")
1025-
// Refresh the SSH keys list to show the updated comment
1019+
// For encrypted keys, suspend TUI to allow passphrase input
1020+
if key.IsEncrypted {
1021+
t.app.Suspend(func() {
1022+
if err := t.gitService.UpdateKeyComment(key.Path, newComment); err != nil {
1023+
fmt.Printf("\nFailed to update comment: %v\n", err)
1024+
fmt.Print("Press Enter to continue...")
1025+
var dummy string
1026+
_, _ = fmt.Scanln(&dummy)
1027+
}
1028+
})
1029+
// Refresh after suspend (back in main thread)
10261030
t.refreshSSHKeysList()
1031+
t.showStatusTemp("SSH key comment updated successfully")
1032+
} else {
1033+
// For unencrypted keys, update directly
1034+
if err := t.gitService.UpdateKeyComment(key.Path, newComment); err != nil {
1035+
t.storeErrorf("[Edit SSH Key Comment] Failed to update comment for '%s': %v", key.Name, err)
1036+
t.showStatusTempColor(fmt.Sprintf("Failed to update comment: %v", err), "#FF6B6B")
1037+
} else {
1038+
t.showStatusTemp("SSH key comment updated successfully")
1039+
// Refresh the SSH keys list to show the updated comment
1040+
t.refreshSSHKeysList()
1041+
}
10271042
}
10281043
}).
10291044
OnCancel(t.handleModalClose)
@@ -1072,11 +1087,23 @@ func (t *tui) handleEditServerKeyComment() {
10721087
editor := NewEditKeyComment(t.app).
10731088
SetKey(sshKey.Name, sshKey.Path, sshKey.Comment).
10741089
OnSave(func(newComment string) {
1075-
// Update the key comment using gitService
1076-
if err := t.gitService.UpdateKeyComment(sshKey.Path, newComment); err != nil {
1077-
t.storeErrorf("[Edit SSH Key Comment] Failed to update comment for '%s': %v", sshKey.Name, err)
1078-
t.showStatusTempColor(fmt.Sprintf("Failed to update comment: %v", err), "#FF6B6B")
1079-
return
1090+
// For encrypted keys, suspend TUI to allow passphrase input
1091+
if sshKey.IsEncrypted {
1092+
t.app.Suspend(func() {
1093+
if err := t.gitService.UpdateKeyComment(sshKey.Path, newComment); err != nil {
1094+
fmt.Printf("\nFailed to update comment: %v\n", err)
1095+
fmt.Print("Press Enter to continue...")
1096+
var dummy string
1097+
_, _ = fmt.Scanln(&dummy)
1098+
}
1099+
})
1100+
} else {
1101+
// For unencrypted keys, update directly
1102+
if err := t.gitService.UpdateKeyComment(sshKey.Path, newComment); err != nil {
1103+
t.storeErrorf("[Edit SSH Key Comment] Failed to update comment for '%s': %v", sshKey.Name, err)
1104+
t.showStatusTempColor(fmt.Sprintf("Failed to update comment: %v", err), "#FF6B6B")
1105+
return
1106+
}
10801107
}
10811108

10821109
// If the key was loaded in the agent, we need to unload and reload it

internal/adapters/ui/server_details.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) {
297297
}
298298

299299
// Commands list
300-
text += "\n[::b]Commands:[-]\n Enter: SSH connect\n f: Port forward\n x: Stop forwarding\n c: Copy SSH command\n g: Ping server\n r: Refresh list\n a: Add new server\n e: Edit entry\n t: Edit tags\n d: Delete entry\n p: Pin/Unpin"
300+
text += "\n[::b]Commands:[-]\n [yellow]Enter[-]: SSH connect\n [yellow]f[-]: Port forward\n [yellow]x[-]: Stop forwarding\n [yellow]c[-]: Copy SSH command\n [yellow]g[-]: Ping server\n [yellow]r[-]: Refresh list\n [yellow]a[-]: Add new server\n [yellow]e[-]: Edit entry\n [yellow]t[-]: Edit tags\n [yellow]d[-]: Delete entry\n [yellow]p[-]: Pin/Unpin"
301301

302302
sd.TextView.SetText(text)
303303
}

internal/adapters/ui/sshkeys_list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func formatSSHKeyLine(key domain.SSHKey) string {
118118
flags += "[yellow]🔒[-] "
119119
}
120120
if !key.HasPublicKey {
121-
flags += "[red][-] "
121+
flags += "[red][-] "
122122
}
123123

124124
return fmt.Sprintf("%s %s%s [dim](%s)[-]", indicator, flags, key.Name, typeInfo)

internal/core/services/git_service.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,8 +660,11 @@ func (gs *gitService) parseKeyFile(path string, agentKeys map[string]agentKeyInf
660660
default:
661661
key.Type = keyTypeRSA // Default assumption
662662
}
663+
// Check for encryption by looking for bcrypt cipher indicator
664+
// Modern OpenSSH keys use "bcrypt" for encryption (base64: YmNyeXB0)
665+
// Old PEM format uses "Proc-Type: 4,ENCRYPTED"
663666
key.IsEncrypted = strings.Contains(contentStr, "Proc-Type: 4,ENCRYPTED") ||
664-
!strings.Contains(contentStr, "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA")
667+
strings.Contains(contentStr, "YmNyeXB0") // bcrypt cipher indicator
665668
case strings.Contains(contentStr, "DSA PRIVATE KEY"):
666669
key.Type = keyTypeDSA
667670
key.IsEncrypted = strings.Contains(contentStr, "ENCRYPTED")
@@ -971,6 +974,14 @@ func (gs *gitService) mergeAgentKeysWithFilesystem(keys []domain.SSHKey) []domai
971974
if keys[i].Size == 0 && fsKey.Size > 0 {
972975
keys[i].Size = fsKey.Size
973976
}
977+
// Copy HasPublicKey from filesystem key
978+
if fsKey.HasPublicKey {
979+
keys[i].HasPublicKey = true
980+
}
981+
// Copy IsEncrypted from filesystem key
982+
if fsKey.IsEncrypted {
983+
keys[i].IsEncrypted = true
984+
}
974985
}
975986

976987
return keys
@@ -1095,7 +1106,10 @@ func (gs *gitService) UpdateKeyComment(keyPath, comment string) error {
10951106
// Use ssh-keygen to update the comment
10961107
// #nosec G204 -- keyPath and comment are validated inputs
10971108
cmd := exec.Command("ssh-keygen", "-c", "-C", comment, "-f", keyPath)
1098-
// Discard output - the command prints to stdout/stderr but we only care about success/failure
1109+
// Connect stdin/stdout/stderr for interactive passphrase prompt (for encrypted keys)
1110+
cmd.Stdin = os.Stdin
1111+
cmd.Stdout = os.Stdout
1112+
cmd.Stderr = os.Stderr
10991113
if err := cmd.Run(); err != nil {
11001114
// Restore original permissions on error
11011115
if privateReadonly {

0 commit comments

Comments
 (0)