1
1
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:
Eugene Pankov 2021-06-04 23:05:20 +02:00
parent 779eb235f3
commit 79a429be5d
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
2 changed files with 126 additions and 120 deletions

View File

@ -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 = [

View File

@ -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,