diff --git a/terminus-ssh/src/index.ts b/terminus-ssh/src/index.ts index ce206593..9656458e 100644 --- a/terminus-ssh/src/index.ts +++ b/terminus-ssh/src/index.ts @@ -10,6 +10,7 @@ import { SSHModalComponent } from './components/sshModal.component' import { PromptModalComponent } from './components/promptModal.component' import { SSHSettingsTabComponent } from './components/sshSettingsTab.component' import { SSHService } from './services/ssh.service' +import { PasswordStorageService } from './services/passwordStorage.service' import { ButtonProvider } from './buttonProvider' import { SSHConfigProvider } from './config' @@ -22,6 +23,7 @@ import { SSHSettingsTabProvider } from './settings' FormsModule, ], providers: [ + PasswordStorageService, SSHService, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: ConfigProvider, useClass: SSHConfigProvider, multi: true }, diff --git a/terminus-ssh/src/services/passwordStorage.service.ts b/terminus-ssh/src/services/passwordStorage.service.ts new file mode 100644 index 00000000..83e367ef --- /dev/null +++ b/terminus-ssh/src/services/passwordStorage.service.ts @@ -0,0 +1,73 @@ +import { Injectable, NgZone } from '@angular/core' +import { SSHConnection } from '../api' + +let xkeychain +let wincredmgr +try { + xkeychain = require('xkeychain') +} catch (error) { + try { + wincredmgr = require('wincredmgr') + } catch (error2) { + console.warn('No keychain manager available') + } +} + +@Injectable() +export class PasswordStorageService { + constructor ( + private zone: NgZone, + ) { } + + savePassword (connection: SSHConnection, password: string) { + if (xkeychain) { + xkeychain.setPassword({ + account: connection.user, + service: `ssh@${connection.host}`, + password + }, () => null) + } else { + wincredmgr.WriteCredentials( + 'user', + password, + `ssh:${connection.user}@${connection.host}`, + ) + } + } + + deletePassword (connection: SSHConnection) { + if (xkeychain) { + xkeychain.deletePassword({ + account: connection.user, + service: `ssh@${connection.host}`, + }, () => null) + } else { + wincredmgr.DeleteCredentials( + `ssh:${connection.user}@${connection.host}`, + ) + } + } + + loadPassword (connection: SSHConnection): Promise { + return new Promise(resolve => { + if (!wincredmgr && !xkeychain.isSupported()) { + return resolve(null) + } + if (xkeychain) { + xkeychain.getPassword( + { + account: connection.user, + service: `ssh@${connection.host}`, + }, + (_, result) => this.zone.run(() => resolve(result)) + ) + } else { + try { + resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password) + } catch (error) { + resolve(null) + } + } + }) + } +} diff --git a/terminus-ssh/src/services/ssh.service.ts b/terminus-ssh/src/services/ssh.service.ts index 69c8a9a2..30ca35c4 100644 --- a/terminus-ssh/src/services/ssh.service.ts +++ b/terminus-ssh/src/services/ssh.service.ts @@ -3,92 +3,59 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { Client } from 'ssh2' import * as fs from 'mz/fs' import * as path from 'path' -import { AppService, HostAppService, Platform } from 'terminus-core' +import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core' import { TerminalTabComponent } from 'terminus-terminal' import { SSHConnection, SSHSession } from '../api' import { PromptModalComponent } from '../components/promptModal.component' - +import { PasswordStorageService } from './passwordStorage.service' const { SSH2Stream } = require('ssh2-streams') -let xkeychain -let wincredmgr -try { - xkeychain = require('xkeychain') -} catch (error) { - try { - wincredmgr = require('wincredmgr') - } catch (error2) { - console.warn('No keychain manager available') - } -} - @Injectable() export class SSHService { + private logger: Logger + constructor ( + log: LogService, private app: AppService, private zone: NgZone, private ngbModal: NgbModal, private hostApp: HostAppService, + private passwordStorage: PasswordStorageService, ) { - } - - savePassword (connection: SSHConnection, password: string) { - if (xkeychain) { - xkeychain.setPassword({ - account: connection.user, - service: `ssh@${connection.host}`, - password - }, () => null) - } else { - wincredmgr.WriteCredentials( - 'user', - password, - `ssh:${connection.user}@${connection.host}`, - ) - } - } - - deletePassword (connection: SSHConnection) { - if (xkeychain) { - xkeychain.deletePassword({ - account: connection.user, - service: `ssh@${connection.host}`, - }, () => null) - } else { - wincredmgr.DeleteCredentials( - `ssh:${connection.user}@${connection.host}`, - ) - } - } - - loadPassword (connection: SSHConnection): Promise { - return new Promise(resolve => { - if (xkeychain) { - xkeychain.getPassword({ - account: connection.user, - service: `ssh@${connection.host}`, - }, (_, result) => resolve(result)) - } else { - try { - resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password) - } catch (error) { - resolve(null) - } - } - }) + this.logger = log.create('ssh') } async connect (connection: SSHConnection): Promise { let privateKey: string = null - let keyPath = path.join(process.env.HOME, '.ssh', 'id_rsa') - if (!connection.privateKey && await fs.exists(keyPath)) { - connection.privateKey = keyPath + let privateKeyPassphrase: string = null + let privateKeyPath = connection.privateKey + if (!privateKeyPath) { + let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa') + if (await fs.exists(userKeyPath)) { + this.logger.info('Using user\'s default private key:', userKeyPath) + privateKeyPath = userKeyPath + } } - if (connection.privateKey) { + if (privateKeyPath) { try { - privateKey = (await fs.readFile(connection.privateKey)).toString() - } catch (error) { } + privateKey = (await fs.readFile(privateKeyPath)).toString() + } catch (error) { + // notify: couldn't read key + } + + if (privateKey) { + this.logger.info('Loaded private key from', privateKeyPath) + + if (privateKey.includes('ENCRYPTED')) { + let modal = this.ngbModal.open(PromptModalComponent) + modal.componentInstance.prompt = 'Private key passphrase' + modal.componentInstance.password = true + try { + privateKeyPassphrase = await modal.result + } catch (_err) { } + } + } } let ssh = new Client() @@ -98,12 +65,12 @@ export class SSHService { ssh.on('ready', () => { connected = true if (savedPassword) { - this.savePassword(connection, savedPassword) + this.passwordStorage.savePassword(connection, savedPassword) } this.zone.run(resolve) }) ssh.on('error', error => { - this.deletePassword(connection) + this.passwordStorage.deletePassword(connection) this.zone.run(() => { if (connected) { alert(`SSH error: ${error}`) @@ -136,6 +103,7 @@ export class SSHService { username: connection.user, password: privateKey ? undefined : '', privateKey, + passphrase: privateKeyPassphrase, tryKeyboard: true, agent, agentForward: !!agent, @@ -148,8 +116,8 @@ export class SSHService { return connection.password } - if (!keychainPasswordUsed && (wincredmgr || xkeychain.isSupported())) { - let password = await this.loadPassword(connection) + if (!keychainPasswordUsed) { + let password = await this.passwordStorage.loadPassword(connection) if (password) { keychainPasswordUsed = true return password diff --git a/terminus-ssh/yarn.lock b/terminus-ssh/yarn.lock index 57674b8d..21551c06 100644 --- a/terminus-ssh/yarn.lock +++ b/terminus-ssh/yarn.lock @@ -6,6 +6,10 @@ version "8.0.53" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8" +"@types/openpgp@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/openpgp/-/openpgp-0.0.29.tgz#feabb9d547cb107f7b98fdd51ac616f6cf5aaebd" + "@types/ssh2-streams@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.2.tgz#7aa18b8c2450f17699e9ea18a76efc838188d58d" @@ -744,6 +748,12 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + enhanced-resolve@3.3.0, enhanced-resolve@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3" @@ -1029,7 +1039,7 @@ glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -1182,10 +1192,18 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" +iconv-lite@~0.4.13: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -1369,6 +1387,10 @@ is-regex@^1.0.3: dependencies: has "^1.0.1" +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1708,6 +1730,13 @@ nanomatch@^1.2.5: snapdragon "^0.8.1" to-regex "^3.0.1" +node-fetch@^1.3.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-libs-browser@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" @@ -1736,6 +1765,12 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" +node-localstorage@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-localstorage/-/node-localstorage-1.3.0.tgz#2e436aae8dcc9ace97b43c65c16c0d577be0a55c" + dependencies: + write-file-atomic "^1.1.4" + node-pre-gyp@^0.6.39: version "0.6.39" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" @@ -1844,6 +1879,13 @@ once@^1.3.0, once@^1.3.3: dependencies: wrappy "1" +openpgp@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-2.6.1.tgz#7d9da10433e37d87300fbac1fe173c80f0a908c9" + dependencies: + node-fetch "^1.3.3" + node-localstorage "~1.3.0" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -2408,6 +2450,10 @@ single-line-log@^1.1.2: dependencies: string-width "^1.0.1" +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -2901,6 +2947,14 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +write-file-atomic@^1.1.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + xkeychain@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/xkeychain/-/xkeychain-0.0.6.tgz#1c58b3dd2f80481f8f67949c3511aa14027c2b9b"