Skip to content

Commit

Permalink
fixed #1790 - remember answers to password prompts in keyboard-intera…
Browse files Browse the repository at this point in the history
…ctive authentication
  • Loading branch information
Eugeny committed Aug 31, 2024
1 parent b0fbc33 commit c3df6be
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()'
)
Expand Down
13 changes: 11 additions & 2 deletions tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions tabby-ssh/src/components/sshTab.component.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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()'
)
66 changes: 42 additions & 24 deletions tabby-ssh/src/session/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -93,7 +103,6 @@ export class SSHSession {
private serviceMessage = new Subject<string>()
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
private willDestroy = new Subject<void>()
private keychainPasswordUsed = false

private passwordStorage: PasswordStorageService
private ngbModal: NgbModal
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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,
},
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit c3df6be

Please sign in to comment.