comparison src/ch/ethz/ssh2/KnownHosts.java @ 342:175c7d68f3c4

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