diff --git a/README.md b/README.md index 43d6142..99a3e2d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ sudo apk add bash libstdc++ [OpenSSH](https://www.openssh.com/) supports using a [configuration file](https://linuxize.com/post/using-the-ssh-config-file/) to store all your different SSH connections. To use an SSH config file, run the `Remote-SSH: Open SSH Configuration File...` command. +## Saved passwords + +After you log in to a host with a typed password, you're offered to save it in the system keychain (macOS Keychain, Windows Credential Manager, or libsecret on Linux), so the next connection doesn't prompt. A saved password the server rejects is removed and you're prompted again. Passwords are saved per user, host and port, including for `ProxyJump` hops. One-time codes and other second factors are never saved. + +Set `remote.SSH.rememberPasswords` to `ask` (default), `always` or `never`, and run `Remote-SSH: Forget Saved Passwords` to remove them all. On Linux without a running keyring, the editor may fall back to weaker storage and warns when it does. + ## SSH configuration guides - [GSSAPI / Kerberos authentication](https://github.com/jeanp413/open-remote-ssh/blob/master/docs/gssapi-kerberos-authentication.md.md) diff --git a/package.json b/package.json index ef0f526..aee79eb 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "onCommand:openremotessh.openEmptyWindowInCurrentWindow", "onCommand:openremotessh.openConfigFile", "onCommand:openremotessh.showLog", + "onCommand:openremotessh.forgetSavedPasswords", "onResolveRemoteAuthority:ssh-remote", "onView:sshHosts" ], @@ -151,6 +152,22 @@ "default": "reject", "scope": "application" }, + "remote.SSH.rememberPasswords": { + "type": "string", + "enum": [ + "ask", + "always", + "never" + ], + "enumDescriptions": [ + "Ask after each successful login with a typed password.", + "Save typed passwords without asking.", + "Never save passwords. Passwords saved earlier are still used; run \"Remote-SSH: Forget Saved Passwords\" to remove them." + ], + "description": "Whether to offer saving SSH passwords in the system keychain after a successful password login.", + "default": "ask", + "scope": "application" + }, "remote.SSH.defaultExtensions": { "type": "array", "items": { @@ -265,6 +282,11 @@ "title": "Show Log", "category": "Remote-SSH" }, + { + "command": "openremotessh.forgetSavedPasswords", + "title": "Forget Saved Passwords", + "category": "Remote-SSH" + }, { "command": "openremotessh.explorer.emptyWindowInNewWindow", "title": "Connect to Host in New Window", diff --git a/src/authResolver.ts b/src/authResolver.ts index 98ab2fe..41ad0d8 100644 --- a/src/authResolver.ts +++ b/src/authResolver.ts @@ -1,4 +1,4 @@ -import type { ParsedKey } from 'ssh2-streams'; +import type { ParsedKey, Prompt } from 'ssh2-streams'; import type { ServerVersion } from './server-config/types'; import * as cp from 'child_process'; @@ -16,6 +16,7 @@ import NativeSSHConnection, { isGssapiAuthenticationRequested } from './ssh/nati import type { SSHClient, SSHExecChannel } from './ssh/sshClient'; import SSHConfiguration, { getSSHConfigPath, type HostConfiguration } from './ssh/sshConfig'; import { gatherIdentityFiles, SSHKey } from './ssh/identityFiles'; +import { getPasswordSecretKey, getSavedPassword, savePassword, deleteSavedPassword, type RememberPasswordsPolicy } from './ssh/savedPasswords'; import { untildify, exists as fileExists } from './common/files'; import { findRandomPort } from './common/ports'; import { disposeAll } from './common/disposable'; @@ -162,6 +163,21 @@ export function getProxyJumpPort(dest: SSHDestination, config: Record