Browse Source

support encrypted private ssh keys (fixes #262)

Eugene Pankov 8 years ago
parent
commit
ccbcd30813

+ 2 - 0
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 },

+ 73 - 0
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<string> {
+        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)
+                }
+            }
+        })
+    }
+}

+ 38 - 70
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<string> {
-        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<TerminalTabComponent> {
         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

+ 55 - 1
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"
+
 [email protected], 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"