• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@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 
KnownHostsEntry(String[] patterns, Object key)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 
KnownHosts()69 	public KnownHosts()
70 	{
71 	}
72 
KnownHosts(char[] knownHostsData)73 	public KnownHosts(char[] knownHostsData) throws IOException
74 	{
75 		initialize(knownHostsData);
76 	}
77 
KnownHosts(String knownHosts)78 	public KnownHosts(String knownHosts) throws IOException
79 	{
80 		initialize(new File(knownHosts));
81 	}
82 
KnownHosts(File knownHosts)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 	 */
addHostkey(String hostnames[], String serverHostKeyAlgorithm, byte[] serverHostKey)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 	 */
addHostkeys(char[] knownHostsData)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 	 */
addHostkeys(File knownHosts)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 	 */
createHashedHostname(String hostname)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 
hmacSha1Hash(byte[] salt, String hostname)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 
checkHashed(String entry, String hostname)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 
checkKey(String remoteHostname, Object remoteKey)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 
getAllKeys(String hostname)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 	 */
getPreferredServerHostkeyAlgorithmOrder(String hostname)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 
hostnameMatches(String[] hostpatterns, String hostname)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 
initialize(char[] knownHostsData)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 
initialize(File knownHosts)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 
matchKeys(Object key1, Object key2)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 
pseudoRegex(char[] pattern, int i, char[] match, int j)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 
recommendHostkeyAlgorithms(String hostname)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 	 */
verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey)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 	 */
addHostkeyToFile(File knownHosts, String[] hostnames, String serverHostKeyAlgorithm, byte[] serverHostKey)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 	 */
rawFingerPrint(String type, String keyType, byte[] hostkey)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 	 */
rawToHexFingerprint(byte[] fingerprint)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 	 */
rawToBubblebabbleFingerprint(byte[] raw)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 	 */
createHexFingerprint(String keytype, byte[] publickey)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 	 */
createBubblebabbleFingerprint(String keytype, byte[] publickey)946 	public static String createBubblebabbleFingerprint(String keytype, byte[] publickey)
947 	{
948 		byte[] raw = rawFingerPrint("sha1", keytype, publickey);
949 		return rawToBubblebabbleFingerprint(raw);
950 	}
951 }
952