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 }