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