mirror of
https://github.com/Eugeny/tabby.git
synced 2024-12-22 01:51:36 +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 colors from 'ansi-colors'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import socksv5 from 'socksv5'
|
import socksv5 from 'socksv5'
|
||||||
import { Injector } from '@angular/core'
|
import { Injector } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { HostAppService, Logger, NotificationsService, Platform, PlatformService } from 'terminus-core'
|
||||||
import { BaseSession } from 'terminus-terminal'
|
import { BaseSession } from 'terminus-terminal'
|
||||||
import { Server, Socket, createServer, createConnection } from 'net'
|
import { Server, Socket, createServer, createConnection } from 'net'
|
||||||
import { Client, ClientChannel } from 'ssh2'
|
import { Client, ClientChannel } from 'ssh2'
|
||||||
import { Logger } from 'terminus-core'
|
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { ProxyCommandStream } from './services/ssh.service'
|
import { ProxyCommandStream } from './services/ssh.service'
|
||||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||||
import { PromptModalComponent } from './components/promptModal.component'
|
import { PromptModalComponent } from './components/promptModal.component'
|
||||||
|
|
||||||
|
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||||
|
|
||||||
export interface LoginScript {
|
export interface LoginScript {
|
||||||
expect: string
|
expect: string
|
||||||
send: string
|
send: string
|
||||||
@ -133,13 +139,21 @@ export class SSHSession extends BaseSession {
|
|||||||
logger: Logger
|
logger: Logger
|
||||||
jumpStream: any
|
jumpStream: any
|
||||||
proxyCommandStream: ProxyCommandStream|null = null
|
proxyCommandStream: ProxyCommandStream|null = null
|
||||||
authMethodsLeft: string[] = []
|
savedPassword?: string
|
||||||
savedPassword: string|undefined
|
|
||||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||||
|
|
||||||
|
agentPath?: string
|
||||||
|
privateKey?: string
|
||||||
|
|
||||||
|
private authMethodsLeft: string[] = []
|
||||||
private serviceMessage = new Subject<string>()
|
private serviceMessage = new Subject<string>()
|
||||||
private keychainPasswordUsed = false
|
private keychainPasswordUsed = false
|
||||||
|
|
||||||
private passwordStorage: PasswordStorageService
|
private passwordStorage: PasswordStorageService
|
||||||
private ngbModal: NgbModal
|
private ngbModal: NgbModal
|
||||||
|
private hostApp: HostAppService
|
||||||
|
private platform: PlatformService
|
||||||
|
private notifications: NotificationsService
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
@ -148,6 +162,9 @@ export class SSHSession extends BaseSession {
|
|||||||
super()
|
super()
|
||||||
this.passwordStorage = injector.get(PasswordStorageService)
|
this.passwordStorage = injector.get(PasswordStorageService)
|
||||||
this.ngbModal = injector.get(NgbModal)
|
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.scripts = connection.scripts ?? []
|
||||||
this.destroyed$.subscribe(() => {
|
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> {
|
async start (): Promise<void> {
|
||||||
this.open = true
|
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 = [
|
export const ALGORITHM_BLACKLIST = [
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import { Duplex } from 'stream'
|
import { Duplex } from 'stream'
|
||||||
import * as crypto from 'crypto'
|
|
||||||
import { Injectable, Injector, NgZone } from '@angular/core'
|
import { Injectable, Injector, NgZone } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Client } from 'ssh2'
|
import { Client } from 'ssh2'
|
||||||
import * as fs from 'mz/fs'
|
|
||||||
import { exec } from 'child_process'
|
import { exec } from 'child_process'
|
||||||
import * as path from 'path'
|
|
||||||
import * as sshpk from 'sshpk'
|
|
||||||
import { Subject, Observable } from 'rxjs'
|
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 { SettingsTabComponent } from 'terminus-settings'
|
||||||
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHConnection, SSHSession } from '../api'
|
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHConnection, SSHSession } from '../api'
|
||||||
import { PromptModalComponent } from '../components/promptModal.component'
|
import { PromptModalComponent } from '../components/promptModal.component'
|
||||||
@ -17,8 +13,6 @@ import { PasswordStorageService } from './passwordStorage.service'
|
|||||||
import { SSHTabComponent } from '../components/sshTab.component'
|
import { SSHTabComponent } from '../components/sshTab.component'
|
||||||
import { ChildProcess } from 'node:child_process'
|
import { ChildProcess } from 'node:child_process'
|
||||||
|
|
||||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SSHService {
|
export class SSHService {
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
@ -28,12 +22,10 @@ export class SSHService {
|
|||||||
private log: LogService,
|
private log: LogService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
private hostApp: HostAppService,
|
|
||||||
private passwordStorage: PasswordStorageService,
|
private passwordStorage: PasswordStorageService,
|
||||||
private notifications: NotificationsService,
|
private notifications: NotificationsService,
|
||||||
private app: AppService,
|
private app: AppService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private platform: PlatformService,
|
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('ssh')
|
this.logger = log.create('ssh')
|
||||||
}
|
}
|
||||||
@ -44,75 +36,13 @@ export class SSHService {
|
|||||||
return session
|
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> {
|
async connectSession (session: SSHSession): Promise<void> {
|
||||||
const log = (s: any) => session.emitServiceMessage(s)
|
const log = (s: any) => session.emitServiceMessage(s)
|
||||||
|
|
||||||
let privateKey: string|null = null
|
|
||||||
|
|
||||||
const ssh = new Client()
|
const ssh = new Client()
|
||||||
session.ssh = ssh
|
session.ssh = ssh
|
||||||
|
await session.init()
|
||||||
|
|
||||||
let connected = false
|
let connected = false
|
||||||
const algorithms = {}
|
const algorithms = {}
|
||||||
for (const key of Object.keys(session.connection.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 {
|
try {
|
||||||
if (session.connection.proxyCommand) {
|
if (session.connection.proxyCommand) {
|
||||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${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,
|
sock: session.proxyCommandStream ?? session.jumpStream,
|
||||||
username: session.connection.user,
|
username: session.connection.user,
|
||||||
password: session.connection.privateKey ? undefined : '',
|
password: session.connection.privateKey ? undefined : '',
|
||||||
privateKey: privateKey ?? undefined,
|
privateKey: session.privateKey ?? undefined,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
agent: agent ?? undefined,
|
agent: session.agentPath,
|
||||||
agentForward: session.connection.agentForward && !!agent,
|
agentForward: session.connection.agentForward && !!session.agentPath,
|
||||||
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
||||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||||
readyTimeout: session.connection.readyTimeout,
|
readyTimeout: session.connection.readyTimeout,
|
||||||
|
Loading…
Reference in New Issue
Block a user