Mercurial > 510Connectbot
comparison src/com/trilead/ssh2/KnownHosts.java @ 0:0ce5cc452d02
initial version
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Thu, 22 May 2014 10:41:19 -0700 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:0ce5cc452d02 |
---|---|
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 } |