diff --git a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java index 61114c63..2080a122 100644 --- a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java +++ b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.verification; -import net.schmizz.sshj.common.*; +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.transport.mac.HMACSHA1; import net.schmizz.sshj.transport.mac.MAC; import org.slf4j.Logger; @@ -41,348 +46,361 @@ import java.util.List; * @see Hashed hostnames spec */ public class OpenSSHKnownHosts - implements HostKeyVerifier { + implements HostKeyVerifier { - protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final Logger log = LoggerFactory.getLogger(getClass()); - protected final File khFile; - protected final List entries = new ArrayList(); + protected final File khFile; + protected final List entries = new ArrayList(); - public OpenSSHKnownHosts(File khFile) throws IOException { - this.khFile = khFile; - if (khFile.exists()) { - final BufferedReader br = new BufferedReader(new FileReader(khFile)); - try { - // Read in the file, storing each line as an entry - String line; - while ((line = br.readLine()) != null) - try { - HostEntry entry = EntryFactory.parseEntry(line); - if (entry != null) { - entries.add(entry); - } - } catch (SSHException ignore) { - log.debug("Bad line ({}): {} ", ignore.toString(), line); - } - } finally { - IOUtils.closeQuietly(br); - } - } - } + public OpenSSHKnownHosts(File khFile) + throws IOException { + this.khFile = khFile; + if (khFile.exists()) { + final BufferedReader br = new BufferedReader(new FileReader(khFile)); + try { + // Read in the file, storing each line as an entry + String line; + while ((line = br.readLine()) != null) + try { + HostEntry entry = EntryFactory.parseEntry(line); + if (entry != null) { + entries.add(entry); + } + } catch (SSHException ignore) { + log.debug("Bad line ({}): {} ", ignore.toString(), line); + } + } finally { + IOUtils.closeQuietly(br); + } + } + } - public File getFile() { - return khFile; - } + public File getFile() { + return khFile; + } - @Override - public boolean verify(final String hostname, final int port, final PublicKey key) { - final KeyType type = KeyType.fromKey(key); - if (type == KeyType.UNKNOWN) - return false; + @Override + public boolean verify(final String hostname, final int port, final PublicKey key) { + final KeyType type = KeyType.fromKey(key); + if (type == KeyType.UNKNOWN) + return false; - final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname; + final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname; - for (HostEntry e : entries) - try { - if (e.appliesTo(type, adjustedHostname)) - return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key); - } catch (IOException ioe) { - log.error("Error with {}: {}", e, ioe); - return false; - } - return hostKeyUnverifiableAction(adjustedHostname, key); - } + for (HostEntry e : entries) + try { + if (e.appliesTo(type, adjustedHostname)) + return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key); + } catch (IOException ioe) { + log.error("Error with {}: {}", e, ioe); + return false; + } + return hostKeyUnverifiableAction(adjustedHostname, key); + } - protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { - return false; - } + protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { + return false; + } - protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) { - log.warn("Host key for `{}` has changed!", hostname); - return false; - } + protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) { + log.warn("Host key for `{}` has changed!", hostname); + return false; + } - public List entries() { - return entries; - } + public List entries() { + return entries; + } - private static final String LS = System.getProperty("line.separator"); + private static final String LS = System.getProperty("line.separator"); - public void write() - throws IOException { - final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile)); - try { - for (HostEntry entry : entries) - bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8)); - } finally { - bos.close(); - } - } + public void write() + throws IOException { + final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile)); + try { + for (HostEntry entry : entries) + bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8)); + } finally { + bos.close(); + } + } - public static File detectSSHDir() { - final File sshDir = new File(System.getProperty("user.home"), ".ssh"); - return sshDir.exists() ? sshDir : null; - } + public static File detectSSHDir() { + final File sshDir = new File(System.getProperty("user.home"), ".ssh"); + return sshDir.exists() ? sshDir : null; + } - /** - * Each line in these files contains the following fields: markers - * (optional), hostnames, bits, exponent, modulus, comment. The fields are - * separated by spaces. - *

- * The marker is optional, but if it is present then it must be one of - * ``@cert-authority'', to indicate that the line contains a certification - * authority (CA) key, or ``@revoked'', to indicate that the key contained - * on the line is revoked and must not ever be accepted. Only one marker - * should be used on a key line. - *

- * Hostnames is a comma-separated list of patterns (`*' and `?' act as - * wildcards); each pattern in turn is matched against the canonical host - * name (when authenticating a client) or against the user-supplied name - * (when authenticating a server). A pattern may also be preceded by `!' to - * indicate negation: if the host name matches a negated pattern, it is not - * accepted (by that line) even if it matched another pattern on the line. - * A hostname or address may optionally be enclosed within `[' and `]' - * brackets then followed by `:' and a non-standard port number. - *

- * Alternately, hostnames may be stored in a hashed form which hides host - * names and addresses should the file's contents be disclosed. Hashed - * hostnames start with a `|' character. Only one hashed hostname may - * appear on a single line and none of the above negation or wildcard - * operators may be applied. - *

- * Bits, exponent, and modulus are taken directly from the RSA host key; - * they can be obtained, for example, from /etc/ssh/ssh_host_key.pub. The - * optional comment field continues to the end of the line, and is not used. - *

- * Lines starting with `#' and empty lines are ignored as comments. - */ - public static class EntryFactory { + /** + * Each line in these files contains the following fields: markers + * (optional), hostnames, bits, exponent, modulus, comment. The fields are + * separated by spaces. + *

+ * The marker is optional, but if it is present then it must be one of + * ``@cert-authority'', to indicate that the line contains a certification + * authority (CA) key, or ``@revoked'', to indicate that the key contained + * on the line is revoked and must not ever be accepted. Only one marker + * should be used on a key line. + *

+ * Hostnames is a comma-separated list of patterns (`*' and `?' act as + * wildcards); each pattern in turn is matched against the canonical host + * name (when authenticating a client) or against the user-supplied name + * (when authenticating a server). A pattern may also be preceded by `!' to + * indicate negation: if the host name matches a negated pattern, it is not + * accepted (by that line) even if it matched another pattern on the line. + * A hostname or address may optionally be enclosed within `[' and `]' + * brackets then followed by `:' and a non-standard port number. + *

+ * Alternately, hostnames may be stored in a hashed form which hides host + * names and addresses should the file's contents be disclosed. Hashed + * hostnames start with a `|' character. Only one hashed hostname may + * appear on a single line and none of the above negation or wildcard + * operators may be applied. + *

+ * Bits, exponent, and modulus are taken directly from the RSA host key; + * they can be obtained, for example, from /etc/ssh/ssh_host_key.pub. The + * optional comment field continues to the end of the line, and is not used. + *

+ * Lines starting with `#' and empty lines are ignored as comments. + */ + public static class EntryFactory { - public static HostEntry parseEntry(String line) throws IOException { - if (isComment(line)) { - return new CommentEntry(line); - } + public static HostEntry parseEntry(String line) + throws IOException { + if (isComment(line)) { + return new CommentEntry(line); + } - String[] split = line.split(" "); - int i = 0; - Marker marker = getMarker(split[i]); - if (marker != null) { - i++; - } + String[] split = line.split(" "); + int i = 0; + Marker marker = getMarker(split[i]); + if (marker != null) { + i++; + } - String hostnames = split[i++]; - String sType = split[i++]; - KeyType type = KeyType.fromString(sType); - PublicKey key; + String hostnames = split[i++]; + String sType = split[i++]; + KeyType type = KeyType.fromString(sType); + PublicKey key; - if (isType(type)) { - String sKey = split[i++]; - key = getKey(sKey); - } else if (isBits(sType)) { - type = KeyType.RSA; - int bits = Integer.valueOf(sType); - BigInteger e = new BigInteger(split[i++]); - BigInteger n = new BigInteger(split[i++]); - try { - final KeyFactory keyFactory = SecurityUtils.getKeyFactory("RSA"); - key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); - } catch (Exception ex) { - logger.error("Error reading entry {}, could not create key", line, ex); - return null; - } - } else { - logger.error("Error reading entry {}, could not determine type", line); - return null; - } + if (isType(type)) { + String sKey = split[i++]; + key = getKey(sKey); + } else if (isBits(sType)) { + type = KeyType.RSA; + int bits = Integer.valueOf(sType); + BigInteger e = new BigInteger(split[i++]); + BigInteger n = new BigInteger(split[i++]); + try { + final KeyFactory keyFactory = SecurityUtils.getKeyFactory("RSA"); + key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); + } catch (Exception ex) { + logger.error("Error reading entry {}, could not create key", line, ex); + return null; + } + } else { + logger.error("Error reading entry {}, could not determine type", line); + return null; + } - if (isHashed(hostnames)) { - return new HashedEntry(marker, hostnames, type, key); - } else { - return new SimpleEntry(marker, hostnames, type, key); - } - } + if (isHashed(hostnames)) { + return new HashedEntry(marker, hostnames, type, key); + } else { + return new SimpleEntry(marker, hostnames, type, key); + } + } - private static PublicKey getKey(String sKey) throws IOException { - return new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey(); - } + private static PublicKey getKey(String sKey) + throws IOException { + return new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey(); + } - private static boolean isBits(String type) { - try { - Integer.parseInt(type); - return true; - } catch (NumberFormatException e) { - return false; - } - } + private static boolean isBits(String type) { + try { + Integer.parseInt(type); + return true; + } catch (NumberFormatException e) { + return false; + } + } - private static boolean isType(KeyType type) { - return type != KeyType.UNKNOWN; - } + private static boolean isType(KeyType type) { + return type != KeyType.UNKNOWN; + } - private static boolean isComment(String line) { - return line.isEmpty() || line.startsWith("#"); - } + private static boolean isComment(String line) { + return line.isEmpty() || line.startsWith("#"); + } - public static Marker getMarker(String line) { - if (line.equals("@cert-authority")) return Marker.CA_CERT; - if (line.equals("@revoked")) return Marker.REVOKED; - return null; - } + public static Marker getMarker(String line) { + if (line.equals("@cert-authority")) return Marker.CA_CERT; + if (line.equals("@revoked")) return Marker.REVOKED; + return null; + } - public static boolean isHashed(String line) { - return line.startsWith("|1|"); - } + public static boolean isHashed(String line) { + return line.startsWith("|1|"); + } - } + } - public interface HostEntry { - boolean appliesTo(KeyType type, String host) throws IOException; - boolean verify(PublicKey key) throws IOException; - String getLine(); - } + public interface HostEntry { + boolean appliesTo(KeyType type, String host) + throws IOException; - public static class CommentEntry implements HostEntry { - private final String comment; + boolean verify(PublicKey key) + throws IOException; - public CommentEntry(String comment) { - this.comment = comment; - } + String getLine(); + } - @Override - public boolean appliesTo(KeyType type, String host) { - return false; - } + public static class CommentEntry implements HostEntry { + private final String comment; - @Override - public boolean verify(PublicKey key) { - return false; - } + public CommentEntry(String comment) { + this.comment = comment; + } - @Override - public String getLine() { - return comment; - } - } + @Override + public boolean appliesTo(KeyType type, String host) { + return false; + } - public static abstract class AbstractEntry implements HostEntry { + @Override + public boolean verify(PublicKey key) { + return false; + } - protected final OpenSSHKnownHosts.Marker marker; - protected final KeyType type; - protected PublicKey key; + @Override + public String getLine() { + return comment; + } + } - public AbstractEntry(Marker marker, KeyType type, PublicKey key) { - this.marker = marker; - this.type = type; - this.key = key; - } + public static abstract class AbstractEntry implements HostEntry { - @Override - public boolean verify(PublicKey key) throws IOException { - return key.equals(this.key) && marker != Marker.REVOKED; - } + protected final OpenSSHKnownHosts.Marker marker; + protected final KeyType type; + protected PublicKey key; - public String getLine() { - final StringBuilder line = new StringBuilder(); + public AbstractEntry(Marker marker, KeyType type, PublicKey key) { + this.marker = marker; + this.type = type; + this.key = key; + } - if (marker != null) line.append(marker.getMarkerString()).append(" "); + @Override + public boolean verify(PublicKey key) + throws IOException { + return key.equals(this.key) && marker != Marker.REVOKED; + } - line.append(getHostPart()); - line.append(" ").append(type.toString()); - line.append(" ").append(getKeyString()); - return line.toString(); - } + public String getLine() { + final StringBuilder line = new StringBuilder(); - private String getKeyString() { - final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key); - return Base64.encodeBytes(buf.array(), buf.rpos(), buf.available()); - } + if (marker != null) line.append(marker.getMarkerString()).append(" "); - protected abstract String getHostPart(); - } + line.append(getHostPart()); + line.append(" ").append(type.toString()); + line.append(" ").append(getKeyString()); + return line.toString(); + } - public static class SimpleEntry extends AbstractEntry { - private List hosts; - private String hostnames; + private String getKeyString() { + final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key); + return Base64.encodeBytes(buf.array(), buf.rpos(), buf.available()); + } - public SimpleEntry(Marker marker, String hostnames, KeyType type, PublicKey key) { - super(marker, type, key); - this.hostnames = hostnames; - hosts = Arrays.asList(hostnames.split(",")); - } + protected abstract String getHostPart(); + } - @Override - protected String getHostPart() { - return hostnames; - } + public static class SimpleEntry extends AbstractEntry { + private List hosts; + private String hostnames; - @Override - public boolean appliesTo(KeyType type, String host) throws IOException { - return type == this.type && hostnames.contains(host); - } - } + public SimpleEntry(Marker marker, String hostnames, KeyType type, PublicKey key) { + super(marker, type, key); + this.hostnames = hostnames; + hosts = Arrays.asList(hostnames.split(",")); + } - public static class HashedEntry extends AbstractEntry { - private final MAC sha1 = new HMACSHA1(); + @Override + protected String getHostPart() { + return hostnames; + } - private String salt; - private byte[] saltyBytes; + @Override + public boolean appliesTo(KeyType type, String host) + throws IOException { + return type == this.type && hostnames.contains(host); + } + } - private final String hashedHost; + public static class HashedEntry extends AbstractEntry { + private final MAC sha1 = new HMACSHA1(); - public HashedEntry(Marker marker, String hash, KeyType type, PublicKey key) throws SSHException { - super(marker, type, key); - this.hashedHost = hash; - { - final String[] hostParts = hashedHost.split("\\|"); - if (hostParts.length != 4) - throw new SSHException("Unrecognized format for hashed hostname"); - salt = hostParts[2]; - } - } + private String salt; + private byte[] saltyBytes; - @Override - public boolean appliesTo(KeyType type, String host) throws IOException { - return this.type == type && hashedHost.equals(hashHost(host)); - } + private final String hashedHost; - private String hashHost(String host) throws IOException { - sha1.init(getSaltyBytes()); - return "|1|" + salt + "|" + Base64.encodeBytes(sha1.doFinal(host.getBytes(IOUtils.UTF8))); - } + public HashedEntry(Marker marker, String hash, KeyType type, PublicKey key) + throws SSHException { + super(marker, type, key); + this.hashedHost = hash; + { + final String[] hostParts = hashedHost.split("\\|"); + if (hostParts.length != 4) + throw new SSHException("Unrecognized format for hashed hostname"); + salt = hostParts[2]; + } + } - private byte[] getSaltyBytes() throws IOException { - if (saltyBytes == null) { - saltyBytes = Base64.decode(salt); - } - return saltyBytes; - } + @Override + public boolean appliesTo(KeyType type, String host) + throws IOException { + return this.type == type && hashedHost.equals(hashHost(host)); + } + + private String hashHost(String host) + throws IOException { + sha1.init(getSaltyBytes()); + return "|1|" + salt + "|" + Base64.encodeBytes(sha1.doFinal(host.getBytes(IOUtils.UTF8))); + } + + private byte[] getSaltyBytes() + throws IOException { + if (saltyBytes == null) { + saltyBytes = Base64.decode(salt); + } + return saltyBytes; + } - @Override - public String getLine() { - return null; - } + @Override + public String getLine() { + return null; + } - @Override - protected String getHostPart() { - return hashedHost; - } - } + @Override + protected String getHostPart() { + return hashedHost; + } + } - public enum Marker { - CA_CERT("@cert-authority"), REVOKED("@revoked"); + public enum Marker { + CA_CERT("@cert-authority"), REVOKED("@revoked"); - private final String sMarker; + private final String sMarker; - Marker(String sMarker) { - this.sMarker = sMarker; - } + Marker(String sMarker) { + this.sMarker = sMarker; + } - public String getMarkerString() { - return sMarker; - } - } + public String getMarkerString() { + return sMarker; + } + } - private static final Logger logger = LoggerFactory.getLogger(OpenSSHKnownHosts.class); + private static final Logger logger = LoggerFactory.getLogger(OpenSSHKnownHosts.class); } \ No newline at end of file