Skip to content

Commit 606d878

Browse files
committed
Added keychain integration on OSX
1 parent 4193dfe commit 606d878

File tree

3 files changed

+76
-11
lines changed

3 files changed

+76
-11
lines changed

src/clj_ssh/keychain.clj

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
(ns clj-ssh.keychain
2+
"Primitive keychain support for clj-ssh. Only implemented on OSX at the moment."
3+
(:require
4+
[clojure.contrib.logging :as logging]))
5+
6+
;; working towards clojure 1.1/1.2 compat
7+
(try
8+
(require '[clojure.contrib.shell :as shell])
9+
(catch Exception e
10+
(require '[clojure.contrib.shell-out :as shell])))
11+
12+
(defn ask-passphrase [path]
13+
(when-let [console (. System console)]
14+
(print "Passphrase for" path ": ")
15+
(.readPassword console)))
16+
17+
(defmulti keychain-passphrase "Obtain password for path" (fn [system path] system))
18+
19+
(defmethod keychain-passphrase :default
20+
[system path]
21+
(logging/warn "Passphrase required, but no keychain implemented.")
22+
(ask-passphrase path))
23+
24+
(defmethod keychain-passphrase "Mac OS X"
25+
[system path]
26+
(let [result (shell/sh
27+
:return-map true
28+
"/usr/bin/security" "find-generic-password" "-a"
29+
(format "%s" path)
30+
"-g")]
31+
(when (zero? (result :exit))
32+
(second (re-find #"password: \"(.*)\"" (result :err))))))
33+
34+
(defn passphrase
35+
"Obtain a passphrase for the given key path"
36+
[path]
37+
(keychain-passphrase (System/getProperty "os.name") path))
38+

src/clj_ssh/ssh.clj

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Licensed under EPL (http://www.eclipse.org/legal/epl-v10.html)"
4040
(:use
4141
[clojure.contrib.def :only [defvar]])
4242
(:require
43+
clj-ssh.keychain
4344
[clojure.contrib.logging :as logging])
4445
(:import [com.jcraft.jsch
4546
JSch Session Channel ChannelShell ChannelExec ChannelSftp
@@ -123,12 +124,19 @@ Licensed under EPL (http://www.eclipse.org/legal/epl-v10.html)"
123124
(if (.canRead id-file)
124125
id-file)))
125126

127+
(defn has-identity?
128+
"Check if the given identity is present."
129+
([name] (has-identity? *ssh-agent* name))
130+
([agent name] (some #(= name %) (.getIdentityNames agent))))
131+
126132
(defn make-identity
127133
"Create a JSch identity. This can be used to check whether the key is
128134
encrypted."
129135
([private-key-path public-key-path]
130136
(make-identity *ssh-agent* private-key-path public-key-path))
131137
([#^JSch agent #^String private-key-path #^String public-key-path]
138+
(logging/trace
139+
(format "Make identity %s %s" private-key-path public-key-path))
132140
(call-method
133141
com.jcraft.jsch.IdentityFile 'newInstance [String String JSch]
134142
nil private-key-path public-key-path agent)))
@@ -139,7 +147,7 @@ Licensed under EPL (http://www.eclipse.org/legal/epl-v10.html)"
139147
(add-identity *ssh-agent* (default-identity) nil))
140148
([private-key]
141149
(add-identity *ssh-agent* private-key nil))
142-
([#^JSch agent private-key]
150+
([agent private-key]
143151
(if (ssh-agent? agent)
144152
(add-identity agent private-key nil)
145153
(add-identity *ssh-agent* agent private-key)))
@@ -151,10 +159,22 @@ Licensed under EPL (http://www.eclipse.org/legal/epl-v10.html)"
151159
(file-path private-key))
152160
(and passphrase (.getBytes passphrase)))))
153161

154-
(defn has-identity?
155-
"Check if the given identity is present."
156-
([name] (has-identity? *ssh-agent* name))
157-
([agent name] (some #(= name %) (.getIdentityNames *ssh-agent*))))
162+
(defn add-identity-with-keychain
163+
"Add a private key, only if not already known, using the keychain to obtain
164+
a passphrase if required"
165+
([] (add-identity-with-keychain *ssh-agent* (default-identity)))
166+
([private-key-path] (add-identity-with-keychain *ssh-agent* private-key-path))
167+
([agent private-key-path]
168+
(when-not (has-identity? agent private-key-path)
169+
(let [identity (make-identity
170+
agent
171+
(file-path private-key-path)
172+
(str private-key-path ".pub"))]
173+
(if (.isEncrypted identity)
174+
(if-let [passphrase (clj-ssh.keychain/passphrase private-key-path)]
175+
(add-identity agent identity passphrase)
176+
(logging/error "Passphrase required, but none findable."))
177+
(add-identity agent identity))))))
158178

159179
(defn create-ssh-agent
160180
"Create an ssh-agent. By default try and add the current user's id_rsa key."
@@ -166,13 +186,13 @@ Licensed under EPL (http://www.eclipse.org/legal/epl-v10.html)"
166186
(let [agent (JSch.)]
167187
(when add-default-identity?
168188
(if-let [default-id (default-identity)]
169-
(add-identity agent default-id)))
189+
(add-identity-with-keychain agent default-id)))
170190
agent)))
171191
([private-key passphrase?]
172192
(let [agent (JSch.)]
173193
(if passphrase?
174194
(add-identity agent private-key passphrase?)
175-
(add-identity agent private-key))
195+
(add-identity-with-keychain agent private-key))
176196
agent)))
177197

178198
(defmacro with-ssh-agent

test/clj_ssh/ssh_test.clj

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ list, Alan Dipert and MeikelBrandmeyer."
4141
(defn home
4242
[] (. System getProperty "user.home"))
4343

44+
(defn with-test-private-key-path
45+
[f]
46+
(binding [clj-ssh.ssh/*default-identity* (private-key-path)]
47+
(f)))
48+
49+
(use-fixtures :once with-test-private-key-path)
50+
4451
(with-private-vars [clj-ssh.ssh [file-path camelize]]
4552

4653
(deftest file-path-test
@@ -51,9 +58,9 @@ list, Alan Dipert and MeikelBrandmeyer."
5158
(is (= "StrictHostKeyChecking" (camelize "strict-host-key-checking")))))
5259

5360

54-
(deftest default-identity-test
55-
(is (= (str (. System getProperty "user.home") "/.ssh/id_rsa")
56-
(.getPath (default-identity)))))
61+
;; (deftest default-identity-test
62+
;; (is (= (str (. System getProperty "user.home") "/.ssh/id_rsa")
63+
;; (.getPath (default-identity)))))
5764

5865
(deftest default-session-options-test
5966
(let [old *default-session-options*
@@ -68,7 +75,7 @@ list, Alan Dipert and MeikelBrandmeyer."
6875
(is (not (ssh-agent? "i'm not an ssh-agent"))))
6976

7077
(deftest create-ssh-agent-test
71-
(is (ssh-agent? (create-ssh-agent)))
78+
(is (ssh-agent? (create-ssh-agent false)))
7279
(is (ssh-agent? (create-ssh-agent (private-key-path)))))
7380

7481
(deftest with-ssh-agent-test

0 commit comments

Comments
 (0)