mirror of
https://github.com/Eugeny/tabby.git
synced 2024-12-21 09:31:44 +03:00
private key and agent auth cleanup
This commit is contained in:
parent
779eb235f3
commit
79a429be5d
@ -1,17 +1,23 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import * as crypto from 'crypto'
|
||||
import * as path from 'path'
|
||||
import * as sshpk from 'sshpk'
|
||||
import colors from 'ansi-colors'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import socksv5 from 'socksv5'
|
||||
import { Injector } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HostAppService, Logger, NotificationsService, Platform, PlatformService } from 'terminus-core'
|
||||
import { BaseSession } from 'terminus-terminal'
|
||||
import { Server, Socket, createServer, createConnection } from 'net'
|
||||
import { Client, ClientChannel } from 'ssh2'
|
||||
import { Logger } from 'terminus-core'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { ProxyCommandStream } from './services/ssh.service'
|
||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
|
||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||
|
||||
export interface LoginScript {
|
||||
expect: string
|
||||
send: string
|
||||
@ -133,13 +139,21 @@ export class SSHSession extends BaseSession {
|
||||
logger: Logger
|
||||
jumpStream: any
|
||||
proxyCommandStream: ProxyCommandStream|null = null
|
||||
authMethodsLeft: string[] = []
|
||||
savedPassword: string|undefined
|
||||
savedPassword?: string
|
||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||
|
||||
agentPath?: string
|
||||
privateKey?: string
|
||||
|
||||
private authMethodsLeft: string[] = []
|
||||
private serviceMessage = new Subject<string>()
|
||||
private keychainPasswordUsed = false
|
||||
|
||||
private passwordStorage: PasswordStorageService
|
||||
private ngbModal: NgbModal
|
||||
private hostApp: HostAppService
|
||||
private platform: PlatformService
|
||||
private notifications: NotificationsService
|
||||
|
||||
constructor (
|
||||
injector: Injector,
|
||||
@ -148,6 +162,9 @@ export class SSHSession extends BaseSession {
|
||||
super()
|
||||
this.passwordStorage = injector.get(PasswordStorageService)
|
||||
this.ngbModal = injector.get(NgbModal)
|
||||
this.hostApp = injector.get(HostAppService)
|
||||
this.platform = injector.get(PlatformService)
|
||||
this.notifications = injector.get(NotificationsService)
|
||||
|
||||
this.scripts = connection.scripts ?? []
|
||||
this.destroyed$.subscribe(() => {
|
||||
@ -159,6 +176,48 @@ export class SSHSession extends BaseSession {
|
||||
})
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
|
||||
this.agentPath = WINDOWS_OPENSSH_AGENT_PIPE
|
||||
} else {
|
||||
if (await this.platform.isProcessRunning('pageant.exe')) {
|
||||
this.agentPath = 'pageant'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.agentPath = process.env.SSH_AUTH_SOCK!
|
||||
}
|
||||
|
||||
this.authMethodsLeft = ['none']
|
||||
if (!this.connection.auth || this.connection.auth === 'publicKey') {
|
||||
try {
|
||||
await this.loadPrivateKey()
|
||||
} catch (e) {
|
||||
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key: ${e}`)
|
||||
}
|
||||
if (!this.privateKey) {
|
||||
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Private key auth selected, but no key is loaded`)
|
||||
} else {
|
||||
this.authMethodsLeft.push('publickey')
|
||||
}
|
||||
}
|
||||
if (!this.connection.auth || this.connection.auth === 'agent') {
|
||||
if (!this.agentPath) {
|
||||
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running agent is detected`)
|
||||
} else {
|
||||
this.authMethodsLeft.push('agent')
|
||||
}
|
||||
}
|
||||
if (!this.connection.auth || this.connection.auth === 'password') {
|
||||
this.authMethodsLeft.push('password')
|
||||
}
|
||||
if (!this.connection.auth || this.connection.auth === 'keyboardInteractive') {
|
||||
this.authMethodsLeft.push('keyboard-interactive')
|
||||
}
|
||||
this.authMethodsLeft.push('hostbased')
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
this.open = true
|
||||
|
||||
@ -500,6 +559,64 @@ export class SSHSession extends BaseSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadPrivateKey (): Promise<void> {
|
||||
let privateKeyPath = this.connection.privateKey
|
||||
|
||||
if (!privateKeyPath) {
|
||||
const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')
|
||||
if (await fs.exists(userKeyPath)) {
|
||||
this.emitServiceMessage('Using user\'s default private key')
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
|
||||
if (privateKeyPath) {
|
||||
this.emitServiceMessage('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||
try {
|
||||
const privateKeyContents = (await fs.readFile(privateKeyPath)).toString()
|
||||
const parsedKey = await this.parsePrivateKey(privateKeyContents)
|
||||
this.privateKey = parsedKey.toString('openssh')
|
||||
} catch (error) {
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ' Could not read the private key file')
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${error}`)
|
||||
this.notifications.error('Could not read the private key file')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async parsePrivateKey (privateKey: string): Promise<any> {
|
||||
const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
|
||||
let passphrase: string|null = await this.passwordStorage.loadPrivateKeyPassword(keyHash)
|
||||
while (true) {
|
||||
try {
|
||||
return sshpk.parsePrivateKey(privateKey, 'auto', { passphrase })
|
||||
} catch (e) {
|
||||
if (e instanceof sshpk.KeyEncryptedError || e instanceof sshpk.KeyParseError) {
|
||||
await this.passwordStorage.deletePrivateKeyPassword(keyHash)
|
||||
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
modal.componentInstance.showRememberCheckbox = true
|
||||
|
||||
try {
|
||||
const result = await modal.result
|
||||
passphrase = result?.value
|
||||
if (passphrase && result.remember) {
|
||||
this.passwordStorage.savePrivateKeyPassword(keyHash, passphrase)
|
||||
}
|
||||
} catch {
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
this.notifications.error('Could not read the private key', e.toString())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ALGORITHM_BLACKLIST = [
|
||||
|
@ -1,15 +1,11 @@
|
||||
import colors from 'ansi-colors'
|
||||
import { Duplex } from 'stream'
|
||||
import * as crypto from 'crypto'
|
||||
import { Injectable, Injector, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Client } from 'ssh2'
|
||||
import * as fs from 'mz/fs'
|
||||
import { exec } from 'child_process'
|
||||
import * as path from 'path'
|
||||
import * as sshpk from 'sshpk'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { HostAppService, Platform, Logger, LogService, AppService, SelectorOption, ConfigService, NotificationsService, PlatformService } from 'terminus-core'
|
||||
import { Logger, LogService, AppService, SelectorOption, ConfigService, NotificationsService } from 'terminus-core'
|
||||
import { SettingsTabComponent } from 'terminus-settings'
|
||||
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHConnection, SSHSession } from '../api'
|
||||
import { PromptModalComponent } from '../components/promptModal.component'
|
||||
@ -17,8 +13,6 @@ import { PasswordStorageService } from './passwordStorage.service'
|
||||
import { SSHTabComponent } from '../components/sshTab.component'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
|
||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SSHService {
|
||||
private logger: Logger
|
||||
@ -28,12 +22,10 @@ export class SSHService {
|
||||
private log: LogService,
|
||||
private zone: NgZone,
|
||||
private ngbModal: NgbModal,
|
||||
private hostApp: HostAppService,
|
||||
private passwordStorage: PasswordStorageService,
|
||||
private notifications: NotificationsService,
|
||||
private app: AppService,
|
||||
private config: ConfigService,
|
||||
private platform: PlatformService,
|
||||
) {
|
||||
this.logger = log.create('ssh')
|
||||
}
|
||||
@ -44,75 +36,13 @@ export class SSHService {
|
||||
return session
|
||||
}
|
||||
|
||||
async loadPrivateKeyForSession (session: SSHSession): Promise<string|null> {
|
||||
let privateKey: string|null = null
|
||||
let privateKeyPath = session.connection.privateKey
|
||||
|
||||
if (!privateKeyPath) {
|
||||
const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')
|
||||
if (await fs.exists(userKeyPath)) {
|
||||
session.emitServiceMessage('Using user\'s default private key')
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
|
||||
if (privateKeyPath) {
|
||||
session.emitServiceMessage('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||
try {
|
||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||
} catch (error) {
|
||||
session.emitServiceMessage(colors.bgRed.black(' X ') + ' Could not read the private key file')
|
||||
session.emitServiceMessage(colors.bgRed.black(' X ') + ` ${error}`)
|
||||
this.notifications.error('Could not read the private key file')
|
||||
}
|
||||
|
||||
if (privateKey) {
|
||||
const parsedKey = await this.parsePrivateKey(privateKey)
|
||||
privateKey = parsedKey.toString('openssh')
|
||||
}
|
||||
}
|
||||
return privateKey
|
||||
}
|
||||
|
||||
async parsePrivateKey (privateKey: string): Promise<any> {
|
||||
const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
|
||||
let passphrase: string|null = await this.passwordStorage.loadPrivateKeyPassword(keyHash)
|
||||
while (true) {
|
||||
try {
|
||||
return sshpk.parsePrivateKey(privateKey, 'auto', { passphrase })
|
||||
} catch (e) {
|
||||
if (e instanceof sshpk.KeyEncryptedError || e instanceof sshpk.KeyParseError) {
|
||||
await this.passwordStorage.deletePrivateKeyPassword(keyHash)
|
||||
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
modal.componentInstance.showRememberCheckbox = true
|
||||
|
||||
try {
|
||||
const result = await modal.result
|
||||
passphrase = result?.value
|
||||
if (passphrase && result.remember) {
|
||||
this.passwordStorage.savePrivateKeyPassword(keyHash, passphrase)
|
||||
}
|
||||
} catch {
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
this.notifications.error('Could not read the private key', e.toString())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectSession (session: SSHSession): Promise<void> {
|
||||
const log = (s: any) => session.emitServiceMessage(s)
|
||||
|
||||
let privateKey: string|null = null
|
||||
|
||||
const ssh = new Client()
|
||||
session.ssh = ssh
|
||||
await session.init()
|
||||
|
||||
let connected = false
|
||||
const algorithms = {}
|
||||
for (const key of Object.keys(session.connection.algorithms ?? {})) {
|
||||
@ -186,47 +116,6 @@ export class SSHService {
|
||||
})
|
||||
})
|
||||
|
||||
let agent: string|null = null
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
|
||||
agent = WINDOWS_OPENSSH_AGENT_PIPE
|
||||
} else {
|
||||
if (await this.platform.isProcessRunning('pageant.exe')) {
|
||||
agent = 'pageant'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
agent = process.env.SSH_AUTH_SOCK!
|
||||
}
|
||||
|
||||
session.authMethodsLeft = ['none']
|
||||
if (!session.connection.auth || session.connection.auth === 'publicKey') {
|
||||
try {
|
||||
privateKey = await this.loadPrivateKeyForSession(session)
|
||||
} catch (e) {
|
||||
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key: ${e}`)
|
||||
}
|
||||
if (!privateKey) {
|
||||
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Private key auth selected, but no key is loaded`)
|
||||
} else {
|
||||
session.authMethodsLeft.push('publickey')
|
||||
}
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'agent') {
|
||||
if (!agent) {
|
||||
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running agent is detected`)
|
||||
} else {
|
||||
session.authMethodsLeft.push('agent')
|
||||
}
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'password') {
|
||||
session.authMethodsLeft.push('password')
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'keyboardInteractive') {
|
||||
session.authMethodsLeft.push('keyboard-interactive')
|
||||
}
|
||||
session.authMethodsLeft.push('hostbased')
|
||||
|
||||
try {
|
||||
if (session.connection.proxyCommand) {
|
||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${session.connection.proxyCommand}`)
|
||||
@ -245,10 +134,10 @@ export class SSHService {
|
||||
sock: session.proxyCommandStream ?? session.jumpStream,
|
||||
username: session.connection.user,
|
||||
password: session.connection.privateKey ? undefined : '',
|
||||
privateKey: privateKey ?? undefined,
|
||||
privateKey: session.privateKey ?? undefined,
|
||||
tryKeyboard: true,
|
||||
agent: agent ?? undefined,
|
||||
agentForward: session.connection.agentForward && !!agent,
|
||||
agent: session.agentPath,
|
||||
agentForward: session.connection.agentForward && !!session.agentPath,
|
||||
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||
readyTimeout: session.connection.readyTimeout,
|
||||
|
Loading…
Reference in New Issue
Block a user