fix(ssh): allow blank passwords in profile and keychain auth (#11358)
Co-authored-by: Eugene <inbox@null.page>
This commit is contained in:
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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user