0
|
1
|
|
2 package com.trilead.ssh2;
|
|
3
|
|
4 import java.io.BufferedReader;
|
|
5 import java.io.CharArrayReader;
|
|
6 import java.io.CharArrayWriter;
|
|
7 import java.io.File;
|
|
8 import java.io.FileReader;
|
|
9 import java.io.IOException;
|
|
10 import java.io.RandomAccessFile;
|
|
11 import java.io.UnsupportedEncodingException;
|
|
12 import java.net.InetAddress;
|
|
13 import java.net.UnknownHostException;
|
|
14 import java.security.InvalidKeyException;
|
|
15 import java.security.MessageDigest;
|
|
16 import java.security.NoSuchAlgorithmException;
|
|
17 import java.security.PublicKey;
|
|
18 import java.security.SecureRandom;
|
|
19 import java.security.interfaces.DSAPublicKey;
|
|
20 import java.security.interfaces.ECPublicKey;
|
|
21 import java.security.interfaces.RSAPublicKey;
|
|
22 import java.util.Iterator;
|
|
23 import java.util.LinkedList;
|
|
24 import java.util.Locale;
|
|
25 import java.util.Vector;
|
|
26
|
|
27 import javax.crypto.Mac;
|
|
28 import javax.crypto.spec.SecretKeySpec;
|
|
29
|
|
30 import com.trilead.ssh2.crypto.Base64;
|
|
31 import com.trilead.ssh2.signature.DSASHA1Verify;
|
|
32 import com.trilead.ssh2.signature.ECDSASHA2Verify;
|
|
33 import com.trilead.ssh2.signature.RSASHA1Verify;
|
|
34
|
|
35
|
|
36 /**
|
|
37 * The <code>KnownHosts</code> class is a handy tool to verify received server hostkeys
|
|
38 * based on the information in <code>known_hosts</code> files (the ones used by OpenSSH).
|
|
39 * <p>
|
|
40 * It offers basically an in-memory database for known_hosts entries, as well as some
|
|
41 * helper functions. Entries from a <code>known_hosts</code> file can be loaded at construction time.
|
|
42 * It is also possible to add more keys later (e.g., one can parse different
|
|
43 * <code>known_hosts<code> files).
|
|
44 * <p>
|
|
45 * It is a thread safe implementation, therefore, you need only to instantiate one
|
|
46 * <code>KnownHosts</code> for your whole application.
|
|
47 *
|
|
48 * @author Christian Plattner, plattner@trilead.com
|
|
49 * @version $Id: KnownHosts.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $
|
|
50 */
|
|
51
|
|
52 public class KnownHosts {
|
|
53 public static final int HOSTKEY_IS_OK = 0;
|
|
54 public static final int HOSTKEY_IS_NEW = 1;
|
|
55 public static final int HOSTKEY_HAS_CHANGED = 2;
|
|
56
|
|
57 private class KnownHostsEntry {
|
|
58 String[] patterns;
|
|
59 PublicKey key;
|
|
60
|
|
61 KnownHostsEntry(String[] patterns, PublicKey key) {
|
|
62 this.patterns = patterns;
|
|
63 this.key = key;
|
|
64 }
|
|
65 }
|
|
66
|
|
67 private LinkedList<KnownHostsEntry> publicKeys = new LinkedList<KnownHostsEntry>();
|
|
68
|
|
69 public KnownHosts() {
|
|
70 }
|
|
71
|
|
72 public KnownHosts(char[] knownHostsData) throws IOException {
|
|
73 initialize(knownHostsData);
|
|
74 }
|
|
75
|
|
76 public KnownHosts(File knownHosts) throws IOException {
|
|
77 initialize(knownHosts);
|
|
78 }
|
|
79
|
|
80 /**
|
|
81 * Adds a single public key entry to the database. Note: this will NOT add the public key
|
|
82 * to any physical file (e.g., "~/.ssh/known_hosts") - use <code>addHostkeyToFile()</code> for that purpose.
|
|
83 * This method is designed to be used in a {@link ServerHostKeyVerifier}.
|
|
84 *
|
|
85 * @param hostnames a list of hostname patterns - at least one most be specified. Check out the
|
|
86 * OpenSSH sshd man page for a description of the pattern matching algorithm.
|
|
87 * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}.
|
|
88 * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}.
|
|
89 * @throws IOException
|
|
90 */
|
|
91 public void addHostkey(String hostnames[], String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException {
|
|
92 if (hostnames == null)
|
|
93 throw new IllegalArgumentException("hostnames may not be null");
|
|
94
|
|
95 if ("ssh-rsa".equals(serverHostKeyAlgorithm)) {
|
|
96 RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey);
|
|
97
|
|
98 synchronized (publicKeys) {
|
|
99 publicKeys.add(new KnownHostsEntry(hostnames, rpk));
|
|
100 }
|
|
101 }
|
|
102 else if ("ssh-dss".equals(serverHostKeyAlgorithm)) {
|
|
103 DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey);
|
|
104
|
|
105 synchronized (publicKeys) {
|
|
106 publicKeys.add(new KnownHostsEntry(hostnames, dpk));
|
|
107 }
|
|
108 }
|
|
109 else if (serverHostKeyAlgorithm.startsWith(ECDSASHA2Verify.ECDSA_SHA2_PREFIX)) {
|
|
110 ECPublicKey epk = ECDSASHA2Verify.decodeSSHECDSAPublicKey(serverHostKey);
|
|
111
|
|
112 synchronized (publicKeys) {
|
|
113 publicKeys.add(new KnownHostsEntry(hostnames, epk));
|
|
114 }
|
|
115 }
|
|
116 else
|
|
117 throw new IOException("Unknwon host key type (" + serverHostKeyAlgorithm + ")");
|
|
118 }
|
|
119
|
|
120 /**
|
|
121 * Parses the given known_hosts data and adds entries to the database.
|
|
122 *
|
|
123 * @param knownHostsData
|
|
124 * @throws IOException
|
|
125 */
|
|
126 public void addHostkeys(char[] knownHostsData) throws IOException {
|
|
127 initialize(knownHostsData);
|
|
128 }
|
|
129
|
|
130 /**
|
|
131 * Parses the given known_hosts file and adds entries to the database.
|
|
132 *
|
|
133 * @param knownHosts
|
|
134 * @throws IOException
|
|
135 */
|
|
136 public void addHostkeys(File knownHosts) throws IOException {
|
|
137 initialize(knownHosts);
|
|
138 }
|
|
139
|
|
140 /**
|
|
141 * Generate the hashed representation of the given hostname. Useful for adding entries
|
|
142 * with hashed hostnames to a known_hosts file. (see -H option of OpenSSH key-gen).
|
|
143 *
|
|
144 * @param hostname
|
|
145 * @return the hashed representation, e.g., "|1|cDhrv7zwEUV3k71CEPHnhHZezhA=|Xo+2y6rUXo2OIWRAYhBOIijbJMA="
|
|
146 */
|
|
147 public static final String createHashedHostname(String hostname) {
|
|
148 MessageDigest sha1;
|
|
149
|
|
150 try {
|
|
151 sha1 = MessageDigest.getInstance("SHA1");
|
|
152 }
|
|
153 catch (NoSuchAlgorithmException e) {
|
|
154 throw new RuntimeException("VM doesn't support SHA1", e);
|
|
155 }
|
|
156
|
|
157 byte[] salt = new byte[sha1.getDigestLength()];
|
|
158 new SecureRandom().nextBytes(salt);
|
|
159 byte[] hash = hmacSha1Hash(salt, hostname);
|
|
160 String base64_salt = new String(Base64.encode(salt));
|
|
161 String base64_hash = new String(Base64.encode(hash));
|
|
162 return new String("|1|" + base64_salt + "|" + base64_hash);
|
|
163 }
|
|
164
|
|
165 private static final byte[] hmacSha1Hash(byte[] salt, String hostname) {
|
|
166 Mac hmac;
|
|
167
|
|
168 try {
|
|
169 hmac = Mac.getInstance("HmacSHA1");
|
|
170
|
|
171 if (salt.length != hmac.getMacLength())
|
|
172 throw new IllegalArgumentException("Salt has wrong length (" + salt.length + ")");
|
|
173
|
|
174 hmac.init(new SecretKeySpec(salt, "HmacSHA1"));
|
|
175 }
|
|
176 catch (NoSuchAlgorithmException e) {
|
|
177 throw new RuntimeException("Unable to HMAC-SHA1", e);
|
|
178 }
|
|
179 catch (InvalidKeyException e) {
|
|
180 throw new RuntimeException("Unable to create SecretKey", e);
|
|
181 }
|
|
182
|
|
183 try {
|
|
184 hmac.update(hostname.getBytes("ISO-8859-1"));
|
|
185 }
|
|
186 catch (UnsupportedEncodingException ignore) {
|
|
187 /* Actually, ISO-8859-1 is supported by all correct
|
|
188 * Java implementations. But... you never know. */
|
|
189 hmac.update(hostname.getBytes());
|
|
190 }
|
|
191
|
|
192 return hmac.doFinal();
|
|
193 }
|
|
194
|
|
195 private final boolean checkHashed(String entry, String hostname) {
|
|
196 if (entry.startsWith("|1|") == false)
|
|
197 return false;
|
|
198
|
|
199 int delim_idx = entry.indexOf('|', 3);
|
|
200
|
|
201 if (delim_idx == -1)
|
|
202 return false;
|
|
203
|
|
204 String salt_base64 = entry.substring(3, delim_idx);
|
|
205 String hash_base64 = entry.substring(delim_idx + 1);
|
|
206 byte[] salt = null;
|
|
207 byte[] hash = null;
|
|
208
|
|
209 try {
|
|
210 salt = Base64.decode(salt_base64.toCharArray());
|
|
211 hash = Base64.decode(hash_base64.toCharArray());
|
|
212 }
|
|
213 catch (IOException e) {
|
|
214 return false;
|
|
215 }
|
|
216
|
|
217 try {
|
|
218 MessageDigest sha1 = MessageDigest.getInstance("SHA1");
|
|
219
|
|
220 if (salt.length != sha1.getDigestLength())
|
|
221 return false;
|
|
222 }
|
|
223 catch (NoSuchAlgorithmException e) {
|
|
224 throw new RuntimeException("VM does not support SHA1", e);
|
|
225 }
|
|
226
|
|
227 byte[] dig = hmacSha1Hash(salt, hostname);
|
|
228
|
|
229 for (int i = 0; i < dig.length; i++)
|
|
230 if (dig[i] != hash[i])
|
|
231 return false;
|
|
232
|
|
233 return true;
|
|
234 }
|
|
235
|
|
236 private int checkKey(String remoteHostname, PublicKey remoteKey) {
|
|
237 int result = HOSTKEY_IS_NEW;
|
|
238
|
|
239 synchronized (publicKeys) {
|
|
240 Iterator<KnownHostsEntry> i = publicKeys.iterator();
|
|
241
|
|
242 while (i.hasNext()) {
|
|
243 KnownHostsEntry ke = i.next();
|
|
244
|
|
245 if (hostnameMatches(ke.patterns, remoteHostname) == false)
|
|
246 continue;
|
|
247
|
|
248 boolean res = matchKeys(ke.key, remoteKey);
|
|
249
|
|
250 if (res == true)
|
|
251 return HOSTKEY_IS_OK;
|
|
252
|
|
253 result = HOSTKEY_HAS_CHANGED;
|
|
254 }
|
|
255 }
|
|
256
|
|
257 return result;
|
|
258 }
|
|
259
|
|
260 private Vector<PublicKey> getAllKeys(String hostname) {
|
|
261 Vector<PublicKey> keys = new Vector<PublicKey>();
|
|
262
|
|
263 synchronized (publicKeys) {
|
|
264 Iterator<KnownHostsEntry> i = publicKeys.iterator();
|
|
265
|
|
266 while (i.hasNext()) {
|
|
267 KnownHostsEntry ke = i.next();
|
|
268
|
|
269 if (hostnameMatches(ke.patterns, hostname) == false)
|
|
270 continue;
|
|
271
|
|
272 keys.addElement(ke.key);
|
|
273 }
|
|
274 }
|
|
275
|
|
276 return keys;
|
|
277 }
|
|
278
|
|
279 /**
|
|
280 * Try to find the preferred order of hostkey algorithms for the given hostname.
|
|
281 * Based on the type of hostkey that is present in the internal database
|
|
282 * (i.e., either <code>ssh-rsa</code> or <code>ssh-dss</code>)
|
|
283 * an ordered list of hostkey algorithms is returned which can be passed
|
|
284 * to <code>Connection.setServerHostKeyAlgorithms</code>.
|
|
285 *
|
|
286 * @param hostname
|
|
287 * @return <code>null</code> if no key for the given hostname is present or
|
|
288 * there are keys of multiple types present for the given hostname. Otherwise,
|
|
289 * an array with hostkey algorithms is returned (i.e., an array of length 2).
|
|
290 */
|
|
291 public String[] getPreferredServerHostkeyAlgorithmOrder(String hostname) {
|
|
292 String[] algos = recommendHostkeyAlgorithms(hostname);
|
|
293
|
|
294 if (algos != null)
|
|
295 return algos;
|
|
296
|
|
297 InetAddress[] ipAdresses = null;
|
|
298
|
|
299 try {
|
|
300 ipAdresses = InetAddress.getAllByName(hostname);
|
|
301 }
|
|
302 catch (UnknownHostException e) {
|
|
303 return null;
|
|
304 }
|
|
305
|
|
306 for (int i = 0; i < ipAdresses.length; i++) {
|
|
307 algos = recommendHostkeyAlgorithms(ipAdresses[i].getHostAddress());
|
|
308
|
|
309 if (algos != null)
|
|
310 return algos;
|
|
311 }
|
|
312
|
|
313 return null;
|
|
314 }
|
|
315
|
|
316 private final boolean hostnameMatches(String[] hostpatterns, String hostname) {
|
|
317 boolean isMatch = false;
|
|
318 boolean negate = false;
|
|
319 hostname = hostname.toLowerCase(Locale.US);
|
|
320
|
|
321 for (int k = 0; k < hostpatterns.length; k++) {
|
|
322 if (hostpatterns[k] == null)
|
|
323 continue;
|
|
324
|
|
325 String pattern = null;
|
|
326
|
|
327 /* In contrast to OpenSSH we also allow negated hash entries (as well as hashed
|
|
328 * entries in lines with multiple entries).
|
|
329 */
|
|
330
|
|
331 if ((hostpatterns[k].length() > 0) && (hostpatterns[k].charAt(0) == '!')) {
|
|
332 pattern = hostpatterns[k].substring(1);
|
|
333 negate = true;
|
|
334 }
|
|
335 else {
|
|
336 pattern = hostpatterns[k];
|
|
337 negate = false;
|
|
338 }
|
|
339
|
|
340 /* Optimize, no need to check this entry */
|
|
341
|
|
342 if ((isMatch) && (negate == false))
|
|
343 continue;
|
|
344
|
|
345 /* Now compare */
|
|
346
|
|
347 if (pattern.charAt(0) == '|') {
|
|
348 if (checkHashed(pattern, hostname)) {
|
|
349 if (negate)
|
|
350 return false;
|
|
351
|
|
352 isMatch = true;
|
|
353 }
|
|
354 }
|
|
355 else {
|
|
356 pattern = pattern.toLowerCase(Locale.US);
|
|
357
|
|
358 if ((pattern.indexOf('?') != -1) || (pattern.indexOf('*') != -1)) {
|
|
359 if (pseudoRegex(pattern.toCharArray(), 0, hostname.toCharArray(), 0)) {
|
|
360 if (negate)
|
|
361 return false;
|
|
362
|
|
363 isMatch = true;
|
|
364 }
|
|
365 }
|
|
366 else if (pattern.compareTo(hostname) == 0) {
|
|
367 if (negate)
|
|
368 return false;
|
|
369
|
|
370 isMatch = true;
|
|
371 }
|
|
372 }
|
|
373 }
|
|
374
|
|
375 return isMatch;
|
|
376 }
|
|
377
|
|
378 private void initialize(char[] knownHostsData) throws IOException {
|
|
379 BufferedReader br = new BufferedReader(new CharArrayReader(knownHostsData));
|
|
380
|
|
381 while (true) {
|
|
382 String line = br.readLine();
|
|
383
|
|
384 if (line == null)
|
|
385 break;
|
|
386
|
|
387 line = line.trim();
|
|
388
|
|
389 if (line.startsWith("#"))
|
|
390 continue;
|
|
391
|
|
392 String[] arr = line.split(" ");
|
|
393
|
|
394 if (arr.length >= 3) {
|
|
395 if ((arr[1].compareTo("ssh-rsa") == 0) || (arr[1].compareTo("ssh-dss") == 0)) {
|
|
396 String[] hostnames = arr[0].split(",");
|
|
397 byte[] msg = Base64.decode(arr[2].toCharArray());
|
|
398 addHostkey(hostnames, arr[1], msg);
|
|
399 }
|
|
400 }
|
|
401 }
|
|
402 }
|
|
403
|
|
404 private void initialize(File knownHosts) throws IOException {
|
|
405 char[] buff = new char[512];
|
|
406 CharArrayWriter cw = new CharArrayWriter();
|
|
407 knownHosts.createNewFile();
|
|
408 FileReader fr = new FileReader(knownHosts);
|
|
409
|
|
410 while (true) {
|
|
411 int len = fr.read(buff);
|
|
412
|
|
413 if (len < 0)
|
|
414 break;
|
|
415
|
|
416 cw.write(buff, 0, len);
|
|
417 }
|
|
418
|
|
419 fr.close();
|
|
420 initialize(cw.toCharArray());
|
|
421 }
|
|
422
|
|
423 private final boolean matchKeys(PublicKey key1, PublicKey key2) {
|
|
424 return key1.equals(key2);
|
|
425 }
|
|
426
|
|
427 private final boolean pseudoRegex(char[] pattern, int i, char[] match, int j) {
|
|
428 /* This matching logic is equivalent to the one present in OpenSSH 4.1 */
|
|
429 while (true) {
|
|
430 /* Are we at the end of the pattern? */
|
|
431 if (pattern.length == i)
|
|
432 return (match.length == j);
|
|
433
|
|
434 if (pattern[i] == '*') {
|
|
435 i++;
|
|
436
|
|
437 if (pattern.length == i)
|
|
438 return true;
|
|
439
|
|
440 if ((pattern[i] != '*') && (pattern[i] != '?')) {
|
|
441 while (true) {
|
|
442 if ((pattern[i] == match[j]) && pseudoRegex(pattern, i + 1, match, j + 1))
|
|
443 return true;
|
|
444
|
|
445 j++;
|
|
446
|
|
447 if (match.length == j)
|
|
448 return false;
|
|
449 }
|
|
450 }
|
|
451
|
|
452 while (true) {
|
|
453 if (pseudoRegex(pattern, i, match, j))
|
|
454 return true;
|
|
455
|
|
456 j++;
|
|
457
|
|
458 if (match.length == j)
|
|
459 return false;
|
|
460 }
|
|
461 }
|
|
462
|
|
463 if (match.length == j)
|
|
464 return false;
|
|
465
|
|
466 if ((pattern[i] != '?') && (pattern[i] != match[j]))
|
|
467 return false;
|
|
468
|
|
469 i++;
|
|
470 j++;
|
|
471 }
|
|
472 }
|
|
473
|
|
474 private String[] recommendHostkeyAlgorithms(String hostname) {
|
|
475 String preferredAlgo = null;
|
|
476 Vector<PublicKey> keys = getAllKeys(hostname);
|
|
477
|
|
478 for (int i = 0; i < keys.size(); i++) {
|
|
479 String thisAlgo = null;
|
|
480
|
|
481 if (keys.elementAt(i) instanceof RSAPublicKey)
|
|
482 thisAlgo = "ssh-rsa";
|
|
483 else if (keys.elementAt(i) instanceof DSAPublicKey)
|
|
484 thisAlgo = "ssh-dss";
|
|
485 else
|
|
486 continue;
|
|
487
|
|
488 if (preferredAlgo != null) {
|
|
489 /* If we find different key types, then return null */
|
|
490 if (preferredAlgo.compareTo(thisAlgo) != 0)
|
|
491 return null;
|
|
492
|
|
493 /* OK, we found the same algo again, optimize */
|
|
494 continue;
|
|
495 }
|
|
496 }
|
|
497
|
|
498 /* If we did not find anything that we know of, return null */
|
|
499
|
|
500 if (preferredAlgo == null)
|
|
501 return null;
|
|
502
|
|
503 /* Now put the preferred algo to the start of the array.
|
|
504 * You may ask yourself why we do it that way - basically, we could just
|
|
505 * return only the preferred algorithm: since we have a saved key of that
|
|
506 * type (sent earlier from the remote host), then that should work out.
|
|
507 * However, imagine that the server is (for whatever reasons) not offering
|
|
508 * that type of hostkey anymore (e.g., "ssh-rsa" was disabled and
|
|
509 * now "ssh-dss" is being used). If we then do not let the server send us
|
|
510 * a fresh key of the new type, then we shoot ourself into the foot:
|
|
511 * the connection cannot be established and hence the user cannot decide
|
|
512 * if he/she wants to accept the new key.
|
|
513 */
|
|
514
|
|
515 if (preferredAlgo.equals("ssh-rsa"))
|
|
516 return new String[] { "ssh-rsa", "ssh-dss" };
|
|
517
|
|
518 return new String[] { "ssh-dss", "ssh-rsa" };
|
|
519 }
|
|
520
|
|
521 /**
|
|
522 * Checks the internal hostkey database for the given hostkey.
|
|
523 * If no matching key can be found, then the hostname is resolved to an IP address
|
|
524 * and the search is repeated using that IP address.
|
|
525 *
|
|
526 * @param hostname the server's hostname, will be matched with all hostname patterns
|
|
527 * @param serverHostKeyAlgorithm type of hostkey, either <code>ssh-rsa</code> or <code>ssh-dss</code>
|
|
528 * @param serverHostKey the key blob
|
|
529 * @return <ul>
|
|
530 * <li><code>HOSTKEY_IS_OK</code>: the given hostkey matches an entry for the given hostname</li>
|
|
531 * <li><code>HOSTKEY_IS_NEW</code>: no entries found for this hostname and this type of hostkey</li>
|
|
532 * <li><code>HOSTKEY_HAS_CHANGED</code>: hostname is known, but with another key of the same type
|
|
533 * (man-in-the-middle attack?)</li>
|
|
534 * </ul>
|
|
535 * @throws IOException if the supplied key blob cannot be parsed or does not match the given hostkey type.
|
|
536 */
|
|
537 public int verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException {
|
|
538 PublicKey remoteKey = null;
|
|
539
|
|
540 if ("ssh-rsa".equals(serverHostKeyAlgorithm)) {
|
|
541 remoteKey = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey);
|
|
542 }
|
|
543 else if ("ssh-dss".equals(serverHostKeyAlgorithm)) {
|
|
544 remoteKey = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey);
|
|
545 }
|
|
546 else if (serverHostKeyAlgorithm.startsWith("ecdsa-sha2-")) {
|
|
547 remoteKey = ECDSASHA2Verify.decodeSSHECDSAPublicKey(serverHostKey);
|
|
548 }
|
|
549 else
|
|
550 throw new IllegalArgumentException("Unknown hostkey type " + serverHostKeyAlgorithm);
|
|
551
|
|
552 int result = checkKey(hostname, remoteKey);
|
|
553
|
|
554 if (result == HOSTKEY_IS_OK)
|
|
555 return result;
|
|
556
|
|
557 InetAddress[] ipAdresses = null;
|
|
558
|
|
559 try {
|
|
560 ipAdresses = InetAddress.getAllByName(hostname);
|
|
561 }
|
|
562 catch (UnknownHostException e) {
|
|
563 return result;
|
|
564 }
|
|
565
|
|
566 for (int i = 0; i < ipAdresses.length; i++) {
|
|
567 int newresult = checkKey(ipAdresses[i].getHostAddress(), remoteKey);
|
|
568
|
|
569 if (newresult == HOSTKEY_IS_OK)
|
|
570 return newresult;
|
|
571
|
|
572 if (newresult == HOSTKEY_HAS_CHANGED)
|
|
573 result = HOSTKEY_HAS_CHANGED;
|
|
574 }
|
|
575
|
|
576 return result;
|
|
577 }
|
|
578
|
|
579 /**
|
|
580 * Adds a single public key entry to the a known_hosts file.
|
|
581 * This method is designed to be used in a {@link ServerHostKeyVerifier}.
|
|
582 *
|
|
583 * @param knownHosts the file where the publickey entry will be appended.
|
|
584 * @param hostnames a list of hostname patterns - at least one most be specified. Check out the
|
|
585 * OpenSSH sshd man page for a description of the pattern matching algorithm.
|
|
586 * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}.
|
|
587 * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}.
|
|
588 * @throws IOException
|
|
589 */
|
|
590 public final static void addHostkeyToFile(File knownHosts, String[] hostnames, String serverHostKeyAlgorithm,
|
|
591 byte[] serverHostKey) throws IOException {
|
|
592 if ((hostnames == null) || (hostnames.length == 0))
|
|
593 throw new IllegalArgumentException("Need at least one hostname specification");
|
|
594
|
|
595 if ((serverHostKeyAlgorithm == null) || (serverHostKey == null))
|
|
596 throw new IllegalArgumentException();
|
|
597
|
|
598 CharArrayWriter writer = new CharArrayWriter();
|
|
599
|
|
600 for (int i = 0; i < hostnames.length; i++) {
|
|
601 if (i != 0)
|
|
602 writer.write(',');
|
|
603
|
|
604 writer.write(hostnames[i]);
|
|
605 }
|
|
606
|
|
607 writer.write(' ');
|
|
608 writer.write(serverHostKeyAlgorithm);
|
|
609 writer.write(' ');
|
|
610 writer.write(Base64.encode(serverHostKey));
|
|
611 writer.write("\n");
|
|
612 char[] entry = writer.toCharArray();
|
|
613 RandomAccessFile raf = new RandomAccessFile(knownHosts, "rw");
|
|
614 long len = raf.length();
|
|
615
|
|
616 if (len > 0) {
|
|
617 raf.seek(len - 1);
|
|
618 int last = raf.read();
|
|
619
|
|
620 if (last != '\n')
|
|
621 raf.write('\n');
|
|
622 }
|
|
623
|
|
624 raf.write(new String(entry).getBytes("ISO-8859-1"));
|
|
625 raf.close();
|
|
626 }
|
|
627
|
|
628 /**
|
|
629 * Generates a "raw" fingerprint of a hostkey.
|
|
630 *
|
|
631 * @param type either "md5" or "sha1"
|
|
632 * @param keyType either "ssh-rsa" or "ssh-dss"
|
|
633 * @param hostkey the hostkey
|
|
634 * @return the raw fingerprint
|
|
635 */
|
|
636 static final private byte[] rawFingerPrint(String type, String keyType, byte[] hostkey) {
|
|
637 MessageDigest dig = null;
|
|
638
|
|
639 try {
|
|
640 if ("md5".equals(type)) {
|
|
641 dig = MessageDigest.getInstance("MD5");
|
|
642 }
|
|
643 else if ("sha1".equals(type)) {
|
|
644 dig = MessageDigest.getInstance("SHA1");
|
|
645 }
|
|
646 else {
|
|
647 throw new IllegalArgumentException("Unknown hash type " + type);
|
|
648 }
|
|
649 }
|
|
650 catch (NoSuchAlgorithmException e) {
|
|
651 throw new IllegalArgumentException("Unknown hash type " + type);
|
|
652 }
|
|
653
|
|
654 if (keyType.startsWith("ecdsa-sha2-")) {
|
|
655 }
|
|
656 else if ("ssh-rsa".equals(keyType)) {
|
|
657 }
|
|
658 else if ("ssh-dss".equals(keyType)) {
|
|
659 }
|
|
660 else
|
|
661 throw new IllegalArgumentException("Unknown key type " + keyType);
|
|
662
|
|
663 if (hostkey == null)
|
|
664 throw new IllegalArgumentException("hostkey is null");
|
|
665
|
|
666 dig.update(hostkey);
|
|
667 return dig.digest();
|
|
668 }
|
|
669
|
|
670 /**
|
|
671 * Convert a raw fingerprint to hex representation (XX:YY:ZZ...).
|
|
672 * @param fingerprint raw fingerprint
|
|
673 * @return the hex representation
|
|
674 */
|
|
675 static final private String rawToHexFingerprint(byte[] fingerprint) {
|
|
676 final char[] alpha = "0123456789abcdef".toCharArray();
|
|
677 StringBuffer sb = new StringBuffer();
|
|
678
|
|
679 for (int i = 0; i < fingerprint.length; i++) {
|
|
680 if (i != 0)
|
|
681 sb.append(':');
|
|
682
|
|
683 int b = fingerprint[i] & 0xff;
|
|
684 sb.append(alpha[b >> 4]);
|
|
685 sb.append(alpha[b & 15]);
|
|
686 }
|
|
687
|
|
688 return sb.toString();
|
|
689 }
|
|
690
|
|
691 /**
|
|
692 * Convert a raw fingerprint to bubblebabble representation.
|
|
693 * @param raw raw fingerprint
|
|
694 * @return the bubblebabble representation
|
|
695 */
|
|
696 static final private String rawToBubblebabbleFingerprint(byte[] raw) {
|
|
697 final char[] v = "aeiouy".toCharArray();
|
|
698 final char[] c = "bcdfghklmnprstvzx".toCharArray();
|
|
699 StringBuffer sb = new StringBuffer();
|
|
700 int seed = 1;
|
|
701 int rounds = (raw.length / 2) + 1;
|
|
702 sb.append('x');
|
|
703
|
|
704 for (int i = 0; i < rounds; i++) {
|
|
705 if (((i + 1) < rounds) || ((raw.length) % 2 != 0)) {
|
|
706 sb.append(v[(((raw[2 * i] >> 6) & 3) + seed) % 6]);
|
|
707 sb.append(c[(raw[2 * i] >> 2) & 15]);
|
|
708 sb.append(v[((raw[2 * i] & 3) + (seed / 6)) % 6]);
|
|
709
|
|
710 if ((i + 1) < rounds) {
|
|
711 sb.append(c[(((raw[(2 * i) + 1])) >> 4) & 15]);
|
|
712 sb.append('-');
|
|
713 sb.append(c[(((raw[(2 * i) + 1]))) & 15]);
|
|
714 // As long as seed >= 0, seed will be >= 0 afterwards
|
|
715 seed = ((seed * 5) + (((raw[2 * i] & 0xff) * 7) + (raw[(2 * i) + 1] & 0xff))) % 36;
|
|
716 }
|
|
717 }
|
|
718 else {
|
|
719 sb.append(v[seed % 6]); // seed >= 0, therefore index positive
|
|
720 sb.append('x');
|
|
721 sb.append(v[seed / 6]);
|
|
722 }
|
|
723 }
|
|
724
|
|
725 sb.append('x');
|
|
726 return sb.toString();
|
|
727 }
|
|
728
|
|
729 /**
|
|
730 * Convert a ssh2 key-blob into a human readable hex fingerprint.
|
|
731 * Generated fingerprints are identical to those generated by OpenSSH.
|
|
732 * <p>
|
|
733 * Example fingerprint: d0:cb:76:19:99:5a:03:fc:73:10:70:93:f2:44:63:47.
|
|
734
|
|
735 * @param keytype either "ssh-rsa" or "ssh-dss"
|
|
736 * @param publickey key blob
|
|
737 * @return Hex fingerprint
|
|
738 */
|
|
739 public final static String createHexFingerprint(String keytype, byte[] publickey) {
|
|
740 byte[] raw = rawFingerPrint("md5", keytype, publickey);
|
|
741 return rawToHexFingerprint(raw);
|
|
742 }
|
|
743
|
|
744 /**
|
|
745 * Convert a ssh2 key-blob into a human readable bubblebabble fingerprint.
|
|
746 * The used bubblebabble algorithm (taken from OpenSSH) generates fingerprints
|
|
747 * that are easier to remember for humans.
|
|
748 * <p>
|
|
749 * Example fingerprint: xofoc-bubuz-cazin-zufyl-pivuk-biduk-tacib-pybur-gonar-hotat-lyxux.
|
|
750 *
|
|
751 * @param keytype either "ssh-rsa" or "ssh-dss"
|
|
752 * @param publickey key data
|
|
753 * @return Bubblebabble fingerprint
|
|
754 */
|
|
755 public final static String createBubblebabbleFingerprint(String keytype, byte[] publickey) {
|
|
756 byte[] raw = rawFingerPrint("sha1", keytype, publickey);
|
|
757 return rawToBubblebabbleFingerprint(raw);
|
|
758 }
|
|
759 }
|