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