Home | History | Annotate | Download | only in ssh2
      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