| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- import * as fs from 'mz/fs'
- import * as crypto from 'crypto'
- import * as path from 'path'
- import * as C from 'constants'
- // eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
- import { posix as posixPath } from 'path'
- import * as sshpk from 'sshpk'
- import colors from 'ansi-colors'
- import stripAnsi from 'strip-ansi'
- import socksv5 from 'socksv5'
- import { Injector, NgZone } from '@angular/core'
- import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
- import { FileProvidersService, HostAppService, Logger, NotificationsService, Platform, PlatformService, wrapPromise } from 'terminus-core'
- import { BaseSession } from 'terminus-terminal'
- import { Server, Socket, createServer, createConnection } from 'net'
- import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
- import type { FileEntry, Stats } from 'ssh2-streams'
- import { Subject, Observable } from 'rxjs'
- import { ProxyCommandStream } from './services/ssh.service'
- import { PasswordStorageService } from './services/passwordStorage.service'
- import { PromptModalComponent } from './components/promptModal.component'
- import { promisify } from 'util'
- const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
- export interface LoginScript {
- expect: string
- send: string
- isRegex?: boolean
- optional?: boolean
- }
- export enum SSHAlgorithmType {
- HMAC = 'hmac',
- KEX = 'kex',
- CIPHER = 'cipher',
- HOSTKEY = 'serverHostKey',
- }
- export interface SSHConnection {
- name: string
- host: string
- port?: number
- user: string
- auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
- password?: string
- privateKeys?: string[]
- group: string | null
- scripts?: LoginScript[]
- keepaliveInterval?: number
- keepaliveCountMax?: number
- readyTimeout?: number
- color?: string
- x11?: boolean
- skipBanner?: boolean
- disableDynamicTitle?: boolean
- jumpHost?: string
- agentForward?: boolean
- warnOnClose?: boolean
- algorithms?: Record<string, string[]>
- proxyCommand?: string
- forwardedPorts?: ForwardedPortConfig[]
- }
- export enum PortForwardType {
- Local = 'Local',
- Remote = 'Remote',
- Dynamic = 'Dynamic',
- }
- export interface ForwardedPortConfig {
- type: PortForwardType
- host: string
- port: number
- targetAddress: string
- targetPort: number
- }
- export class ForwardedPort implements ForwardedPortConfig {
- type: PortForwardType
- host = '127.0.0.1'
- port: number
- targetAddress: string
- targetPort: number
- private listener: Server
- async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
- if (this.type === PortForwardType.Local) {
- this.listener = createServer(s => callback(
- () => s,
- () => s.destroy(),
- s.remoteAddress ?? null,
- s.remotePort ?? null,
- this.targetAddress,
- this.targetPort,
- ))
- return new Promise((resolve, reject) => {
- this.listener.listen(this.port, this.host)
- this.listener.on('error', reject)
- this.listener.on('listening', resolve)
- })
- } else if (this.type === PortForwardType.Dynamic) {
- return new Promise((resolve, reject) => {
- this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
- callback(
- () => acceptConnection(true),
- () => rejectConnection(),
- null,
- null,
- info.dstAddr,
- info.dstPort,
- )
- })
- this.listener.on('error', reject)
- this.listener.listen(this.port, this.host, resolve)
- ;(this.listener as any).useAuth(socksv5.auth.None())
- })
- } else {
- throw new Error('Invalid forward type for a local listener')
- }
- }
- stopLocalListener (): void {
- this.listener.close()
- }
- toString (): string {
- if (this.type === PortForwardType.Local) {
- return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
- } if (this.type === PortForwardType.Remote) {
- return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
- } else {
- return `(dynamic) ${this.host}:${this.port}`
- }
- }
- }
- interface AuthMethod {
- type: 'none'|'publickey'|'agent'|'password'|'keyboard-interactive'|'hostbased'
- name?: string
- contents?: Buffer
- }
- export interface SFTPFile {
- name: string
- fullPath: string
- isDirectory: boolean
- isSymlink: boolean
- mode: number
- size: number
- modified: Date
- }
- export class SFTPFileHandle {
- position = 0
- constructor (
- private sftp: SFTPWrapper,
- private handle: Buffer,
- private zone: NgZone,
- ) { }
- read (): Promise<Buffer> {
- const buffer = Buffer.alloc(256 * 1024)
- return wrapPromise(this.zone, new Promise((resolve, reject) => {
- while (true) {
- const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => {
- if (err) {
- reject(err)
- return
- }
- this.position += read
- resolve(buffer.slice(0, read))
- })
- if (!wait) {
- break
- }
- }
- }))
- }
- write (chunk: Buffer): Promise<void> {
- return wrapPromise(this.zone, new Promise<void>((resolve, reject) => {
- while (true) {
- const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => {
- if (err) {
- return reject(err)
- }
- this.position += chunk.length
- resolve()
- })
- if (!wait) {
- break
- }
- }
- }))
- }
- close (): Promise<void> {
- return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle))
- }
- }
- export class SFTPSession {
- constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
- async readdir (p: string): Promise<SFTPFile[]> {
- const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
- return entries.map(entry => this._makeFile(
- posixPath.join(p, entry.filename), entry,
- ))
- }
- readlink (p: string): Promise<string> {
- return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
- }
- async stat (p: string): Promise<SFTPFile> {
- const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
- return {
- name: posixPath.basename(p),
- fullPath: p,
- isDirectory: stats.isDirectory(),
- isSymlink: stats.isSymbolicLink(),
- mode: stats.mode,
- size: stats.size,
- modified: new Date(stats.mtime * 1000),
- }
- }
- async open (p: string, mode: string): Promise<SFTPFileHandle> {
- const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
- return new SFTPFileHandle(this.sftp, handle, this.zone)
- }
- async rmdir (p: string): Promise<void> {
- await promisify((f: any) => this.sftp.rmdir(p, f))()
- }
- async unlink (p: string): Promise<void> {
- await promisify((f: any) => this.sftp.unlink(p, f))()
- }
- private _makeFile (p: string, entry: FileEntry): SFTPFile {
- return {
- fullPath: p,
- name: posixPath.basename(p),
- isDirectory: (entry.attrs.mode & C.S_IFDIR) === C.S_IFDIR,
- isSymlink: (entry.attrs.mode & C.S_IFLNK) === C.S_IFLNK,
- mode: entry.attrs.mode,
- size: entry.attrs.size,
- modified: new Date(entry.attrs.mtime * 1000),
- }
- }
- }
- export class SSHSession extends BaseSession {
- scripts?: LoginScript[]
- shell?: ClientChannel
- ssh: Client
- sftp?: SFTPWrapper
- forwardedPorts: ForwardedPort[] = []
- logger: Logger
- jumpStream: any
- proxyCommandStream: ProxyCommandStream|null = null
- savedPassword?: string
- get serviceMessage$ (): Observable<string> { return this.serviceMessage }
- agentPath?: string
- activePrivateKey: string|null = null
- private remainingAuthMethods: AuthMethod[] = []
- private serviceMessage = new Subject<string>()
- private keychainPasswordUsed = false
- private passwordStorage: PasswordStorageService
- private ngbModal: NgbModal
- private hostApp: HostAppService
- private platform: PlatformService
- private notifications: NotificationsService
- private zone: NgZone
- private fileProviders: FileProvidersService
- constructor (
- injector: Injector,
- public connection: SSHConnection,
- ) {
- 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.zone = injector.get(NgZone)
- this.fileProviders = injector.get(FileProvidersService)
- this.scripts = connection.scripts ?? []
- this.destroyed$.subscribe(() => {
- for (const port of this.forwardedPorts) {
- if (port.type === PortForwardType.Local) {
- port.stopLocalListener()
- }
- }
- })
- }
- 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.remainingAuthMethods = [{ type: 'none' }]
- if (!this.connection.auth || this.connection.auth === 'publicKey') {
- for (const pk of this.connection.privateKeys ?? []) {
- try {
- this.remainingAuthMethods.push({
- type: 'publickey',
- name: pk,
- contents: await this.fileProviders.retrieveFile(pk),
- })
- } catch (error) {
- this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Could not load private key ${pk}: ${error}`)
- }
- }
- }
- 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.remainingAuthMethods.push({ type: 'agent' })
- }
- }
- if (!this.connection.auth || this.connection.auth === 'password') {
- this.remainingAuthMethods.push({ type: 'password' })
- }
- if (!this.connection.auth || this.connection.auth === 'keyboardInteractive') {
- this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
- }
- this.remainingAuthMethods.push({ type: 'hostbased' })
- }
- async openSFTP (): Promise<SFTPSession> {
- if (!this.sftp) {
- this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
- }
- return new SFTPSession(this.sftp, this.zone)
- }
- async start (): Promise<void> {
- this.open = true
- this.proxyCommandStream?.on('error', err => {
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
- this.destroy()
- })
- try {
- this.shell = await this.openShellChannel({ x11: this.connection.x11 })
- } catch (err) {
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected opening a shell channel: ${err}`)
- if (err.toString().includes('Unable to request X11')) {
- this.emitServiceMessage(' Make sure `xauth` is installed on the remote side')
- }
- return
- }
- this.shell.on('greeting', greeting => {
- this.emitServiceMessage(`Shell greeting: ${greeting}`)
- })
- this.shell.on('banner', banner => {
- this.emitServiceMessage(`Shell banner: ${banner}`)
- })
- this.shell.on('data', data => {
- const dataString = data.toString()
- this.emitOutput(data)
- if (this.scripts) {
- let found = false
- for (const script of this.scripts) {
- let match = false
- let cmd = ''
- if (script.isRegex) {
- const re = new RegExp(script.expect, 'g')
- if (dataString.match(re)) {
- cmd = dataString.replace(re, script.send)
- match = true
- found = true
- }
- } else {
- if (dataString.includes(script.expect)) {
- cmd = script.send
- match = true
- found = true
- }
- }
- if (match) {
- this.logger.info('Executing script: "' + cmd + '"')
- this.shell?.write(cmd + '\n')
- this.scripts = this.scripts.filter(x => x !== script)
- } else {
- if (script.optional) {
- this.logger.debug('Skip optional script: ' + script.expect)
- found = true
- this.scripts = this.scripts.filter(x => x !== script)
- } else {
- break
- }
- }
- }
- if (found) {
- this.executeUnconditionalScripts()
- }
- }
- })
- this.shell.on('end', () => {
- this.logger.info('Shell session ended')
- if (this.open) {
- this.destroy()
- }
- })
- this.ssh.on('tcp connection', (details, accept, reject) => {
- this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
- const forward = this.forwardedPorts.find(x => x.port === details.destPort)
- if (!forward) {
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
- return reject()
- }
- const socket = new Socket()
- socket.connect(forward.targetPort, forward.targetAddress)
- socket.on('error', e => {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
- reject()
- })
- socket.on('connect', () => {
- this.logger.info('Connection forwarded')
- const stream = accept()
- stream.pipe(socket)
- socket.pipe(stream)
- stream.on('close', () => {
- socket.destroy()
- })
- socket.on('close', () => {
- stream.close()
- })
- })
- })
- this.ssh.on('x11', (details, accept, reject) => {
- this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
- const displaySpec = process.env.DISPLAY ?? ':0'
- this.logger.debug(`Trying display ${displaySpec}`)
- const xHost = displaySpec.split(':')[0]
- const xDisplay = parseInt(displaySpec.split(':')[1].split('.')[0] || '0')
- const xPort = xDisplay < 100 ? xDisplay + 6000 : xDisplay
- const socket = displaySpec.startsWith('/') ? createConnection(displaySpec) : new Socket()
- if (!displaySpec.startsWith('/')) {
- socket.connect(xPort, xHost)
- }
- socket.on('error', e => {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`)
- this.emitServiceMessage(` Terminus tried to connect to ${xHost}:${xPort} based on the DISPLAY environment var (${displaySpec})`)
- if (process.platform === 'win32') {
- this.emitServiceMessage(' To use X forwarding, you need a local X server, e.g.:')
- this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
- this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
- }
- reject()
- })
- socket.on('connect', () => {
- this.logger.info('Connection forwarded')
- const stream = accept()
- stream.pipe(socket)
- socket.pipe(stream)
- stream.on('close', () => {
- socket.destroy()
- })
- socket.on('close', () => {
- stream.close()
- })
- })
- })
- this.executeUnconditionalScripts()
- }
- emitServiceMessage (msg: string): void {
- this.serviceMessage.next(msg)
- this.logger.info(stripAnsi(msg))
- }
- async handleAuth (methodsLeft?: string[]): Promise<any> {
- this.activePrivateKey = null
- while (true) {
- const method = this.remainingAuthMethods.shift()
- if (!method) {
- return false
- }
- if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
- // Agent can still be used even if not in methodsLeft
- this.logger.info('Server does not support auth method', method.type)
- continue
- }
- if (method.type === 'password') {
- if (this.connection.password) {
- this.emitServiceMessage('Using preset password')
- return {
- type: 'password',
- username: this.connection.user,
- password: this.connection.password,
- }
- }
- if (!this.keychainPasswordUsed) {
- const password = await this.passwordStorage.loadPassword(this.connection)
- if (password) {
- this.emitServiceMessage('Trying saved password')
- this.keychainPasswordUsed = true
- return {
- type: 'password',
- username: this.connection.user,
- password,
- }
- }
- }
- const modal = this.ngbModal.open(PromptModalComponent)
- modal.componentInstance.prompt = `Password for ${this.connection.user}@${this.connection.host}`
- modal.componentInstance.password = true
- modal.componentInstance.showRememberCheckbox = true
- try {
- const result = await modal.result
- if (result) {
- if (result.remember) {
- this.savedPassword = result.value
- }
- return {
- type: 'password',
- username: this.connection.user,
- password: result.value,
- }
- } else {
- continue
- }
- } catch {
- continue
- }
- }
- if (method.type === 'publickey') {
- try {
- const key = await this.loadPrivateKey(method.contents)
- return {
- type: 'publickey',
- username: this.connection.user,
- key,
- }
- } catch (e) {
- this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
- continue
- }
- }
- return method
- }
- }
- async addPortForward (fw: ForwardedPort): Promise<void> {
- if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
- await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
- this.logger.info(`New connection on ${fw}`)
- this.ssh.forwardOut(
- sourceAddress ?? '127.0.0.1',
- sourcePort ?? 0,
- targetAddress,
- targetPort,
- (err, stream) => {
- if (err) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
- return reject()
- }
- const socket = accept()
- stream.pipe(socket)
- socket.pipe(stream)
- stream.on('close', () => {
- socket.destroy()
- })
- socket.on('close', () => {
- stream.close()
- })
- }
- )
- }).then(() => {
- this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
- this.forwardedPorts.push(fw)
- }).catch(e => {
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
- throw e
- })
- }
- if (fw.type === PortForwardType.Remote) {
- await new Promise<void>((resolve, reject) => {
- this.ssh.forwardIn(fw.host, fw.port, err => {
- if (err) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
- return reject(err)
- }
- resolve()
- })
- })
- this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
- this.forwardedPorts.push(fw)
- }
- }
- async removePortForward (fw: ForwardedPort): Promise<void> {
- if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
- fw.stopLocalListener()
- this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
- }
- if (fw.type === PortForwardType.Remote) {
- this.ssh.unforwardIn(fw.host, fw.port)
- this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
- }
- this.emitServiceMessage(`Stopped forwarding ${fw}`)
- }
- resize (columns: number, rows: number): void {
- if (this.shell) {
- this.shell.setWindow(rows, columns, rows, columns)
- }
- }
- write (data: Buffer): void {
- if (this.shell) {
- this.shell.write(data)
- }
- }
- kill (signal?: string): void {
- if (this.shell) {
- this.shell.signal(signal ?? 'TERM')
- }
- }
- async destroy (): Promise<void> {
- this.serviceMessage.complete()
- this.proxyCommandStream?.destroy()
- this.kill()
- this.ssh.end()
- await super.destroy()
- }
- async getChildProcesses (): Promise<any[]> {
- return []
- }
- async gracefullyKillProcess (): Promise<void> {
- this.kill('TERM')
- }
- supportsWorkingDirectory (): boolean {
- return true
- }
- async getWorkingDirectory (): Promise<string|null> {
- return null
- }
- private openShellChannel (options): Promise<ClientChannel> {
- return new Promise<ClientChannel>((resolve, reject) => {
- this.ssh.shell({ term: 'xterm-256color' }, options, (err, shell) => {
- if (err) {
- reject(err)
- } else {
- resolve(shell)
- }
- })
- })
- }
- private executeUnconditionalScripts () {
- if (this.scripts) {
- for (const script of this.scripts) {
- if (!script.expect) {
- console.log('Executing script:', script.send)
- this.shell?.write(script.send + '\n')
- this.scripts = this.scripts.filter(x => x !== script)
- } else {
- break
- }
- }
- }
- }
- async loadPrivateKey (privateKeyContents?: Buffer): Promise<string|null> {
- if (!privateKeyContents) {
- const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')
- if (await fs.exists(userKeyPath)) {
- this.emitServiceMessage('Using user\'s default private key')
- privateKeyContents = fs.readFile(userKeyPath, { encoding: null })
- }
- }
- if (!privateKeyContents) {
- return null
- }
- this.emitServiceMessage('Loading private key')
- try {
- const parsedKey = await this.parsePrivateKey(privateKeyContents.toString())
- this.activePrivateKey = parsedKey.toString('openssh')
- return this.activePrivateKey
- } 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 null
- }
- }
- 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 = [
- // cause native crashes in node crypto, use EC instead
- 'diffie-hellman-group-exchange-sha256',
- 'diffie-hellman-group-exchange-sha1',
- ]
|