This commit is contained in:
Florian Kefferpütz
2025-10-13 21:34:31 +02:00
committed by GitHub
parent 6da2259dc3
commit 264f1cbe85
4 changed files with 64 additions and 23 deletions

View File

@@ -44,7 +44,7 @@ export class AutoSudoPasswordMiddleware extends SessionMiddleware {
async handlePrompt (username: string): Promise<void> {
console.log(`Detected sudo prompt for user: ${username}`)
const pw = await this.ps.loadPassword(this.profile)
const pw = await this.ps.loadPassword(this.profile, username)
if (pw) {
this.outputToTerminal.next(Buffer.from(this.pasteHint))
this.pendingPasswordToPaste = pw
@@ -55,7 +55,7 @@ export class AutoSudoPasswordMiddleware extends SessionMiddleware {
if (this.profile.options.user !== username) {
return null
}
return this.ps.loadPassword(this.profile)
return this.ps.loadPassword(this.profile, username)
}
}

View File

@@ -10,33 +10,45 @@ export const VAULT_SECRET_TYPE_PASSPHRASE = 'ssh:key-passphrase'
export class PasswordStorageService {
constructor (private vault: VaultService) { }
async savePassword (profile: SSHProfile, password: string): Promise<void> {
async savePassword (profile: SSHProfile, password: string, username?: string): Promise<void> {
const account = username ?? profile.options.user
if (this.vault.isEnabled()) {
const key = this.getVaultKeyForConnection(profile)
const key = this.getVaultKeyForConnection(profile, account)
this.vault.addSecret({ type: VAULT_SECRET_TYPE_PASSWORD, key, value: password })
} else {
if (!account) {
return
}
const key = this.getKeytarKeyForConnection(profile)
return keytar.setPassword(key, profile.options.user, password)
return keytar.setPassword(key, account, password)
}
}
async deletePassword (profile: SSHProfile): Promise<void> {
async deletePassword (profile: SSHProfile, username?: string): Promise<void> {
const account = username ?? profile.options.user
if (this.vault.isEnabled()) {
const key = this.getVaultKeyForConnection(profile)
const key = this.getVaultKeyForConnection(profile, account)
this.vault.removeSecret(VAULT_SECRET_TYPE_PASSWORD, key)
} else {
if (!account) {
return
}
const key = this.getKeytarKeyForConnection(profile)
await keytar.deletePassword(key, profile.options.user)
await keytar.deletePassword(key, account)
}
}
async loadPassword (profile: SSHProfile): Promise<string|null> {
async loadPassword (profile: SSHProfile, username?: string): Promise<string|null> {
const account = username ?? profile.options.user
if (this.vault.isEnabled()) {
const key = this.getVaultKeyForConnection(profile)
const key = this.getVaultKeyForConnection(profile, account)
return (await this.vault.getSecret(VAULT_SECRET_TYPE_PASSWORD, key))?.value ?? null
} else {
if (!account) {
return null
}
const key = this.getKeytarKeyForConnection(profile)
return keytar.getPassword(key, profile.options.user)
return keytar.getPassword(key, account)
}
}
@@ -82,9 +94,9 @@ export class PasswordStorageService {
return `ssh-private-key:${id}`
}
private getVaultKeyForConnection (profile: SSHProfile) {
private getVaultKeyForConnection (profile: SSHProfile, username?: string) {
return {
user: profile.options.user,
user: username ?? profile.options.user,
host: profile.options.host,
port: profile.options.port,
}

View File

@@ -29,7 +29,7 @@ export class SSHService {
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<string> {
let uri = `scp://${username ?? profile.options.user}`
const password = await this.passwordStorage.loadPassword(profile)
const password = await this.passwordStorage.loadPassword(profile, username)
if (password) {
uri += ':' + encodeURIComponent(password)
}

View File

@@ -200,15 +200,10 @@ export class SSHSession {
if (this.profile.options.password) {
this.allAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
}
const password = await this.passwordStorage.loadPassword(this.profile)
if (password) {
this.allAuthMethods.push({ type: 'saved-password', password })
}
}
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
if (this.profile.options.password) {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword: this.profile.options.password })
}
this.allAuthMethods.push({ type: 'keyboard-interactive' })
}
@@ -218,6 +213,38 @@ export class SSHSession {
this.allAuthMethods.push({ type: 'hostbased' })
}
private async populateStoredPasswordsForResolvedUsername (): Promise<void> {
if (!this.authUsername) {
return
}
const storedPassword = await this.passwordStorage.loadPassword(this.profile, this.authUsername)
if (!storedPassword) {
return
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
const hasSavedPassword = this.allAuthMethods.some(method => method.type === 'saved-password' && method.password === storedPassword)
if (!hasSavedPassword) {
const promptIndex = this.allAuthMethods.findIndex(method => method.type === 'prompt-password')
const insertIndex = promptIndex >= 0 ? promptIndex : this.allAuthMethods.length
this.allAuthMethods.splice(insertIndex, 0, { type: 'saved-password', password: storedPassword })
}
}
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const existingSaved = this.allAuthMethods.find(method => method.type === 'keyboard-interactive' && method.savedPassword === storedPassword)
if (!existingSaved) {
const updatable = this.allAuthMethods.find(method => method.type === 'keyboard-interactive' && method.savedPassword === undefined)
if (updatable && updatable.type === 'keyboard-interactive') {
updatable.savedPassword = storedPassword
} else {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword: storedPassword })
}
}
}
}
private async getAgentConnectionSpec (): Promise<russh.AgentConnectionSpec|null> {
if (this.hostApp.platform === Platform.Windows) {
if (this.config.store.ssh.agentType === 'auto') {
@@ -363,12 +390,14 @@ export class SSHSession {
}
}
await this.populateStoredPasswordsForResolvedUsername()
const authenticatedClient = await this.handleAuth()
if (authenticatedClient) {
this.ssh = authenticatedClient
} else {
this.ssh.disconnect()
this.passwordStorage.deletePassword(this.profile)
this.passwordStorage.deletePassword(this.profile, this.authUsername ?? undefined)
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error('Authentication rejected')
}
@@ -376,7 +405,7 @@ export class SSHSession {
// auth success
if (this.savedPassword) {
this.passwordStorage.savePassword(this.profile, this.savedPassword)
this.passwordStorage.savePassword(this.profile, this.savedPassword, this.authUsername ?? undefined)
}
for (const fw of this.profile.options.forwardedPorts ?? []) {