| /* |
| * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. |
| * Please refer to the LICENSE.txt for licensing details. |
| */ |
| |
| package ch.ethz.ssh2; |
| |
| import java.io.BufferedReader; |
| import java.io.CharArrayReader; |
| import java.io.CharArrayWriter; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.RandomAccessFile; |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.security.SecureRandom; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Vector; |
| |
| import ch.ethz.ssh2.crypto.Base64; |
| import ch.ethz.ssh2.crypto.digest.Digest; |
| import ch.ethz.ssh2.crypto.digest.HMAC; |
| import ch.ethz.ssh2.crypto.digest.MD5; |
| import ch.ethz.ssh2.crypto.digest.SHA1; |
| import ch.ethz.ssh2.signature.DSAPublicKey; |
| import ch.ethz.ssh2.signature.DSASHA1Verify; |
| import ch.ethz.ssh2.signature.RSAPublicKey; |
| import ch.ethz.ssh2.signature.RSASHA1Verify; |
| import ch.ethz.ssh2.util.StringEncoder; |
| |
| /** |
| * The <code>KnownHosts</code> class is a handy tool to verify received server hostkeys |
| * based on the information in <code>known_hosts</code> files (the ones used by OpenSSH). |
| * <p/> |
| * It offers basically an in-memory database for known_hosts entries, as well as some |
| * helper functions. Entries from a <code>known_hosts</code> file can be loaded at construction time. |
| * It is also possible to add more keys later (e.g., one can parse different |
| * <code>known_hosts<code> files). |
| * <p/> |
| * It is a thread safe implementation, therefore, you need only to instantiate one |
| * <code>KnownHosts</code> for your whole application. |
| * |
| * @author Christian Plattner |
| * @version $Id: KnownHosts.java 37 2011-05-28 22:31:46Z dkocher@sudo.ch $ |
| */ |
| |
| public class KnownHosts |
| { |
| public static final int HOSTKEY_IS_OK = 0; |
| public static final int HOSTKEY_IS_NEW = 1; |
| public static final int HOSTKEY_HAS_CHANGED = 2; |
| |
| private class KnownHostsEntry |
| { |
| String[] patterns; |
| Object key; |
| |
| KnownHostsEntry(String[] patterns, Object key) |
| { |
| this.patterns = patterns; |
| this.key = key; |
| } |
| } |
| |
| private final LinkedList<KnownHostsEntry> publicKeys = new LinkedList<KnownHosts.KnownHostsEntry>(); |
| |
| public KnownHosts() |
| { |
| } |
| |
| public KnownHosts(char[] knownHostsData) throws IOException |
| { |
| initialize(knownHostsData); |
| } |
| |
| public KnownHosts(String knownHosts) throws IOException |
| { |
| initialize(new File(knownHosts)); |
| } |
| |
| public KnownHosts(File knownHosts) throws IOException |
| { |
| initialize(knownHosts); |
| } |
| |
| /** |
| * Adds a single public key entry to the database. Note: this will NOT add the public key |
| * to any physical file (e.g., "~/.ssh/known_hosts") - use <code>addHostkeyToFile()</code> for that purpose. |
| * This method is designed to be used in a {@link ServerHostKeyVerifier}. |
| * |
| * @param hostnames a list of hostname patterns - at least one most be specified. Check out the |
| * OpenSSH sshd man page for a description of the pattern matching algorithm. |
| * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. |
| * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. |
| * @throws IOException |
| */ |
| public void addHostkey(String hostnames[], String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException |
| { |
| if (hostnames == null) |
| { |
| throw new IllegalArgumentException("hostnames may not be null"); |
| } |
| |
| if ("ssh-rsa".equals(serverHostKeyAlgorithm)) |
| { |
| RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); |
| |
| synchronized (publicKeys) |
| { |
| publicKeys.add(new KnownHostsEntry(hostnames, rpk)); |
| } |
| } |
| else if ("ssh-dss".equals(serverHostKeyAlgorithm)) |
| { |
| DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey); |
| |
| synchronized (publicKeys) |
| { |
| publicKeys.add(new KnownHostsEntry(hostnames, dpk)); |
| } |
| } |
| else |
| { |
| throw new IOException("Unknwon host key type (" + serverHostKeyAlgorithm + ")"); |
| } |
| } |
| |
| /** |
| * Parses the given known_hosts data and adds entries to the database. |
| * |
| * @param knownHostsData |
| * @throws IOException |
| */ |
| public void addHostkeys(char[] knownHostsData) throws IOException |
| { |
| initialize(knownHostsData); |
| } |
| |
| /** |
| * Parses the given known_hosts file and adds entries to the database. |
| * |
| * @param knownHosts |
| * @throws IOException |
| */ |
| public void addHostkeys(File knownHosts) throws IOException |
| { |
| initialize(knownHosts); |
| } |
| |
| /** |
| * Generate the hashed representation of the given hostname. Useful for adding entries |
| * with hashed hostnames to a known_hosts file. (see -H option of OpenSSH key-gen). |
| * |
| * @param hostname |
| * @return the hashed representation, e.g., "|1|cDhrv7zwEUV3k71CEPHnhHZezhA=|Xo+2y6rUXo2OIWRAYhBOIijbJMA=" |
| */ |
| public static String createHashedHostname(String hostname) |
| { |
| SHA1 sha1 = new SHA1(); |
| |
| byte[] salt = new byte[sha1.getDigestLength()]; |
| |
| new SecureRandom().nextBytes(salt); |
| |
| byte[] hash = hmacSha1Hash(salt, hostname); |
| |
| String base64_salt = new String(Base64.encode(salt)); |
| String base64_hash = new String(Base64.encode(hash)); |
| |
| return new String("|1|" + base64_salt + "|" + base64_hash); |
| } |
| |
| private static byte[] hmacSha1Hash(byte[] salt, String hostname) |
| { |
| SHA1 sha1 = new SHA1(); |
| |
| if (salt.length != sha1.getDigestLength()) |
| { |
| throw new IllegalArgumentException("Salt has wrong length (" + salt.length + ")"); |
| } |
| |
| HMAC hmac = new HMAC(sha1, salt, salt.length); |
| |
| hmac.update(StringEncoder.GetBytes(hostname)); |
| |
| byte[] dig = new byte[hmac.getDigestLength()]; |
| |
| hmac.digest(dig); |
| |
| return dig; |
| } |
| |
| private boolean checkHashed(String entry, String hostname) |
| { |
| if (entry.startsWith("|1|") == false) |
| { |
| return false; |
| } |
| |
| int delim_idx = entry.indexOf('|', 3); |
| |
| if (delim_idx == -1) |
| { |
| return false; |
| } |
| |
| String salt_base64 = entry.substring(3, delim_idx); |
| String hash_base64 = entry.substring(delim_idx + 1); |
| |
| byte[] salt = null; |
| byte[] hash = null; |
| |
| try |
| { |
| salt = Base64.decode(salt_base64.toCharArray()); |
| hash = Base64.decode(hash_base64.toCharArray()); |
| } |
| catch (IOException e) |
| { |
| return false; |
| } |
| |
| SHA1 sha1 = new SHA1(); |
| |
| if (salt.length != sha1.getDigestLength()) |
| { |
| return false; |
| } |
| |
| byte[] dig = hmacSha1Hash(salt, hostname); |
| |
| for (int i = 0; i < dig.length; i++) |
| { |
| if (dig[i] != hash[i]) |
| { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| private int checkKey(String remoteHostname, Object remoteKey) |
| { |
| int result = HOSTKEY_IS_NEW; |
| |
| synchronized (publicKeys) |
| { |
| for (KnownHostsEntry ke : publicKeys) |
| { |
| if (hostnameMatches(ke.patterns, remoteHostname) == false) |
| { |
| continue; |
| } |
| |
| boolean res = matchKeys(ke.key, remoteKey); |
| |
| if (res == true) |
| { |
| return HOSTKEY_IS_OK; |
| } |
| |
| result = HOSTKEY_HAS_CHANGED; |
| } |
| } |
| return result; |
| } |
| |
| private List<Object> getAllKeys(String hostname) |
| { |
| List<Object> keys = new Vector<Object>(); |
| |
| synchronized (publicKeys) |
| { |
| for (KnownHostsEntry ke : publicKeys) |
| { |
| if (hostnameMatches(ke.patterns, hostname) == false) |
| { |
| continue; |
| } |
| |
| keys.add(ke.key); |
| } |
| } |
| |
| return keys; |
| } |
| |
| /** |
| * Try to find the preferred order of hostkey algorithms for the given hostname. |
| * Based on the type of hostkey that is present in the internal database |
| * (i.e., either <code>ssh-rsa</code> or <code>ssh-dss</code>) |
| * an ordered list of hostkey algorithms is returned which can be passed |
| * to <code>Connection.setServerHostKeyAlgorithms</code>. |
| * |
| * @param hostname |
| * @return <code>null</code> if no key for the given hostname is present or |
| * there are keys of multiple types present for the given hostname. Otherwise, |
| * an array with hostkey algorithms is returned (i.e., an array of length 2). |
| */ |
| public String[] getPreferredServerHostkeyAlgorithmOrder(String hostname) |
| { |
| String[] algos = recommendHostkeyAlgorithms(hostname); |
| |
| if (algos != null) |
| { |
| return algos; |
| } |
| |
| InetAddress[] ipAdresses = null; |
| |
| try |
| { |
| ipAdresses = InetAddress.getAllByName(hostname); |
| } |
| catch (UnknownHostException e) |
| { |
| return null; |
| } |
| |
| for (int i = 0; i < ipAdresses.length; i++) |
| { |
| algos = recommendHostkeyAlgorithms(ipAdresses[i].getHostAddress()); |
| |
| if (algos != null) |
| { |
| return algos; |
| } |
| } |
| |
| return null; |
| } |
| |
| private boolean hostnameMatches(String[] hostpatterns, String hostname) |
| { |
| boolean isMatch = false; |
| boolean negate = false; |
| |
| hostname = hostname.toLowerCase(); |
| |
| for (int k = 0; k < hostpatterns.length; k++) |
| { |
| if (hostpatterns[k] == null) |
| { |
| continue; |
| } |
| |
| String pattern = null; |
| |
| /* In contrast to OpenSSH we also allow negated hash entries (as well as hashed |
| * entries in lines with multiple entries). |
| */ |
| |
| if ((hostpatterns[k].length() > 0) && (hostpatterns[k].charAt(0) == '!')) |
| { |
| pattern = hostpatterns[k].substring(1); |
| negate = true; |
| } |
| else |
| { |
| pattern = hostpatterns[k]; |
| negate = false; |
| } |
| |
| /* Optimize, no need to check this entry */ |
| |
| if ((isMatch) && (negate == false)) |
| { |
| continue; |
| } |
| |
| /* Now compare */ |
| |
| if (pattern.charAt(0) == '|') |
| { |
| if (checkHashed(pattern, hostname)) |
| { |
| if (negate) |
| { |
| return false; |
| } |
| isMatch = true; |
| } |
| } |
| else |
| { |
| pattern = pattern.toLowerCase(); |
| |
| if ((pattern.indexOf('?') != -1) || (pattern.indexOf('*') != -1)) |
| { |
| if (pseudoRegex(pattern.toCharArray(), 0, hostname.toCharArray(), 0)) |
| { |
| if (negate) |
| { |
| return false; |
| } |
| isMatch = true; |
| } |
| } |
| else if (pattern.compareTo(hostname) == 0) |
| { |
| if (negate) |
| { |
| return false; |
| } |
| isMatch = true; |
| } |
| } |
| } |
| |
| return isMatch; |
| } |
| |
| private void initialize(char[] knownHostsData) throws IOException |
| { |
| BufferedReader br = new BufferedReader(new CharArrayReader(knownHostsData)); |
| |
| while (true) |
| { |
| String line = br.readLine(); |
| |
| if (line == null) |
| { |
| break; |
| } |
| |
| line = line.trim(); |
| |
| if (line.startsWith("#")) |
| { |
| continue; |
| } |
| |
| String[] arr = line.split(" "); |
| |
| if (arr.length >= 3) |
| { |
| if ((arr[1].compareTo("ssh-rsa") == 0) || (arr[1].compareTo("ssh-dss") == 0)) |
| { |
| String[] hostnames = arr[0].split(","); |
| |
| byte[] msg = Base64.decode(arr[2].toCharArray()); |
| |
| try |
| { |
| addHostkey(hostnames, arr[1], msg); |
| } |
| catch (IOException e) |
| { |
| continue; |
| } |
| } |
| } |
| } |
| } |
| |
| private void initialize(File knownHosts) throws IOException |
| { |
| char[] buff = new char[512]; |
| |
| CharArrayWriter cw = new CharArrayWriter(); |
| |
| knownHosts.createNewFile(); |
| |
| FileReader fr = new FileReader(knownHosts); |
| |
| while (true) |
| { |
| int len = fr.read(buff); |
| if (len < 0) |
| { |
| break; |
| } |
| cw.write(buff, 0, len); |
| } |
| |
| fr.close(); |
| |
| initialize(cw.toCharArray()); |
| } |
| |
| private boolean matchKeys(Object key1, Object key2) |
| { |
| if ((key1 instanceof RSAPublicKey) && (key2 instanceof RSAPublicKey)) |
| { |
| RSAPublicKey savedRSAKey = (RSAPublicKey) key1; |
| RSAPublicKey remoteRSAKey = (RSAPublicKey) key2; |
| |
| if (savedRSAKey.getE().equals(remoteRSAKey.getE()) == false) |
| { |
| return false; |
| } |
| |
| if (savedRSAKey.getN().equals(remoteRSAKey.getN()) == false) |
| { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| if ((key1 instanceof DSAPublicKey) && (key2 instanceof DSAPublicKey)) |
| { |
| DSAPublicKey savedDSAKey = (DSAPublicKey) key1; |
| DSAPublicKey remoteDSAKey = (DSAPublicKey) key2; |
| |
| if (savedDSAKey.getG().equals(remoteDSAKey.getG()) == false) |
| { |
| return false; |
| } |
| |
| if (savedDSAKey.getP().equals(remoteDSAKey.getP()) == false) |
| { |
| return false; |
| } |
| |
| if (savedDSAKey.getQ().equals(remoteDSAKey.getQ()) == false) |
| { |
| return false; |
| } |
| |
| if (savedDSAKey.getY().equals(remoteDSAKey.getY()) == false) |
| { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean pseudoRegex(char[] pattern, int i, char[] match, int j) |
| { |
| /* This matching logic is equivalent to the one present in OpenSSH 4.1 */ |
| |
| while (true) |
| { |
| /* Are we at the end of the pattern? */ |
| |
| if (pattern.length == i) |
| { |
| return (match.length == j); |
| } |
| |
| if (pattern[i] == '*') |
| { |
| i++; |
| |
| if (pattern.length == i) |
| { |
| return true; |
| } |
| |
| if ((pattern[i] != '*') && (pattern[i] != '?')) |
| { |
| while (true) |
| { |
| if ((pattern[i] == match[j]) && pseudoRegex(pattern, i + 1, match, j + 1)) |
| { |
| return true; |
| } |
| j++; |
| if (match.length == j) |
| { |
| return false; |
| } |
| } |
| } |
| |
| while (true) |
| { |
| if (pseudoRegex(pattern, i, match, j)) |
| { |
| return true; |
| } |
| j++; |
| if (match.length == j) |
| { |
| return false; |
| } |
| } |
| } |
| |
| if (match.length == j) |
| { |
| return false; |
| } |
| |
| if ((pattern[i] != '?') && (pattern[i] != match[j])) |
| { |
| return false; |
| } |
| |
| i++; |
| j++; |
| } |
| } |
| |
| private String[] recommendHostkeyAlgorithms(String hostname) |
| { |
| String preferredAlgo = null; |
| |
| List<Object> keys = getAllKeys(hostname); |
| |
| for (Object key : keys) |
| { |
| String thisAlgo = null; |
| |
| if (key instanceof RSAPublicKey) |
| { |
| thisAlgo = "ssh-rsa"; |
| } |
| else if (key instanceof DSAPublicKey) |
| { |
| thisAlgo = "ssh-dss"; |
| } |
| else |
| { |
| continue; |
| } |
| |
| if (preferredAlgo != null) |
| { |
| /* If we find different key types, then return null */ |
| |
| if (preferredAlgo.compareTo(thisAlgo) != 0) |
| { |
| return null; |
| } |
| } |
| else |
| { |
| preferredAlgo = thisAlgo; |
| } |
| } |
| |
| /* If we did not find anything that we know of, return null */ |
| |
| if (preferredAlgo == null) |
| { |
| return null; |
| } |
| |
| /* Now put the preferred algo to the start of the array. |
| * You may ask yourself why we do it that way - basically, we could just |
| * return only the preferred algorithm: since we have a saved key of that |
| * type (sent earlier from the remote host), then that should work out. |
| * However, imagine that the server is (for whatever reasons) not offering |
| * that type of hostkey anymore (e.g., "ssh-rsa" was disabled and |
| * now "ssh-dss" is being used). If we then do not let the server send us |
| * a fresh key of the new type, then we shoot ourself into the foot: |
| * the connection cannot be established and hence the user cannot decide |
| * if he/she wants to accept the new key. |
| */ |
| |
| if (preferredAlgo.equals("ssh-rsa")) |
| { |
| return new String[] { "ssh-rsa", "ssh-dss" }; |
| } |
| |
| return new String[] { "ssh-dss", "ssh-rsa" }; |
| } |
| |
| /** |
| * Checks the internal hostkey database for the given hostkey. |
| * If no matching key can be found, then the hostname is resolved to an IP address |
| * and the search is repeated using that IP address. |
| * |
| * @param hostname the server's hostname, will be matched with all hostname patterns |
| * @param serverHostKeyAlgorithm type of hostkey, either <code>ssh-rsa</code> or <code>ssh-dss</code> |
| * @param serverHostKey the key blob |
| * @return <ul> |
| * <li><code>HOSTKEY_IS_OK</code>: the given hostkey matches an entry for the given hostname</li> |
| * <li><code>HOSTKEY_IS_NEW</code>: no entries found for this hostname and this type of hostkey</li> |
| * <li><code>HOSTKEY_HAS_CHANGED</code>: hostname is known, but with another key of the same type |
| * (man-in-the-middle attack?)</li> |
| * </ul> |
| * @throws IOException if the supplied key blob cannot be parsed or does not match the given hostkey type. |
| */ |
| public int verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException |
| { |
| Object remoteKey = null; |
| |
| if ("ssh-rsa".equals(serverHostKeyAlgorithm)) |
| { |
| remoteKey = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); |
| } |
| else if ("ssh-dss".equals(serverHostKeyAlgorithm)) |
| { |
| remoteKey = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey); |
| } |
| else |
| { |
| throw new IllegalArgumentException("Unknown hostkey type " + serverHostKeyAlgorithm); |
| } |
| |
| int result = checkKey(hostname, remoteKey); |
| |
| if (result == HOSTKEY_IS_OK) |
| { |
| return result; |
| } |
| |
| InetAddress[] ipAdresses = null; |
| |
| try |
| { |
| ipAdresses = InetAddress.getAllByName(hostname); |
| } |
| catch (UnknownHostException e) |
| { |
| return result; |
| } |
| |
| for (int i = 0; i < ipAdresses.length; i++) |
| { |
| int newresult = checkKey(ipAdresses[i].getHostAddress(), remoteKey); |
| |
| if (newresult == HOSTKEY_IS_OK) |
| { |
| return newresult; |
| } |
| |
| if (newresult == HOSTKEY_HAS_CHANGED) |
| { |
| result = HOSTKEY_HAS_CHANGED; |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Adds a single public key entry to the a known_hosts file. |
| * This method is designed to be used in a {@link ServerHostKeyVerifier}. |
| * |
| * @param knownHosts the file where the publickey entry will be appended. |
| * @param hostnames a list of hostname patterns - at least one most be specified. Check out the |
| * OpenSSH sshd man page for a description of the pattern matching algorithm. |
| * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. |
| * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. |
| * @throws IOException |
| */ |
| public static void addHostkeyToFile(File knownHosts, String[] hostnames, String serverHostKeyAlgorithm, |
| byte[] serverHostKey) throws IOException |
| { |
| if ((hostnames == null) || (hostnames.length == 0)) |
| { |
| throw new IllegalArgumentException("Need at least one hostname specification"); |
| } |
| |
| if ((serverHostKeyAlgorithm == null) || (serverHostKey == null)) |
| { |
| throw new IllegalArgumentException(); |
| } |
| |
| CharArrayWriter writer = new CharArrayWriter(); |
| |
| for (int i = 0; i < hostnames.length; i++) |
| { |
| if (i != 0) |
| { |
| writer.write(','); |
| } |
| writer.write(hostnames[i]); |
| } |
| |
| writer.write(' '); |
| writer.write(serverHostKeyAlgorithm); |
| writer.write(' '); |
| writer.write(Base64.encode(serverHostKey)); |
| writer.write("\n"); |
| |
| char[] entry = writer.toCharArray(); |
| |
| RandomAccessFile raf = new RandomAccessFile(knownHosts, "rw"); |
| |
| long len = raf.length(); |
| |
| if (len > 0) |
| { |
| raf.seek(len - 1); |
| int last = raf.read(); |
| if (last != '\n') |
| { |
| raf.write('\n'); |
| } |
| } |
| |
| raf.write(StringEncoder.GetBytes(new String(entry))); |
| raf.close(); |
| } |
| |
| /** |
| * Generates a "raw" fingerprint of a hostkey. |
| * |
| * @param type either "md5" or "sha1" |
| * @param keyType either "ssh-rsa" or "ssh-dss" |
| * @param hostkey the hostkey |
| * @return the raw fingerprint |
| */ |
| static private byte[] rawFingerPrint(String type, String keyType, byte[] hostkey) |
| { |
| Digest dig = null; |
| |
| if ("md5".equals(type)) |
| { |
| dig = new MD5(); |
| } |
| else if ("sha1".equals(type)) |
| { |
| dig = new SHA1(); |
| } |
| else |
| { |
| throw new IllegalArgumentException("Unknown hash type " + type); |
| } |
| |
| if ("ssh-rsa".equals(keyType)) |
| { |
| } |
| else if ("ssh-dss".equals(keyType)) |
| { |
| } |
| else |
| { |
| throw new IllegalArgumentException("Unknown key type " + keyType); |
| } |
| |
| if (hostkey == null) |
| { |
| throw new IllegalArgumentException("hostkey is null"); |
| } |
| |
| dig.update(hostkey); |
| byte[] res = new byte[dig.getDigestLength()]; |
| dig.digest(res); |
| return res; |
| } |
| |
| /** |
| * Convert a raw fingerprint to hex representation (XX:YY:ZZ...). |
| * |
| * @param fingerprint raw fingerprint |
| * @return the hex representation |
| */ |
| static private String rawToHexFingerprint(byte[] fingerprint) |
| { |
| final char[] alpha = "0123456789abcdef".toCharArray(); |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| for (int i = 0; i < fingerprint.length; i++) |
| { |
| if (i != 0) |
| { |
| sb.append(':'); |
| } |
| int b = fingerprint[i] & 0xff; |
| sb.append(alpha[b >> 4]); |
| sb.append(alpha[b & 15]); |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Convert a raw fingerprint to bubblebabble representation. |
| * |
| * @param raw raw fingerprint |
| * @return the bubblebabble representation |
| */ |
| static private String rawToBubblebabbleFingerprint(byte[] raw) |
| { |
| final char[] v = "aeiouy".toCharArray(); |
| final char[] c = "bcdfghklmnprstvzx".toCharArray(); |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| int seed = 1; |
| |
| int rounds = (raw.length / 2) + 1; |
| |
| sb.append('x'); |
| |
| for (int i = 0; i < rounds; i++) |
| { |
| if (((i + 1) < rounds) || ((raw.length) % 2 != 0)) |
| { |
| sb.append(v[(((raw[2 * i] >> 6) & 3) + seed) % 6]); |
| sb.append(c[(raw[2 * i] >> 2) & 15]); |
| sb.append(v[((raw[2 * i] & 3) + (seed / 6)) % 6]); |
| |
| if ((i + 1) < rounds) |
| { |
| sb.append(c[(((raw[(2 * i) + 1])) >> 4) & 15]); |
| sb.append('-'); |
| sb.append(c[(((raw[(2 * i) + 1]))) & 15]); |
| // As long as seed >= 0, seed will be >= 0 afterwards |
| seed = ((seed * 5) + (((raw[2 * i] & 0xff) * 7) + (raw[(2 * i) + 1] & 0xff))) % 36; |
| } |
| } |
| else |
| { |
| sb.append(v[seed % 6]); // seed >= 0, therefore index positive |
| sb.append('x'); |
| sb.append(v[seed / 6]); |
| } |
| } |
| |
| sb.append('x'); |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Convert a ssh2 key-blob into a human readable hex fingerprint. |
| * Generated fingerprints are identical to those generated by OpenSSH. |
| * <p/> |
| * Example fingerprint: d0:cb:76:19:99:5a:03:fc:73:10:70:93:f2:44:63:47. |
| * |
| * @param keytype either "ssh-rsa" or "ssh-dss" |
| * @param publickey key blob |
| * @return Hex fingerprint |
| */ |
| public static String createHexFingerprint(String keytype, byte[] publickey) |
| { |
| byte[] raw = rawFingerPrint("md5", keytype, publickey); |
| return rawToHexFingerprint(raw); |
| } |
| |
| /** |
| * Convert a ssh2 key-blob into a human readable bubblebabble fingerprint. |
| * The used bubblebabble algorithm (taken from OpenSSH) generates fingerprints |
| * that are easier to remember for humans. |
| * <p/> |
| * Example fingerprint: xofoc-bubuz-cazin-zufyl-pivuk-biduk-tacib-pybur-gonar-hotat-lyxux. |
| * |
| * @param keytype either "ssh-rsa" or "ssh-dss" |
| * @param publickey key data |
| * @return Bubblebabble fingerprint |
| */ |
| public static String createBubblebabbleFingerprint(String keytype, byte[] publickey) |
| { |
| byte[] raw = rawFingerPrint("sha1", keytype, publickey); |
| return rawToBubblebabbleFingerprint(raw); |
| } |
| } |