fixed #1790 - remember answers to password prompts in keyboard-interactive authentication

This commit is contained in:
Eugene 2024-08-31 22:09:35 +02:00
parent b0fbc33963
commit c3df6be8c7
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
4 changed files with 64 additions and 28 deletions

View File

@ -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()'
)

View File

@ -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()

View File

@ -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()'
)

View File

@ -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<string>()
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
private willDestroy = new Subject<void>()
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 {