diff --git a/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts b/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts index b9e16d8c9..4917a19a7 100644 --- a/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts +++ b/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts @@ -34,7 +34,7 @@ export class KeyboardInteractiveAuthComponent implements OnInit { async ngOnInit (): Promise { 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 diff --git a/tabby-ssh/src/components/sshProfileSettings.component.ts b/tabby-ssh/src/components/sshProfileSettings.component.ts index 7880826c4..58490c932 100644 --- a/tabby-ssh/src/components/sshProfileSettings.component.ts +++ b/tabby-ssh/src/components/sshProfileSettings.component.ts @@ -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 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 { + 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 !!v) - .map(([key, _]) => key) + .filter(([, v]) => !!v) + .map(([key]) => key) if(k !== SSHAlgorithmType.COMPRESSION) { this.profile.options.algorithms[k].sort() } } diff --git a/tabby-ssh/src/services/ssh.service.ts b/tabby-ssh/src/services/ssh.service.ts index 8a3a193dc..cff9a0908 100644 --- a/tabby-ssh/src/services/ssh.service.ts +++ b/tabby-ssh/src/services/ssh.service.ts @@ -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 diff --git a/tabby-ssh/src/session/ssh.ts b/tabby-ssh/src/session/ssh.ts index cce6446b5..c3f9acba0 100644 --- a/tabby-ssh/src/session/ssh.ts +++ b/tabby-ssh/src/session/ssh.ts @@ -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