From c3df6be8c732c4641a883254edaeda69c3730997 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 31 Aug 2024 22:09:35 +0200 Subject: [PATCH] fixed #1790 - remember answers to password prompts in keyboard-interactive authentication --- ...keyboardInteractiveAuthPanel.component.pug | 12 +++- .../keyboardInteractiveAuthPanel.component.ts | 13 +++- tabby-ssh/src/components/sshTab.component.pug | 1 + tabby-ssh/src/session/ssh.ts | 66 ++++++++++++------- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.pug b/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.pug index e203b26b56..c6ab0700e8 100644 --- a/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.pug +++ b/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.pug @@ -14,11 +14,19 @@ input.form-control.mt-2( ) .d-flex.mt-3 - button.btn.btn-secondary( + checkbox( + *ngIf='isPassword()', + [(ngModel)]='remember', + [text]='"Save password"|translate' + ) + + .ms-auto + + button.btn.btn-secondary.me-3( *ngIf='step > 0', (click)='previous()' ) - .ms-auto + button.btn.btn-primary( (click)='next()' ) diff --git a/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts b/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts index 8d71414423..bb2a851d67 100644 --- a/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts +++ b/tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts @@ -1,6 +1,7 @@ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core' import { KeyboardInteractivePrompt } from '../session/ssh' - +import { SSHProfile } from '../api' +import { PasswordStorageService } from '../services/passwordStorage.service' @Component({ selector: 'keyboard-interactive-auth-panel', @@ -9,13 +10,17 @@ import { KeyboardInteractivePrompt } from '../session/ssh' changeDetection: ChangeDetectionStrategy.OnPush, }) export class KeyboardInteractiveAuthComponent { + @Input() profile: SSHProfile @Input() prompt: KeyboardInteractivePrompt @Input() step = 0 @Output() done = new EventEmitter() @ViewChild('input') input: ElementRef + remember = false + + constructor (private passwordStorage: PasswordStorageService) {} isPassword (): boolean { - return this.prompt.prompts[this.step].prompt.toLowerCase().includes('password') || !this.prompt.prompts[this.step].echo + return this.prompt.isAPasswordPrompt(this.step) } previous (): void { @@ -26,6 +31,10 @@ export class KeyboardInteractiveAuthComponent { } next (): void { + if (this.isPassword() && this.remember) { + this.passwordStorage.savePassword(this.profile, this.prompt.responses[this.step]) + } + if (this.step === this.prompt.prompts.length - 1) { this.prompt.respond() this.done.emit() diff --git a/tabby-ssh/src/components/sshTab.component.pug b/tabby-ssh/src/components/sshTab.component.pug index f6dd1aac94..bf1d245711 100644 --- a/tabby-ssh/src/components/sshTab.component.pug +++ b/tabby-ssh/src/components/sshTab.component.pug @@ -51,6 +51,7 @@ sftp-panel.bg-dark( keyboard-interactive-auth-panel.bg-dark( *ngIf='activeKIPrompt', [prompt]='activeKIPrompt', + [profile]='profile', (click)='$event.stopPropagation()', (done)='activeKIPrompt = null; frontend?.focus()' ) diff --git a/tabby-ssh/src/session/ssh.ts b/tabby-ssh/src/session/ssh.ts index d8de9a35f6..8444795eaf 100644 --- a/tabby-ssh/src/session/ssh.ts +++ b/tabby-ssh/src/session/ssh.ts @@ -26,7 +26,13 @@ export interface Prompt { } type AuthMethod = { - type: 'none'|'password'|'keyboard-interactive'|'hostbased' + type: 'none'|'prompt-password'|'hostbased' +} | { + type: 'keyboard-interactive', + savedPassword?: string +} | { + type: 'saved-password', + password: string } | { type: 'publickey' name: string @@ -62,6 +68,10 @@ export class KeyboardInteractivePrompt { this.responses = new Array(this.prompts.length).fill('') } + isAPasswordPrompt (index: number): boolean { + return this.prompts[index].prompt.toLowerCase().includes('password') && !this.prompts[index].echo + } + respond (): void { this._resolve(this.responses) } @@ -93,7 +103,6 @@ export class SSHSession { private serviceMessage = new Subject() private keyboardInteractivePrompt = new Subject() private willDestroy = new Subject() - private keychainPasswordUsed = false private passwordStorage: PasswordStorageService private ngbModal: NgbModal @@ -168,9 +177,20 @@ export class SSHSession { } } if (!this.profile.options.auth || this.profile.options.auth === 'password') { - this.remainingAuthMethods.push({ type: 'password' }) + if (this.profile.options.password) { + this.remainingAuthMethods.push({ type: 'saved-password', password: this.profile.options.password }) + } + const password = await this.passwordStorage.loadPassword(this.profile) + if (password) { + this.remainingAuthMethods.push({ type: 'saved-password', password }) + } + this.remainingAuthMethods.push({ type: 'prompt-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.remainingAuthMethods.push({ type: 'keyboard-interactive', savedPassword }) + } this.remainingAuthMethods.push({ type: 'keyboard-interactive' }) } this.remainingAuthMethods.push({ type: 'hostbased' }) @@ -276,7 +296,7 @@ export class SSHSession { }, keepaliveIntervalSeconds: Math.round((this.profile.options.keepaliveInterval ?? 15000) / 1000), keepaliveCountMax: this.profile.options.keepaliveCountMax, - connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : null, + connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : undefined, }, ) @@ -470,27 +490,14 @@ export class SSHSession { this.logger.info('Server does not support auth method', method.type) continue } - if (method.type === 'password') { - if (this.profile.options.password) { - this.emitServiceMessage(this.translate.instant('Using preset password')) - const result = await this.ssh.authenticateWithPassword(this.authUsername, this.profile.options.password) - if (result) { - return result - } + if (method.type === 'saved-password') { + this.emitServiceMessage(this.translate.instant('Using saved password')) + const result = await this.ssh.authenticateWithPassword(this.authUsername, method.password) + if (result) { + return result } - - if (!this.keychainPasswordUsed && this.profile.options.user) { - const password = await this.passwordStorage.loadPassword(this.profile) - if (password) { - this.emitServiceMessage(this.translate.instant('Trying saved password')) - this.keychainPasswordUsed = true - const result = await this.ssh.authenticateWithPassword(this.authUsername, password) - if (result) { - return result - } - } - } - + } + if (method.type === 'prompt-password') { const modal = this.ngbModal.open(PromptModalComponent) modal.componentInstance.prompt = `Password for ${this.authUsername}@${this.profile.options.host}` modal.componentInstance.password = true @@ -544,6 +551,17 @@ export class SSHSession { state.instructions, state.prompts(), ) + + if (method.savedPassword) { + // eslint-disable-next-line max-depth + for (let i = 0; i < prompt.prompts.length; i++) { + // eslint-disable-next-line max-depth + if (prompt.isAPasswordPrompt(i)) { + prompt.responses[i] = method.savedPassword + } + } + } + this.emitKeyboardInteractivePrompt(prompt) try {