fix(ssh): allow blank passwords in profile and keychain auth (#11358)

Co-authored-by: Eugene <inbox@null.page>
This commit is contained in:
Utkarsh AdhranandEugene authored and GitHub committed 2026-09-24 22:42:36 +02:00
1 parent 9456248bc3
commit 256f09ddf0
4 files changed
+43 -16

No files matched your search

@@ -34,7 +34,7 @@ export class KeyboardInteractiveAuthComponent implements OnInit {
async ngOnInit (): Promise<void> {
const savedPassword = await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
if (savedPassword != null) {
for (let i = 0; i < this.prompt.prompts.length; i++) {
if (this.prompt.isAPasswordPrompt(i) && !this.prompt.responses[i]) {
this.prompt.responses[i] = savedPassword
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
import { Component, ViewChild } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { firstBy } from 'thenby'
import { FileProvidersService, Platform, HostAppService, PromptModalComponent, PartialProfile, ProfilesService, ProfileSettingsComponent, FullyDefined, ProxifiedConfig } from 'tabby-core'
import { FileProvidersService, Platform, PlatformService, HostAppService, PromptModalComponent, PartialProfile, ProfilesService, ProfileSettingsComponent, FullyDefined, ProxifiedConfig, TranslateService } from 'tabby-core'
import { LoginScriptsSettingsComponent } from 'tabby-terminal'
import { PasswordStorageService } from '../services/passwordStorage.service'
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
@@ -32,6 +33,8 @@ export class SSHProfileSettingsComponent implements ProfileSettingsComponent<SSH
private passwordStorage: PasswordStorageService,
private ngbModal: NgbModal,
private fileProviders: FileProvidersService,
private platform: PlatformService,
private translate: TranslateService,
) { }
async ngOnInit () {
@@ -57,7 +60,8 @@ export class SSHProfileSettingsComponent implements ProfileSettingsComponent<SSH
if (this.profile.options.user) {
try {
this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.profile)
const savedPassword = await this.passwordStorage.loadPassword(this.profile)
this.hasSavedPassword = savedPassword != null
} catch (e) {
console.error('Could not check for saved password', e)
}
@@ -74,13 +78,36 @@ export class SSHProfileSettingsComponent implements ProfileSettingsComponent<SSH
modal.componentInstance.password = true
try {
const result = await modal.result.catch(() => null)
if (result?.value) {
this.passwordStorage.savePassword(this.profile, result.value)
this.hasSavedPassword = true
// Allow saving a blank password (servers with PermitEmptyPasswords),
// but guard against a malformed modal result carrying no string value.
if (typeof result?.value !== 'string') {
return
}
// An empty field is far more often an accidental OK than a deliberate
// blank password, and a stored blank is then offered - and rejected -
// on every connection, so make the intent explicit.
if (result.value === '' && !await this.confirmBlankPassword()) {
return
}
this.passwordStorage.savePassword(this.profile, result.value)
this.hasSavedPassword = true
} catch { }
}
private async confirmBlankPassword (): Promise<boolean> {
return (await this.platform.showMessageBox({
type: 'warning',
message: this.translate.instant(_('Save an empty password?')),
detail: this.translate.instant(_('This only works if the server permits empty passwords. Tabby will offer it on every connection.')),
buttons: [
this.translate.instant(_('Save')),
this.translate.instant(_('Cancel')),
],
defaultId: 0,
cancelId: 1,
})).response === 0
}
clearSavedPassword () {
this.hasSavedPassword = false
this.passwordStorage.deletePassword(this.profile)
@@ -103,8 +130,8 @@ export class SSHProfileSettingsComponent implements ProfileSettingsComponent<SSH
save () {
for (const k of Object.values(SSHAlgorithmType)) {
this.profile.options.algorithms[k] = Object.entries(this.algorithms[k])
.filter(([_, v]) => !!v)
.map(([key, _]) => key)
.filter(([, v]) => !!v)
.map(([key]) => key)
if(k !== SSHAlgorithmType.COMPRESSION) { this.profile.options.algorithms[k].sort() }
}
+2 -2
View File
@@ -40,7 +40,7 @@ export class SSHService {
uri += `;x-tunnelusername=${jumpUsername}`
if (jumpHostProfile.options.auth === 'password') {
const jumpPassword = await this.passwordStorage.loadPassword(jumpHostProfile, jumpUsername)
if (jumpPassword) {
if (jumpPassword != null) {
uri += `;x-tunnelpasswordplain=${encodeURIComponent(jumpPassword)}`
}
}
@@ -61,7 +61,7 @@ export class SSHService {
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<{ uri: string, privateKeyFile?: tmp.FileResult|null }> {
let uri = `scp://${username ?? profile.options.user}`
const password = await this.passwordStorage.loadPassword(profile, username)
if (password) {
if (password != null) {
uri += ':' + encodeURIComponent(password)
}
let tmpFile: tmp.FileResult|null = null
+6 -6
View File
@@ -248,12 +248,12 @@ export class SSHSession {
}
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
if (this.profile.options.password) {
if (typeof this.profile.options.password === 'string') {
this.allAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
}
}
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
if (this.profile.options.password) {
if (typeof this.profile.options.password === 'string') {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword: this.profile.options.password })
}
this.allAuthMethods.push({ type: 'keyboard-interactive' })
@@ -270,7 +270,7 @@ export class SSHSession {
}
const storedPassword = await this.passwordStorage.loadPassword(this.profile, this.authUsername)
if (!storedPassword) {
if (storedPassword == null) {
return
}
@@ -495,7 +495,7 @@ export class SSHSession {
// auth success
if (this.savedPassword) {
if (this.savedPassword != null) {
this.passwordStorage.savePassword(this.profile, this.savedPassword, this.authUsername ?? undefined)
}
@@ -709,7 +709,7 @@ export class SSHSession {
modal.componentInstance.password = true
modal.componentInstance.showRememberCheckbox = true
const prefilledPassword = await this.passwordStorage.loadPassword(this.profile, this.authUsername)
if (prefilledPassword) {
if (prefilledPassword != null) {
modal.componentInstance.value = prefilledPassword
}
@@ -766,7 +766,7 @@ export class SSHSession {
state.prompts(),
)
if (method.savedPassword) {
if (method.savedPassword != null) {
// eslint-disable-next-line max-depth
for (let i = 0; i < prompt.prompts.length; i++) {
// eslint-disable-next-line max-depth