1/*
2 * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
3 * Please refer to the LICENSE.txt for licensing details.
4 */
5
6package ch.ethz.ssh2;
7
8import java.io.BufferedReader;
9import java.io.CharArrayReader;
10import java.io.CharArrayWriter;
11import java.io.File;
12import java.io.FileReader;
13import java.io.IOException;
14import java.io.RandomAccessFile;
15import java.net.InetAddress;
16import java.net.UnknownHostException;
17import java.security.SecureRandom;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Vector;
21
22import ch.ethz.ssh2.crypto.Base64;
23import ch.ethz.ssh2.crypto.digest.Digest;
24import ch.ethz.ssh2.crypto.digest.HMAC;
25import ch.ethz.ssh2.crypto.digest.MD5;
26import ch.ethz.ssh2.crypto.digest.SHA1;
27import ch.ethz.ssh2.signature.DSAPublicKey;
28import ch.ethz.ssh2.signature.DSASHA1Verify;
29import ch.ethz.ssh2.signature.RSAPublicKey;
30import ch.ethz.ssh2.signature.RSASHA1Verify;
31import 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@sudo.ch $
47 */
48
49public 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