1 /*
2 * SSHTools - Java SSH2 API
3 *
4 * Copyright (C) 2002-2003 Lee David Painter and Contributors.
5 *
6 * Contributions made by:
7 *
8 * Brett Smith
9 * Richard Pernavas
10 * Erwin Bolwidt
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 */
26 package com.sshtools.j2ssh.transport;
27
28 import com.sshtools.j2ssh.transport.publickey.SshKeyPairFactory;
29 import com.sshtools.j2ssh.transport.publickey.SshPublicKey;
30 import com.sshtools.j2ssh.util.Base64;
31
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34
35 import java.io.BufferedReader;
36 import java.io.File;
37 import java.io.FileInputStream;
38 import java.io.FileOutputStream;
39 import java.io.FilePermission;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.InputStreamReader;
43
44 import java.security.AccessControlException;
45 import java.security.AccessController;
46
47 import java.util.HashMap;
48 import java.util.Iterator;
49 import java.util.Map;
50 import java.util.StringTokenizer;
51
52
53 /**
54 * <p>
55 * An abstract <code>HostKeyVerification</code> class providing validation
56 * against the known_hosts format.
57 * </p>
58 *
59 * @author Lee David Painter
60 * @version $Revision: 1.18 $
61 *
62 * @since 0.2.0
63 */
64 public abstract class AbstractKnownHostsKeyVerification
65 implements HostKeyVerification {
66 private static String defaultHostFile;
67 private static Log log = LogFactory.getLog(HostKeyVerification.class);
68
69 //private List deniedHosts = new ArrayList();
70 private Map allowedHosts = new HashMap();
71 private String knownhosts;
72 private boolean hostFileWriteable;
73
74 //private boolean expectEndElement = false;
75 //private String currentElement = null;
76
77 /**
78 * <p>
79 * Constructs a host key verification instance reading the specified
80 * known_hosts file.
81 * </p>
82 *
83 * @param knownhosts the path of the known_hosts file
84 *
85 * @throws InvalidHostFileException if the known_hosts file is invalid
86 *
87 * @since 0.2.0
88 */
89 public AbstractKnownHostsKeyVerification(String knownhosts)
90 throws InvalidHostFileException {
91 InputStream in = null;
92
93 try {
94 // If no host file is supplied, or there is not enough permission to load
95 // the file, then just create an empty list.
96 if (knownhosts != null) {
97 if (System.getSecurityManager() != null) {
98 AccessController.checkPermission(new FilePermission(
99 knownhosts, "read"));
100 }
101
102 // Load the hosts file. Do not worry if fle doesnt exist, just disable
103 // save of
104 File f = new File(knownhosts);
105
106 if (f.exists()) {
107 in = new FileInputStream(f);
108
109 BufferedReader reader = new BufferedReader(new InputStreamReader(
110 in));
111 String line;
112
113 while ((line = reader.readLine()) != null) {
114 StringTokenizer tokens = new StringTokenizer(line, " ");
115 String host = (String) tokens.nextElement();
116 String algorithm = (String) tokens.nextElement();
117 String key = (String) tokens.nextElement();
118
119 SshPublicKey pk = SshKeyPairFactory.decodePublicKey(Base64.decode(
120 key));
121 /*if (host.indexOf(",") > -1) {
122 host = host.substring(0, host.indexOf(","));
123 }*/
124 putAllowedKey(host, pk);
125
126 //allowedHosts.put(host + "#" + pk.getAlgorithmName(), pk);
127 }
128
129 reader.close();
130 hostFileWriteable = f.canWrite();
131 } else {
132 // Try to create the file and its parents if necersary
133 f.getParentFile().mkdirs();
134
135 if (f.createNewFile()) {
136 FileOutputStream out = new FileOutputStream(f);
137 out.write(toString().getBytes());
138 out.close();
139 hostFileWriteable = true;
140 } else {
141 hostFileWriteable = false;
142 }
143 }
144
145 if (!hostFileWriteable) {
146 log.warn("Host file is not writeable.");
147 }
148
149 this.knownhosts = knownhosts;
150 }
151 } catch (AccessControlException ace) {
152 hostFileWriteable = false;
153 log.warn(
154 "Not enough permission to load a hosts file, so just creating an empty list");
155 } catch (IOException ioe) {
156 hostFileWriteable = false;
157 log.info("Could not open or read " + knownhosts + ": " +
158 ioe.getMessage());
159 } finally {
160 if (in != null) {
161 try {
162 in.close();
163 } catch (IOException ioe) {
164 }
165 }
166 }
167 }
168
169 /**
170 * <p>
171 * Determines whether the host file is writable.
172 * </p>
173 *
174 * @return true if the host file is writable, otherwise false
175 *
176 * @since 0.2.0
177 */
178 public boolean isHostFileWriteable() {
179 return hostFileWriteable;
180 }
181
182 /**
183 * <p>
184 * Called by the <code>verifyHost</code> method when the host key supplied
185 * by the host does not match the current key recording in the known hosts
186 * file.
187 * </p>
188 *
189 * @param host the name of the host
190 * @param allowedHostKey the current key recorded in the known_hosts file.
191 * @param actualHostKey the actual key supplied by the user
192 *
193 * @throws TransportProtocolException if an error occurs
194 *
195 * @since 0.2.0
196 */
197 public abstract void onHostKeyMismatch(String host,
198 SshPublicKey allowedHostKey, SshPublicKey actualHostKey)
199 throws TransportProtocolException;
200
201 /**
202 * <p>
203 * Called by the <code>verifyHost</code> method when the host key supplied
204 * is not recorded in the known_hosts file.
205 * </p>
206 *
207 * <p></p>
208 *
209 * @param host the name of the host
210 * @param key the public key supplied by the host
211 *
212 * @throws TransportProtocolException if an error occurs
213 *
214 * @since 0.2.0
215 */
216 public abstract void onUnknownHost(String host, SshPublicKey key)
217 throws TransportProtocolException;
218
219 /**
220 * <p>
221 * Allows a host key, optionally recording the key to the known_hosts file.
222 * </p>
223 *
224 * @param host the name of the host
225 * @param pk the public key to allow
226 * @param always true if the key should be written to the known_hosts file
227 *
228 * @throws InvalidHostFileException if the host file cannot be written
229 *
230 * @since 0.2.0
231 */
232 public void allowHost(String host, SshPublicKey pk, boolean always)
233 throws InvalidHostFileException {
234 if (log.isDebugEnabled()) {
235 log.debug("Allowing " + host + " with fingerprint " +
236 pk.getFingerprint());
237 }
238
239 // Put the host into the allowed hosts list, overiding any previous
240 // entry
241 putAllowedKey(host, pk);
242
243 //allowedHosts.put(host, pk);
244 // If we always want to allow then save the host file with the
245 // new details
246 if (always) {
247 saveHostFile();
248 }
249 }
250
251 /**
252 * <p>
253 * Returns a Map of the allowed hosts.
254 * </p>
255 *
256 * <p>
257 * The keys of the returned Map are comma separated strings of
258 * "hostname,ipaddress". The value objects are Maps containing a string
259 * key of the public key alogorithm name and the public key as the value.
260 * </p>
261 *
262 * @return the allowed hosts
263 *
264 * @since 0.2.0
265 */
266 public Map allowedHosts() {
267 return allowedHosts;
268 }
269
270 /**
271 * <p>
272 * Removes an allowed host.
273 * </p>
274 *
275 * @param host the host to remove
276 *
277 * @since 0.2.0
278 */
279 public void removeAllowedHost(String host) {
280 Iterator it = allowedHosts.keySet().iterator();
281
282 while (it.hasNext()) {
283 StringTokenizer tokens = new StringTokenizer((String) it.next(), ",");
284
285 while (tokens.hasMoreElements()) {
286 String name = (String) tokens.nextElement();
287
288 if (name.equals(host)) {
289 allowedHosts.remove(name);
290 }
291 }
292 }
293 }
294
295 /**
296 * <p>
297 * Verifies a host key against the list of known_hosts.
298 * </p>
299 *
300 * <p>
301 * If the host unknown or the key does not match the currently allowed host
302 * key the abstract <code>onUnknownHost</code> or
303 * <code>onHostKeyMismatch</code> methods are called so that the caller
304 * may identify and allow the host.
305 * </p>
306 *
307 * @param host the name of the host
308 * @param pk the host key supplied
309 *
310 * @return true if the host is accepted, otherwise false
311 *
312 * @throws TransportProtocolException if an error occurs
313 *
314 * @since 0.2.0
315 */
316 public boolean verifyHost(String host, SshPublicKey pk)
317 throws TransportProtocolException {
318 String fingerprint = pk.getFingerprint();
319 log.info("Verifying " + host + " host key");
320
321 if (log.isDebugEnabled()) {
322 log.debug("Fingerprint: " + fingerprint);
323 }
324
325 Iterator it = allowedHosts.keySet().iterator();
326
327 while (it.hasNext()) {
328 // Could be a comma delimited string of names/ip addresses
329 String names = (String) it.next();
330
331 if (names.equals(host)) {
332 return validateHost(names, pk);
333 }
334
335 StringTokenizer tokens = new StringTokenizer(names, ",");
336
337 while (tokens.hasMoreElements()) {
338 // Try the allowed hosts by looking at the allowed hosts map
339 String name = (String) tokens.nextElement();
340
341 if (name.equalsIgnoreCase(host)) {
342 return validateHost(names, pk);
343 }
344 }
345 }
346
347 // The host is unknown os ask the user
348 onUnknownHost(host, pk);
349
350 // Recheck ans return the result
351 return checkKey(host, pk);
352 }
353
354 private boolean validateHost(String names, SshPublicKey pk)
355 throws TransportProtocolException {
356 // The host is allowed so check the fingerprint
357 SshPublicKey pub = getAllowedKey(names, pk.getAlgorithmName()); //shPublicKey) allowedHosts.get(host + "#" + pk.getAlgorithmName());
358
359 if ((pub != null) && pk.equals(pub)) {
360 return true;
361 } else {
362 // The host key does not match the recorded so call the abstract
363 // method so that the user can decide
364 if (pub == null) {
365 onUnknownHost(names, pk);
366 } else {
367 onHostKeyMismatch(names, pub, pk);
368 }
369
370 // Recheck the after the users input
371 return checkKey(names, pk);
372 }
373 }
374
375 private boolean checkKey(String host, SshPublicKey key) {
376 SshPublicKey pk = getAllowedKey(host, key.getAlgorithmName()); //shPublicKey) allowedHosts.get(host + "#" + key.getAlgorithmName());
377
378 if (pk != null) {
379 if (pk.equals(key)) {
380 return true;
381 }
382 }
383
384 return false;
385 }
386
387 private SshPublicKey getAllowedKey(String names, String algorithm) {
388 if (allowedHosts.containsKey(names)) {
389 Map map = (Map) allowedHosts.get(names);
390
391 return (SshPublicKey) map.get(algorithm);
392 }
393
394 return null;
395 }
396
397 private void putAllowedKey(String host, SshPublicKey key) {
398 if (!allowedHosts.containsKey(host)) {
399 allowedHosts.put(host, new HashMap());
400 }
401
402 Map map = (Map) allowedHosts.get(host);
403 map.put(key.getAlgorithmName(), key);
404 }
405
406 /**
407 * <p>
408 * Save's the host key file to be saved.
409 * </p>
410 *
411 * @throws InvalidHostFileException if the host file is invalid
412 *
413 * @since 0.2.0
414 */
415 public void saveHostFile() throws InvalidHostFileException {
416 if (!hostFileWriteable) {
417 throw new InvalidHostFileException("Host file is not writeable.");
418 }
419
420 log.info("Saving " + defaultHostFile);
421
422 try {
423 File f = new File(knownhosts);
424 FileOutputStream out = new FileOutputStream(f);
425 out.write(toString().getBytes());
426 out.close();
427 } catch (IOException e) {
428 throw new InvalidHostFileException("Could not write to " +
429 knownhosts);
430 }
431 }
432
433 /**
434 * <p>
435 * Outputs the allowed hosts in the known_hosts file format.
436 * </p>
437 *
438 * <p>
439 * The format consists of any number of lines each representing one key for
440 * a single host.
441 * </p>
442 * <code> titan,192.168.1.12 ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4Ed.....
443 * titan,192.168.1.12 ssh-rsa AAAAB3NzaC1kc3MAAACBAP1/U4Ed.....
444 * einstein,192.168.1.40 ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4Ed..... </code>
445 *
446 * @return
447 *
448 * @since 0.2.0
449 */
450 public String toString() {
451 String knownhosts = "";
452 Map.Entry entry;
453 Map.Entry entry2;
454 Iterator it = allowedHosts.entrySet().iterator();
455
456 while (it.hasNext()) {
457 entry = (Map.Entry) it.next();
458
459 Iterator it2 = ((Map) entry.getValue()).entrySet().iterator();
460
461 while (it2.hasNext()) {
462 entry2 = (Map.Entry) it2.next();
463
464 SshPublicKey pk = (SshPublicKey) entry2.getValue();
465 knownhosts += (entry.getKey().toString() + " " +
466 pk.getAlgorithmName() + " " +
467 Base64.encodeBytes(pk.getEncoded(), true) + "\n");
468 }
469 }
470
471 return knownhosts;
472 }
473 }