com.sshtools.publickey.AbstractKnownHostsKeyVerification Maven / Gradle / Ivy
/**
* Copyright 2003-2016 SSHTOOLS Limited. All Rights Reserved.
*
* For product documentation visit https://www.sshtools.com/
*
* This file is part of J2SSH Maverick.
*
* J2SSH Maverick is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* J2SSH Maverick is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with J2SSH Maverick. If not, see .
*/
package com.sshtools.publickey;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;
import com.sshtools.ssh.HostKeyVerification;
import com.sshtools.ssh.SshException;
import com.sshtools.ssh.components.ComponentManager;
import com.sshtools.ssh.components.SshHmac;
import com.sshtools.ssh.components.SshPublicKey;
import com.sshtools.ssh.components.SshRsaPublicKey;
import com.sshtools.util.Base64;
/**
*
* An abstract HostKeyVerification
* class implementation providing validation against the known_hosts format.
*
*
* @author Lee David Painter
*/
public abstract class AbstractKnownHostsKeyVerification implements
HostKeyVerification {
private Hashtable> allowedHosts = new Hashtable>();
private Hashtable> temporaryHosts = new Hashtable>();
private String knownhosts;
private boolean hostFileWriteable;
private boolean hashHosts = true;
private File knownhostsFile;
// Hashed support
private static final String HASH_MAGIC = "|1|";
private static final String HASH_DELIM = "|";
/**
* Construct a known_hosts database based on the default path of
* ~/.ssh/known_hosts.
*
*/
public AbstractKnownHostsKeyVerification() throws SshException {
this(null);
}
public File getKnownHostsFile() {
return knownhostsFile;
}
/**
*
* Constructs a known_hosts database based on the path provided.
*
*
* @param knownhosts
* the path of the known_hosts file
*
* @throws InvalidHostFileException
* if the known_hosts file is invalid
*
* @since 0.2.0
*/
public AbstractKnownHostsKeyVerification(String knownhosts)
throws SshException {
InputStream in = null;
if (knownhosts == null) {
String homeDir = "";
try {
homeDir = System.getProperty("user.home");
} catch (SecurityException e) {
// ignore
}
knownhostsFile = new File(homeDir, ".ssh" + File.separator
+ "known_hosts");
knownhosts = knownhostsFile.getAbsolutePath();
} else {
knownhostsFile = new File(knownhosts);
}
try {
// If no host file is supplied, or there is not enough permission to
// load
// the file, then just create an empty list.
if (System.getSecurityManager() != null) {
System.getSecurityManager().checkRead(knownhosts);
}
// Load the hosts file. Do not worry if file doesn't exist, just
// disable
// save of
if (knownhostsFile.exists()) {
in = new FileInputStream(knownhostsFile);
BufferedReader reader = new BufferedReader(
new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.equals("")) {
StringTokenizer tokens = new StringTokenizer(line, " ");
if (!tokens.hasMoreTokens()) {
// Do not fail just tell the implementation to allow
// it to decide what to do.
onInvalidHostEntry(line);
continue;
}
String host = (String) tokens.nextElement();
String algorithm = null;
try {
if (!tokens.hasMoreTokens()) {
// Do not fail just tell the implementation to
// allow it to decide what to do.
onInvalidHostEntry(line);
continue;
}
Integer.parseInt(algorithm = (String) tokens
.nextElement());
// Do not support SSH1 keys
} catch (OutOfMemoryError ox) {
reader.close();
throw new SshException(
"Error parsing known_hosts file, is your file corrupt? "
+ knownhostsFile.getAbsolutePath(),
SshException.POSSIBLE_CORRUPT_FILE);
} catch (NumberFormatException ex) {
if (!tokens.hasMoreTokens()) {
// Do not fail just tell the implementation to
// allow it to decide what to do.
onInvalidHostEntry(line);
continue;
}
String key = (String) tokens.nextElement();
try {
SshPublicKey pk;
if (algorithm != null) {
pk = SshPublicKeyFileFactory
.decodeSSH2PublicKey(algorithm,
Base64.decode(key));
} else {
pk = SshPublicKeyFileFactory
.decodeSSH2PublicKey(Base64
.decode(key));
}
putAllowedKey(host, pk, true);
} catch (IOException ex2) {
onInvalidHostEntry(line);
} catch (OutOfMemoryError oex) {
reader.close();
throw new SshException(
"Error parsing known_hosts file, is your file corrupt? "
+ knownhostsFile
.getAbsolutePath(),
SshException.POSSIBLE_CORRUPT_FILE);
}
}
}
}
reader.close();
in.close();
hostFileWriteable = knownhostsFile.canWrite();
} else {
// Try to create the file and its parents if necessary
File parent = new File(knownhostsFile.getParent());
parent.mkdirs();
FileOutputStream out = new FileOutputStream(knownhostsFile);
out.write(toString().getBytes());
out.close();
hostFileWriteable = true;
}
this.knownhosts = knownhosts;
} catch (IOException ioe) {
hostFileWriteable = false;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
}
}
}
}
public void setHashHosts(boolean hashHosts) {
this.hashHosts = hashHosts;
}
protected void onInvalidHostEntry(String entry) throws SshException {
// Do nothing
}
/**
*
* Determines whether the host file is writable.
*
*
* @return true if the host file is writable, otherwise false
*
* @since 0.2.0
*/
public boolean isHostFileWriteable() {
return hostFileWriteable;
}
/**
*
* Called by the verifyHost
method when the host key supplied
* by the host does not match the current key recording in the known hosts
* file.
*
*
* @param host
* the name of the host
* @param allowedHostKey
* the current key recorded in the known_hosts file.
* @param actualHostKey
* the actual key supplied by the user
*
* @throws SshException
* if an error occurs
*
* @since 0.2.0
*/
public abstract void onHostKeyMismatch(String host,
SshPublicKey allowedHostKey, SshPublicKey actualHostKey)
throws SshException;
/**
*
* Called by the verifyHost
method when the host key supplied
* is not recorded in the known_hosts file.
*
*
*
*
*
* @param host
* the name of the host
* @param key
* the public key supplied by the host
*
* @throws SshException
* if an error occurs
*
* @since 0.2.0
*/
public abstract void onUnknownHost(String host, SshPublicKey key)
throws SshException;
/**
*
* Allows a host key, optionally recording the key to the known_hosts file.
*
*
* @param host
* the name of the host
* @param pk
* the public key to allow
* @param always
* true if the key should be written to the known_hosts file
*
* @throws InvalidHostFileException
* if the host file cannot be written
*
* @since 0.2.0
*/
public void allowHost(String host, SshPublicKey pk, boolean always)
throws SshException {
// Put the host into the allowed hosts list, overiding any previous
// entry
if (hashHosts) {
SshHmac sha1 = (SshHmac) ComponentManager.getInstance()
.supportedHMacsCS().getInstance("hmac-sha1");
byte[] hashSalt = new byte[sha1.getMacLength()];
ComponentManager.getInstance().getRND().nextBytes(hashSalt);
sha1.init(hashSalt);
sha1.update(host.getBytes());
byte[] theHash = sha1.doFinal();
String names = HASH_MAGIC + Base64.encodeBytes(hashSalt, false)
+ HASH_DELIM + Base64.encodeBytes(theHash, false);
putAllowedKey(names, pk, always);
} else {
putAllowedKey(host, pk, always);
}
// allowedHosts.put(host, pk);
// If we always want to allow then save the host file with the
// new details
if (always) {
try {
saveHostFile();
} catch (IOException ex) {
throw new SshException("knownhosts file could not be saved! "
+ ex.getMessage(), SshException.INTERNAL_ERROR);
}
}
}
/**
*
* Returns a Map of the allowed hosts.
*
*
*
* The keys of the returned Map are comma separated strings of
* "hostname,ipaddress". The value objects are Maps containing a string key
* of the public key alogorithm name and the public key as the value.
*
*
* @return Hashtable>
*
* @since 0.2.0
*/
public Hashtable> allowedHosts() {
return allowedHosts;
}
/**
*
* Removes an allowed host.
*
*
* @param host
* the host to remove
*
* @since 0.2.0
*/
public synchronized void removeAllowedHost(String host) {
if (allowedHosts.containsKey(host)) {
allowedHosts.remove(host);
}
/*
* for (Enumeration e = allowedHosts.keys(); e.hasMoreElements(); ) {
* StringTokenizer tokens = new StringTokenizer( (String)
* e.nextElement(), ",");
*
* while (tokens.hasMoreElements()) { String name = (String)
* tokens.nextElement();
*
* if (name.equals(host)) { allowedHosts.remove(name); } } }
*/
}
/**
*
* Verifies a host key against the list of known_hosts.
*
*
*
* If the host unknown or the key does not match the currently allowed host
* key the abstract onUnknownHost
or
* onHostKeyMismatch
methods are called so that the caller may
* identify and allow the host.
*
*
* @param host
* the name of the host
* @param pk
* the host key supplied
*
* @return true if the host is accepted, otherwise false
*
* @throws SshException
* if an error occurs
*
* @since 0.2.0
*/
public boolean verifyHost(String host, SshPublicKey pk) throws SshException {
return verifyHost(host, pk, true);
}
private synchronized boolean verifyHost(String host, SshPublicKey pk,
boolean validateUnknown) throws SshException {
String fqn = null;
String ip = null;
if (System.getProperty("maverick.knownHosts.enableReverseDNS", "true")
.equalsIgnoreCase("true")) {
try {
InetAddress addr = InetAddress.getByName(host);
fqn = addr.getHostName();
ip = addr.getHostAddress();
} catch (UnknownHostException ex) {
// Just record the host as the user typed it
}
}
for (Enumeration e = allowedHosts.keys(); e.hasMoreElements();) {
// Could be a comma delimited string of names/ip addresses
String names = (String) e.nextElement();
if (names.startsWith(HASH_MAGIC)) {
// Create hash
if (checkHash(names, host)) {
return validateHost(names, pk);
}
if (ip != null) {
if (checkHash(names, ip)) {
return validateHost(names, pk);
}
}
} else if (names.equals(host)) {
return validateHost(names, pk);
}
StringTokenizer tokens = new StringTokenizer(names, ",");
while (tokens.hasMoreElements()) {
// Try the allowed hosts by looking at the allowed hosts map
String name = (String) tokens.nextElement();
if ((fqn != null && name.equals(fqn))
|| (ip != null && name.equals(ip))) {
return validateHost(names, pk);
}
}
}
for (Enumeration e = temporaryHosts.keys(); e.hasMoreElements();) {
// Could be a comma delimited string of names/ip addresses
String names = e.nextElement();
if (names.startsWith(HASH_MAGIC)) {
// Create hash
if (checkHash(names, host)) {
return validateHost(names, pk);
}
if (ip != null) {
if (checkHash(names, ip)) {
return validateHost(names, pk);
}
}
} else if (names.equals(host)) {
return validateHost(names, pk);
}
StringTokenizer tokens = new StringTokenizer(names, ",");
while (tokens.hasMoreElements()) {
// Try the allowed hosts by looking at the allowed hosts map
String name = (String) tokens.nextElement();
if ((fqn != null && name.equals(fqn))
|| (ip != null && name.equals(ip))) {
return validateHost(names, pk);
}
}
}
// The host is unknown os ask the user
if(!validateUnknown)
return false;
onUnknownHost(host, pk);
// Recheck ans return the result
return verifyHost(host, pk, false);
}
private boolean checkHash(String names, String host) throws SshException {
SshHmac sha1 = (SshHmac) ComponentManager.getInstance()
.supportedHMacsCS().getInstance("hmac-sha1");
String hashData = names.substring(HASH_MAGIC.length());
String hashSalt = hashData.substring(0, hashData.indexOf(HASH_DELIM));
String hashStr = hashData.substring(hashData.indexOf(HASH_DELIM) + 1);
byte[] theHash = Base64.decode(hashStr);
sha1.init(Base64.decode(hashSalt));
sha1.update(host.getBytes());
byte[] ourHash = sha1.doFinal();
return Arrays.equals(theHash, ourHash);
}
private boolean validateHost(String names, SshPublicKey pk)
throws SshException {
// The host is allowed so check the fingerprint
SshPublicKey pub = getAllowedKey(names, pk.getAlgorithm());
if ((pub != null) && pk.equals(pub)) {
return true;
}
// The host key does not match the recorded so call the abstract
// method so that the user can decide
if (pub == null) {
onUnknownHost(names, pk);
} else {
onHostKeyMismatch(names, pub, pk);
}
// Recheck the after the users input
return checkKey(names, pk);
}
private boolean checkKey(String host, SshPublicKey key) {
SshPublicKey pk = getAllowedKey(host, key.getAlgorithm());
if (pk != null) {
if (pk.equals(key)) {
return true;
}
}
return false;
}
private synchronized SshPublicKey getAllowedKey(String names, String algorithm) {
try {
for (Iterator it = temporaryHosts.keySet().iterator(); it.hasNext();) {
String name = it.next();
if (name.startsWith(HASH_DELIM)) {
if (checkHash(name, names)) {
Hashtable map = temporaryHosts.get(name);
return (SshPublicKey) map.get(algorithm);
}
}
}
} catch (SshException e) {
}
if (temporaryHosts.containsKey(names)) {
Hashtable map = temporaryHosts.get(names);
return (SshPublicKey) map.get(algorithm);
}
try {
for (Iterator it = allowedHosts.keySet().iterator(); it.hasNext();) {
String name = (String) it.next();
if (name.startsWith(HASH_DELIM)) {
if (checkHash(name, names)) {
Hashtable map = allowedHosts.get(name);
return (SshPublicKey) map.get(algorithm);
}
}
}
} catch (SshException e) {
}
if (allowedHosts.containsKey(names)) {
Hashtable map = allowedHosts.get(names);
return (SshPublicKey) map.get(algorithm);
}
return null;
}
private synchronized void putAllowedKey(String host, SshPublicKey key, boolean always) {
if (always) {
if (!allowedHosts.containsKey(host)) {
allowedHosts.put(host, new Hashtable());
}
Hashtable keys = allowedHosts.get(host);
keys.put(key.getAlgorithm(), key);
} else {
if (!temporaryHosts.containsKey(host)) {
temporaryHosts.put(host, new Hashtable());
}
Hashtable keys = temporaryHosts.get(host);
keys.put(key.getAlgorithm(), key);
}
}
/**
*
* Save's the host key file to be saved.
*
*
* @throws InvalidHostFileException
* if the host file is invalid
*
* @since 0.2.0
*/
public synchronized void saveHostFile() throws IOException {
if (!hostFileWriteable) {
throw new IOException("Host file is not writeable.");
}
try {
File f = new File(knownhosts);
FileOutputStream out = new FileOutputStream(f);
out.write(toString().getBytes());
out.close();
} catch (IOException e) {
throw new IOException("Could not write to " + knownhosts);
}
}
/**
*
* Outputs the allowed hosts in the known_hosts file format.
*
*
*
* The format consists of any number of lines each representing one key for
* a single host.
*
* titan,192.168.1.12 ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4Ed.....
* titan,192.168.1.12 ssh-rsa AAAAB3NzaC1kc3MAAACBAP1/U4Ed.....
* einstein,192.168.1.40 ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4Ed.....
*
* @return String
*
* @since 0.2.0
*/
public String toString() {
StringBuffer knownhostsBuf = new StringBuffer("");
String eol = System.getProperty("line.separator");
for (Enumeration e = allowedHosts.keys(); e.hasMoreElements();) {
String host = e.nextElement();
Hashtable table = allowedHosts.get(host);
for (Enumeration e2 = table.keys(); e2.hasMoreElements();) {
String type = e2.nextElement();
SshPublicKey pk = (SshPublicKey) table.get(type);
if (pk instanceof SshRsaPublicKey
&& ((SshRsaPublicKey) pk).getVersion() == 1) {
SshRsaPublicKey ssh1 = (SshRsaPublicKey) pk;
knownhostsBuf.append(host + " "
+ String.valueOf(ssh1.getModulus().bitLength())
+ " " + ssh1.getPublicExponent() + " "
+ ssh1.getModulus() + eol);
} else {
try {
knownhostsBuf
.append((host
+ " "
+ pk.getAlgorithm()
+ " "
+ Base64.encodeBytes(pk.getEncoded(),
true) + eol));
} catch (SshException ex) {
// Bad encoding... Ignore??
}
}
}
}
return knownhostsBuf.toString();
}
}