You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2016/02/14 15:16:52 UTC

[1/3] mina-sshd git commit: [SSHD-266] Support for known hosts

Repository: mina-sshd
Updated Branches:
  refs/heads/master 305850cfb -> 509c871ea


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
index e4f844a..a978b06 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
@@ -21,13 +21,19 @@ package org.apache.sshd.server.config.keys;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -47,6 +53,16 @@ import org.apache.sshd.server.session.ServerSession;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class AuthorizedKeysAuthenticator extends ModifiableFileWatcher implements PublickeyAuthenticator {
+
+    /**
+     * Standard OpenSSH authorized keys file name
+     */
+    public static final String STD_AUTHORIZED_KEYS_FILENAME = "authorized_keys";
+
+    private static final class LazyDefaultAuthorizedKeysFileHolder {
+        private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_AUTHORIZED_KEYS_FILENAME);
+    }
+
     private final AtomicReference<PublickeyAuthenticator> delegateHolder =  // assumes initially reject-all
             new AtomicReference<PublickeyAuthenticator>(RejectAllPublickeyAuthenticator.INSTANCE);
 
@@ -130,4 +146,29 @@ public class AuthorizedKeysAuthenticator extends ModifiableFileWatcher implement
         updateReloadAttributes();
         return entries;
     }
+
+    /**
+     * @return The default {@link Path} location of the OpenSSH authorized keys file
+     */
+    @SuppressWarnings("synthetic-access")
+    public static Path getDefaultAuthorizedKeysFile() {
+        return LazyDefaultAuthorizedKeysFileHolder.KEYS_FILE;
+    }
+
+    /**
+     * Reads read the contents of the default OpenSSH <code>authorized_keys</code> file
+     *
+     * @param options The {@link OpenOption}s to use when reading the file
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there -
+     * or empty if file does not exist
+     * @throws IOException If failed to read keys from file
+     */
+    public static List<AuthorizedKeyEntry> readDefaultAuthorizedKeys(OpenOption ... options) throws IOException {
+        Path keysFile = getDefaultAuthorizedKeysFile();
+        if (Files.exists(keysFile, IoUtils.EMPTY_LINK_OPTIONS)) {
+            return AuthorizedKeyEntry.readAuthorizedKeys(keysFile);
+        } else {
+            return Collections.emptyList();
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
index 6cdc7ea..ea9579f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
@@ -29,6 +29,7 @@ import java.util.Collection;
 import java.util.Collections;
 
 import org.apache.sshd.common.auth.UsernameHolder;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.Pair;
@@ -64,7 +65,7 @@ public class DefaultAuthorizedKeysAuthenticator extends AuthorizedKeysAuthentica
     }
 
     public DefaultAuthorizedKeysAuthenticator(String user, boolean strict) {
-        this(user, AuthorizedKeyEntry.getDefaultAuthorizedKeysFile(), strict);
+        this(user, getDefaultAuthorizedKeysFile(), strict);
     }
 
     public DefaultAuthorizedKeysAuthenticator(File file, boolean strict) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
index 24b47cb..666c9cd 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
@@ -26,6 +26,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
+import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyPair;
@@ -97,6 +98,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
@@ -1463,6 +1465,11 @@ public class ClientTest extends BaseTestSupport {
             session.addPasswordIdentity(getCurrentTestName());
             session.auth().verify(5L, TimeUnit.SECONDS);
 
+            InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+            assertNotNull("No reported connect address", addr);
+            assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
+            assertEquals("Mismatched connect port", port, addr.getPort());
+
             ClientSession returnValue = session;
             session = null; // avoid 'finally' close
             return returnValue;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
index da8bfa0..a75e18f 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
@@ -61,17 +61,17 @@ public class ConfigFileHostEntryResolverTest extends BaseTestSupport {
         testConfigFileReload("Non-existing", path, reloadCount, null, resolver, expected, null);
         testConfigFileReload("Empty", path, reloadCount, Collections.<HostConfigEntry>emptyList(), resolver, expected, null);
         testConfigFileReload("Global", path, reloadCount,
-                Collections.singletonList(new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, expected.getHost(), expected.getPort(), expected.getUsername())),
+                Collections.singletonList(new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, expected.getHost(), expected.getPort(), expected.getUsername())),
                 resolver, expected, expected);
         testConfigFileReload("Wildcard", path, reloadCount,
                 Arrays.asList(
-                        new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
-                        new HostConfigEntry(expected.getHost() + String.valueOf(HostConfigEntry.WILDCARD_PATTERN), expected.getHost(), expected.getPort(), expected.getUsername())),
+                        new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
+                        new HostConfigEntry(expected.getHost() + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), expected.getHost(), expected.getPort(), expected.getUsername())),
                 resolver, expected, expected);
         testConfigFileReload("Specific", path, reloadCount,
                 Arrays.asList(
-                        new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
-                        new HostConfigEntry(getClass().getSimpleName() + String.valueOf(HostConfigEntry.WILDCARD_PATTERN), getClass().getSimpleName(), 1234, getClass().getSimpleName()),
+                        new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
+                        new HostConfigEntry(getClass().getSimpleName() + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), getClass().getSimpleName(), 1234, getClass().getSimpleName()),
                         expected),
                 resolver, expected, expected);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index 9ff1dc1..4296b72 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -114,7 +114,7 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
     public void testNegatedHostEntriesResolution() throws Exception {
         HostConfigEntry positiveEntry = new HostConfigEntry(TEST_LOCALHOST, TEST_LOCALHOST, port, getCurrentTestName());
         HostConfigEntry negativeEntry = new HostConfigEntry(
-                String.valueOf(HostConfigEntry.NEGATION_CHAR_PATTERN) + positiveEntry.getHost(),
+                String.valueOf(HostPatternsHolder.NEGATION_CHAR_PATTERN) + positiveEntry.getHost(),
                 positiveEntry.getHostName(),
                 getMovedPortNumber(positiveEntry.getPort()),
                 getClass().getPackage().getName());
@@ -264,15 +264,7 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
     private static <S extends Session> S assertEffectiveRemoteAddress(S session, HostConfigEntry entry) {
         IoSession ioSession = session.getIoSession();
         SocketAddress remoteAddress = ioSession.getRemoteAddress();
-        InetSocketAddress inetAddress;
-        if (remoteAddress instanceof InetSocketAddress) {
-            inetAddress = (InetSocketAddress) remoteAddress;
-        } else if (remoteAddress instanceof SshdSocketAddress) {
-            inetAddress = ((SshdSocketAddress) remoteAddress).toInetSocketAddress();
-        } else {
-            throw new ClassCastException("Unknown remote address type: " + remoteAddress);
-        }
-
+        InetSocketAddress inetAddress = SshdSocketAddress.toInetSocketAddress(remoteAddress);
         assertEquals("Mismatched effective port", entry.getPort(), inetAddress.getPort());
         assertEquals("Mismatched effective user", entry.getUsername(), session.getUsername());
         return session;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
index d86a565..635c6cc 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
@@ -50,7 +50,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
         StringBuilder sb = new StringBuilder(testHost.length() + Byte.SIZE);
         List<Pair<Pattern, Boolean>> patterns = new ArrayList<>(elements.length + 1);
         // all wildcard patterns are not negated - only the actual host
-        patterns.add(HostConfigEntry.toPattern(String.valueOf(HostConfigEntry.NEGATION_CHAR_PATTERN) + testHost));
+        patterns.add(HostPatternsHolder.toPattern(String.valueOf(HostPatternsHolder.NEGATION_CHAR_PATTERN) + testHost));
 
         for (int i = 0; i < elements.length; i++) {
             sb.setLength(0);
@@ -60,17 +60,17 @@ public class HostConfigEntryTest extends BaseTestSupport {
                     sb.append('.');
                 }
                 if (i == j) {
-                    sb.append(HostConfigEntry.WILDCARD_PATTERN);
+                    sb.append(HostPatternsHolder.WILDCARD_PATTERN);
                 } else {
                     sb.append(elements[j]);
                 }
             }
 
-            patterns.add(HostConfigEntry.toPattern(sb));
+            patterns.add(HostPatternsHolder.toPattern(sb));
         }
 
         for (int index = 0; index < patterns.size(); index++) {
-            assertFalse("Unexpected match for " + patterns, HostConfigEntry.isHostMatch(testHost, patterns));
+            assertFalse("Unexpected match for " + patterns, HostPatternsHolder.isHostMatch(testHost, patterns));
             Collections.shuffle(patterns);
         }
     }
@@ -79,13 +79,13 @@ public class HostConfigEntryTest extends BaseTestSupport {
     public void testHostWildcardPatternMatching() {
         String pkgName = getClass().getPackage().getName();
         String[] elements = GenericUtils.split(pkgName, '.');
-        StringBuilder sb = new StringBuilder(pkgName.length() + Long.SIZE + 1).append(HostConfigEntry.WILDCARD_PATTERN);
+        StringBuilder sb = new StringBuilder(pkgName.length() + Long.SIZE + 1).append(HostPatternsHolder.WILDCARD_PATTERN);
         for (int index = elements.length - 1; index >= 0; index--) {
             sb.append('.').append(elements[index]);
         }
 
         String value = sb.toString();
-        Pair<Pattern, Boolean> pp = HostConfigEntry.toPattern(value);
+        Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(value);
         Pattern pattern = pp.getFirst();
         String domain = value.substring(1); // chomp the wildcard prefix
         for (String host : new String[] {
@@ -106,13 +106,13 @@ public class HostConfigEntryTest extends BaseTestSupport {
         StringBuilder sb = new StringBuilder().append("10.0.0.");
         int sbLen = sb.length();
 
-        Pattern pattern = HostConfigEntry.toPattern(sb.append(HostConfigEntry.WILDCARD_PATTERN)).getFirst();
+        Pattern pattern = HostPatternsHolder.toPattern(sb.append(HostPatternsHolder.WILDCARD_PATTERN)).getFirst();
         for (int v = 0; v <= 255; v++) {
             sb.setLength(sbLen);    // start from scratch
             sb.append(v);
 
             String address = sb.toString();
-            assertTrue("No match for " + address, HostConfigEntry.isHostMatch(address, pattern));
+            assertTrue("No match for " + address, HostPatternsHolder.isHostMatch(address, pattern));
         }
     }
 
@@ -122,8 +122,8 @@ public class HostConfigEntryTest extends BaseTestSupport {
         StringBuilder sb = new StringBuilder(value);
         for (boolean restoreOriginal : new boolean[] { true, false }) {
             for (int index = 0; index < value.length(); index++) {
-                sb.setCharAt(index, HostConfigEntry.SINGLE_CHAR_PATTERN);
-                testCaseInsensitivePatternMatching(value, HostConfigEntry.toPattern(sb.toString()).getFirst(), true);
+                sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN);
+                testCaseInsensitivePatternMatching(value, HostPatternsHolder.toPattern(sb.toString()).getFirst(), true);
                 if (restoreOriginal) {
                     sb.setCharAt(index, value.charAt(index));
                 }
@@ -143,46 +143,46 @@ public class HostConfigEntryTest extends BaseTestSupport {
             String address = sb.toString();
             // replace the added digits with single char pattern
             for (int index = sbLen; index < sb.length(); index++) {
-                sb.setCharAt(index, HostConfigEntry.SINGLE_CHAR_PATTERN);
+                sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN);
             }
 
             String pattern = sb.toString();
-            Pair<Pattern, Boolean> pp = HostConfigEntry.toPattern(pattern);
-            assertTrue("No match for " + address + " on pattern=" + pattern, HostConfigEntry.isHostMatch(address, Collections.singletonList(pp)));
+            Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(pattern);
+            assertTrue("No match for " + address + " on pattern=" + pattern, HostPatternsHolder.isHostMatch(address, Collections.singletonList(pp)));
         }
     }
 
     @Test
     public void testIsValidPatternChar() {
         for (char ch = '\0'; ch <= ' '; ch++) {
-            assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostConfigEntry.isValidPatternChar(ch));
+            assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostPatternsHolder.isValidPatternChar(ch));
         }
 
         for (char ch = 'a'; ch <= 'z'; ch++) {
-            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
         }
 
         for (char ch = 'A'; ch <= 'Z'; ch++) {
-            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
         }
 
         for (char ch = '0'; ch <= '9'; ch++) {
-            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
         }
 
-        for (char ch : new char[] { '-', '_', '.', HostConfigEntry.SINGLE_CHAR_PATTERN, HostConfigEntry.WILDCARD_PATTERN }) {
-            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+        for (char ch : new char[] { '-', '_', '.', HostPatternsHolder.SINGLE_CHAR_PATTERN, HostPatternsHolder.WILDCARD_PATTERN }) {
+            assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
         }
 
         for (char ch : new char[] {
                     '(', ')', '{', '}', '[', ']', '@',
                     '#', '$', '^', '&', '%', '~', '<', '>',
                     ',', '/', '\\', '\'', '"', ':', ';' }) {
-            assertFalse("Unexpected valid character: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+            assertFalse("Unexpected valid character: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
         }
 
         for (char ch = 0x7E; ch <= 0xFF; ch++) {
-            assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostConfigEntry.isValidPatternChar(ch));
+            assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostPatternsHolder.isValidPatternChar(ch));
         }
     }
 
@@ -220,7 +220,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
 
         // global entry MUST be 1st one
         HostConfigEntry globalEntry = entries.get(0);
-        assertEquals("Mismatched global entry pattern", HostConfigEntry.ALL_HOSTS_PATTERN, globalEntry.getHost());
+        assertEquals("Mismatched global entry pattern", HostPatternsHolder.ALL_HOSTS_PATTERN, globalEntry.getHost());
 
         for (int index = 1; index < entries.size(); index++) {
             HostConfigEntry entry = entries.get(index);
@@ -274,8 +274,8 @@ public class HostConfigEntryTest extends BaseTestSupport {
         final String HOST = getCurrentTestName();
         HostConfigEntry expected = new HostConfigEntry(HOST, HOST, 7365, HOST);
         List<HostConfigEntry> matches = new ArrayList<>();
-        matches.add(new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, getClass().getSimpleName(), Short.MAX_VALUE, getClass().getSimpleName()));
-        matches.add(new HostConfigEntry(HOST + String.valueOf(HostConfigEntry.WILDCARD_PATTERN), getClass().getSimpleName(), Byte.MAX_VALUE, getClass().getSimpleName()));
+        matches.add(new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), Short.MAX_VALUE, getClass().getSimpleName()));
+        matches.add(new HostConfigEntry(HOST + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), getClass().getSimpleName(), Byte.MAX_VALUE, getClass().getSimpleName()));
         matches.add(expected);
 
         for (int index = 0; index < matches.size(); index++) {
@@ -307,7 +307,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
     }
     private static void testCaseInsensitivePatternMatching(String value, Pattern pattern, boolean expected) {
         for (int index = 0; index < value.length(); index++) {
-            boolean actual = HostConfigEntry.isHostMatch(value, pattern);
+            boolean actual = HostPatternsHolder.isHostMatch(value, pattern);
             assertEquals("Mismatched match result for " + value + " on pattern=" + pattern.pattern(), expected, actual);
             value = shuffleCase(value);
         }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java
new file mode 100644
index 0000000..2dcdbff
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class KnownHostHashValueTest extends BaseTestSupport {
+    private final String hostName;
+    private final String hashValue;
+    private final KnownHostHashValue hash;
+
+    public KnownHostHashValueTest(String hostName, String hashValue) {
+        this.hostName = hostName;
+        this.hashValue = hashValue;
+        this.hash = KnownHostHashValue.parse(hashValue);
+    }
+
+    @Parameters(name = "host={0}, hash={1}")
+    public static Collection<Object[]> parameters() {
+        return Arrays.<Object[]>asList(
+                (Object[]) new String[]{ "192.168.1.61", "|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" });
+    }
+
+    @Test
+    public void testDecodeEncode() {
+        assertSame("Mismatched digester", KnownHostHashValue.DEFAULT_DIGEST, hash.getDigester());
+        assertEquals("Mismatched encoded form", hashValue, hash.toString());
+    }
+
+    @Test
+    public void testHostMatch() {
+        assertTrue("Specified host does not match", hash.isHostMatch(hostName));
+        assertFalse("Unexpected host match", hash.isHostMatch(getCurrentTestName()));
+    }
+
+    @Test
+    public void testCalculateHashValue() throws Exception {
+        byte[] expected = hash.getDigestValue();
+        byte[] actual = KnownHostHashValue.calculateHashValue(hostName, hash.getDigester(), hash.getSaltValue());
+        assertArrayEquals("Mismatched hash value", expected, actual);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
new file mode 100644
index 0000000..ec073f8
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
@@ -0,0 +1,358 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.keyverifier;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.client.config.hosts.KnownHostHashValue;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.random.JceRandomFactory;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class KnownHostsServerKeyVerifierTest extends BaseTestSupport {
+    private static final String HASHED_HOST = "192.168.1.61";
+    private static final Map<String, PublicKey> hostsKeys = new TreeMap<String, PublicKey>(String.CASE_INSENSITIVE_ORDER);
+    private static Map<String, KnownHostEntry> hostsEntries;
+    private static Path entriesFile;
+
+    public KnownHostsServerKeyVerifierTest() {
+        super();
+    }
+
+    @BeforeClass
+    public static void loadHostsEntries() throws Exception {
+        URL url = KnownHostsServerKeyVerifierTest.class.getResource(KnownHostEntry.STD_HOSTS_FILENAME);
+        assertNotNull("Missing test file resource", url);
+        entriesFile = new File(url.toURI()).toPath();
+        outputDebugMessage("loadHostsEntries(%s)", entriesFile);
+        hostsEntries = loadEntries(entriesFile);
+
+        for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+            String host = ke.getKey();
+            KnownHostEntry entry = ke.getValue();
+            AuthorizedKeyEntry authEntry = ValidateUtils.checkNotNull(entry.getKeyEntry(), "No key extracted from %s", entry);
+            PublicKey key = authEntry.resolvePublicKey(PublicKeyEntryResolver.FAILING);
+            assertNull("Multiple keys for host=" + host, hostsKeys.put(host, key));
+        }
+    }
+
+    @Test
+    public void testNoUpdatesNoNewHostsAuthentication() throws Exception {
+        final AtomicInteger delegateCount = new AtomicInteger(0);
+        ServerKeyVerifier delegate = new ServerKeyVerifier() {
+            @Override
+            public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+                delegateCount.incrementAndGet();
+                fail("verifyServerKey(" + clientSession + ")[" + remoteAddress + "] unexpected invocation");
+                return false;
+            }
+        };
+
+        final AtomicInteger updateCount = new AtomicInteger(0);
+        ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(delegate, createKnownHostsCopy()) {
+            @Override
+            protected KnownHostEntry updateKnownHostsFile(
+                    ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+                    Path file, Collection<HostEntryPair> knownHosts)
+                            throws Exception {
+                updateCount.incrementAndGet();
+                fail("updateKnownHostsFile(" + clientSession + ")[" + remoteAddress + "] unexpected invocation: " + file);
+                return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts);
+            }
+
+        };
+
+        for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+            String host = ke.getKey();
+            PublicKey serverKey = ke.getValue();
+            KnownHostEntry entry = hostsEntries.get(host);
+            outputDebugMessage("Verify host=%s", entry);
+            assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, host, serverKey));
+            assertEquals("Unexpected delegate invocation for host=" + entry, 0, delegateCount.get());
+            assertEquals("Unexpected update invocation for host=" + entry, 0, updateCount.get());
+        }
+    }
+
+    @Test
+    public void testFileUpdatedOnEveryNewHost() throws Exception {
+        final AtomicInteger delegateCount = new AtomicInteger(0);
+        ServerKeyVerifier delegate = new ServerKeyVerifier() {
+            @Override
+            public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+                delegateCount.incrementAndGet();
+                return true;
+            }
+        };
+
+        Path path = getKnownHostCopyPath();
+        Files.deleteIfExists(path);
+
+        final AtomicInteger updateCount = new AtomicInteger(0);
+        ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(delegate, path) {
+            @Override
+            protected KnownHostEntry updateKnownHostsFile(
+                    ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+                    Path file, Collection<HostEntryPair> knownHosts)
+                            throws Exception {
+                updateCount.incrementAndGet();
+                return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts);
+            }
+        };
+
+        int verificationCount = 0;
+        for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+            String host = ke.getKey();
+            PublicKey serverKey = ke.getValue();
+            KnownHostEntry entry = hostsEntries.get(host);
+            outputDebugMessage("Verify host=%s", entry);
+            assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, host, serverKey));
+            verificationCount++;
+            assertEquals("Mismatched number of delegate counts for server=" + entry, verificationCount, delegateCount.get());
+            assertEquals("Mismatched number of update counts for server=" + entry, verificationCount, updateCount.get());
+        }
+
+        // make sure we have all the original entries and ONLY them
+        Map<String, KnownHostEntry> updatedEntries = loadEntries(path);
+        for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+            String host = ke.getKey();
+            KnownHostEntry expected = ke.getValue();
+            KnownHostEntry actual = updatedEntries.remove(host);
+            assertNotNull("No updated entry for host=" + host, actual);
+
+            String expLine = expected.getConfigLine();
+            // if original is a list or hashed then replace them with the expected host
+            if ((expLine.indexOf(',') > 0) || (expLine.indexOf(KnownHostHashValue.HASHED_HOST_DELIMITER) >= 0)) {
+                int pos = expLine.indexOf(' ');
+                expLine = host + expLine.substring(pos);
+            }
+
+            int pos = expLine.indexOf("comment-");
+            if (pos > 0) {
+                expLine = expLine.substring(0, pos).trim();
+            }
+
+            assertEquals("Mismatched entry data for host=" + host, expLine, actual.getConfigLine());
+        }
+
+        assertTrue("Unexpected extra updated hosts: " + updatedEntries, updatedEntries.isEmpty());
+    }
+
+    @Test
+    public void testWriteHashedHostValues() throws Exception {
+        Path path = getKnownHostCopyPath();
+        Files.deleteIfExists(path);
+
+        KnownHostsServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, path) {
+            @Override
+            protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, String hostIdentity) {
+                return KnownHostHashValue.DEFAULT_DIGEST;
+            }
+        };
+
+        ClientFactoryManager manager = Mockito.mock(ClientFactoryManager.class);
+        Mockito.when(manager.getRandomFactory()).thenReturn(JceRandomFactory.INSTANCE);
+
+        ClientSession session = Mockito.mock(ClientSession.class);
+        Mockito.when(session.getFactoryManager()).thenReturn(manager);
+        for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+            String host = ke.getKey();
+            PublicKey serverKey = ke.getValue();
+            KnownHostEntry entry = hostsEntries.get(host);
+            outputDebugMessage("Write host=%s", entry);
+
+            SocketAddress address = new SshdSocketAddress(host, 7365);
+            Mockito.when(session.getConnectAddress()).thenReturn(address);
+            assertTrue("Failed to validate server=" + entry, verifier.verifyServerKey(session, address, serverKey));
+        }
+
+        // force re-read to ensure all values are hashed
+        Collection<HostEntryPair> keys = verifier.reloadKnownHosts(path);
+        for (HostEntryPair ke : keys) {
+            KnownHostEntry entry = ke.getHostEntry();
+            assertNotNull("No hashing for entry=" + entry, entry.getHashedEntry());
+        }
+        verifier.setLoadedHostsEntries(keys);
+
+        // make sure can still validate the original hosts
+        for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+            String host = ke.getKey();
+            PublicKey serverKey = ke.getValue();
+            KnownHostEntry entry = hostsEntries.get(host);
+            outputDebugMessage("Re-validate host=%s", entry);
+
+            SocketAddress address = new SshdSocketAddress(host, 7365);
+            Mockito.when(session.getConnectAddress()).thenReturn(address);
+            assertTrue("Failed to re-validate server=" + entry, verifier.verifyServerKey(session, address, serverKey));
+        }
+    }
+
+    @Test
+    public void testRejectModifiedServerKey() throws Exception {
+        KeyPair kp = Utils.generateKeyPair("RSA", 1024);
+        final PublicKey modifiedKey = kp.getPublic();
+        final AtomicInteger acceptCount = new AtomicInteger(0);
+        ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, createKnownHostsCopy()) {
+            @Override
+            protected boolean acceptModifiedServerKey(
+                    ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual) {
+                acceptCount.incrementAndGet();
+                assertSame("Mismatched actual key for " + remoteAddress, modifiedKey, actual);
+                return super.acceptModifiedServerKey(clientSession, remoteAddress, match, actual);
+            }
+        };
+
+        int validationCount = 0;
+        for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+            String host = ke.getKey();
+            KnownHostEntry entry = ke.getValue();
+            outputDebugMessage("Verify host=%s", entry);
+            assertFalse("Unexpected to verification success for " + entry, invokeVerifier(verifier, host, modifiedKey));
+            validationCount++;
+            assertEquals("Mismatched invocation count for host=" + entry, validationCount, acceptCount.get());
+        }
+    }
+
+    @Test
+    public void testAcceptModifiedServerKeyUpdatesFile() throws Exception {
+        KeyPair kp = Utils.generateKeyPair("RSA", 1024);
+        final PublicKey modifiedKey = kp.getPublic();
+        Path path = createKnownHostsCopy();
+        ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, path) {
+            @Override
+            protected boolean acceptModifiedServerKey(
+                    ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual) {
+                assertSame("Mismatched actual key for " + remoteAddress, modifiedKey, actual);
+                return true;
+            }
+        };
+
+        for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+            String host = ke.getKey();
+            KnownHostEntry entry = ke.getValue();
+            outputDebugMessage("Verify host=%s", entry);
+            assertTrue("Failed to verify " + entry, invokeVerifier(verifier, host, modifiedKey));
+        }
+
+        String expected = PublicKeyEntry.toString(modifiedKey);
+        Map<String, KnownHostEntry> updatedKeys = loadEntries(path);
+        for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+            String host = ke.getKey();
+            KnownHostEntry original = ke.getValue();
+            KnownHostEntry updated = updatedKeys.remove(host);
+            assertNotNull("No updated entry for " + original, updated);
+
+            String actual = updated.getConfigLine();
+            int pos = actual.indexOf(' ');
+            if (actual.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
+                for (pos++; pos < actual.length(); pos++) {
+                    if (actual.charAt(pos) != ' ') {
+                        break;
+                    }
+                }
+                pos = actual.indexOf(' ', pos);
+            }
+
+            actual = GenericUtils.trimToEmpty(actual.substring(pos + 1));
+            assertEquals("Mismatched updated value for host=" + host, expected, actual);
+        }
+
+        assertTrue("Unexpected extra updated entries: " + updatedKeys, updatedKeys.isEmpty());
+    }
+
+    private Path createKnownHostsCopy() throws IOException {
+        Path file = getKnownHostCopyPath();
+        Files.copy(entriesFile, file, StandardCopyOption.REPLACE_EXISTING);
+        return file;
+    }
+
+    private Path getKnownHostCopyPath() throws IOException {
+        Path file = getTempTargetRelativeFile(getClass().getSimpleName(), getCurrentTestName());
+        assertHierarchyTargetFolderExists(file.getParent());
+        return file;
+    }
+
+    private boolean invokeVerifier(ServerKeyVerifier verifier, String host, PublicKey serverKey) {
+        SocketAddress address = new SshdSocketAddress(host, 7365);
+        ClientSession session = Mockito.mock(ClientSession.class);
+        Mockito.when(session.getConnectAddress()).thenReturn(address);
+        Mockito.when(session.toString()).thenReturn(getCurrentTestName() + "[" + host + "]");
+        return verifier.verifyServerKey(session, address, serverKey);
+    }
+
+    private static Map<String, KnownHostEntry> loadEntries(Path file) throws IOException {
+        Collection<KnownHostEntry> entries = KnownHostEntry.readKnownHostEntries(file);
+        if (GenericUtils.isEmpty(entries)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, KnownHostEntry> hostsMap = new TreeMap<String, KnownHostEntry>(String.CASE_INSENSITIVE_ORDER);
+        for (KnownHostEntry entry : entries) {
+            String line = entry.getConfigLine();
+            outputDebugMessage("loadTestLines(%s) processing %s", file, line);
+            // extract hosts
+            int pos = line.indexOf(' ');
+            String patterns = line.substring(0, pos);
+            if (entry.getHashedEntry() != null) {
+                assertNull("Multiple hashed entries in file", hostsMap.put(HASHED_HOST, entry));
+            } else {
+                String[] addrs = GenericUtils.split(patterns, ',');
+                for (String a : addrs) {
+                    assertNull("Multiple entries for address=" + a, hostsMap.put(a, entry));
+                }
+            }
+        }
+
+        return hostsMap;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
index 57d49ba..7d5550d 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
@@ -758,7 +758,7 @@ public class ScpTest extends BaseTestSupport {
                 try {
                     scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
                     outputDebugMessage("Upload success to %s", remotePath);
-                } catch(ScpException e) {
+                } catch (ScpException e) {
                     Integer exitCode = e.getExitStatus();
                     assertNotNull("No upload exit status", exitCode);
                     assertEquals("Mismatched upload exit status", TEST_EXIT_VALUE, exitCode.intValue());
@@ -775,7 +775,7 @@ public class ScpTest extends BaseTestSupport {
                 try {
                     byte[] downloaded = scp.downloadBytes(remotePath);
                     outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8));
-                } catch(ScpException e) {
+                } catch (ScpException e) {
                     Integer exitCode = e.getExitStatus();
                     assertNotNull("No download exit status", exitCode);
                     assertEquals("Mismatched download exit status", TEST_EXIT_VALUE, exitCode.intValue());

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
new file mode 100644
index 0000000..a40eb1e
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
+import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
+import org.junit.FixMethodOrder;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class AuthorizedKeyEntryTest extends AuthorizedKeysTestSupport {
+    public AuthorizedKeyEntryTest() {
+        super();
+    }
+
+    @Test
+    public void testReadAuthorizedKeysFile() throws Exception {
+        Path file = getTempTargetRelativeFile(getCurrentTestName());
+        writeDefaultSupportedKeys(file);
+        runAuthorizedKeysTests(AuthorizedKeyEntry.readAuthorizedKeys(file));
+    }
+
+    @Test
+    public void testEncodePublicKeyEntry() throws Exception {
+        List<String> keyLines = loadDefaultSupportedKeys();
+        StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+        for (String line : keyLines) {
+            int pos = line.indexOf(' ');
+            String data = line;
+            String keyType = line.substring(0, pos);
+            // assume this happens if starts with login options
+            if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) {
+                data = line.substring(pos + 1).trim();
+            }
+
+            AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(data);
+            if (sb.length() > 0) {
+                sb.setLength(0);
+            }
+
+            PublicKey key = entry.appendPublicKey(sb, PublicKeyEntryResolver.FAILING);
+            assertNotNull("No key for line=" + line, key);
+
+            String encoded = sb.toString();
+            assertEquals("Mismatched encoded form for line=" + line, data, encoded);
+        }
+    }
+
+    @Test
+    @Ignore("It might cause some exceptions if user's file contains unsupported keys")
+    public void testReadDefaultAuthorizedKeysFile() throws Exception {
+        Path path = AuthorizedKeysAuthenticator.getDefaultAuthorizedKeysFile();
+        assertNotNull("No default location", path);
+
+        LinkOption[] options = IoUtils.getLinkOptions(false);
+        if (!Files.exists(path, options)) {
+            outputDebugMessage("Verify non-existing %s", path);
+            Collection<AuthorizedKeyEntry> entries = AuthorizedKeysAuthenticator.readDefaultAuthorizedKeys();
+            assertTrue("Non-empty keys even though file not found: " + entries, GenericUtils.isEmpty(entries));
+        } else {
+            assertFalse("Not a file: " + path, Files.isDirectory(path, options));
+            runAuthorizedKeysTests(AuthorizedKeysAuthenticator.readDefaultAuthorizedKeys());
+        }
+    }
+
+    private void runAuthorizedKeysTests(Collection<AuthorizedKeyEntry> entries) throws Exception {
+        testReadAuthorizedKeys(entries);
+        testAuthorizedKeysAuth(entries);
+    }
+
+    private static Collection<AuthorizedKeyEntry> testReadAuthorizedKeys(Collection<AuthorizedKeyEntry> entries) throws Exception {
+        assertFalse("No entries read", GenericUtils.isEmpty(entries));
+
+        Exception err = null;
+        for (AuthorizedKeyEntry entry : entries) {
+            try {
+                ValidateUtils.checkNotNull(entry.resolvePublicKey(PublicKeyEntryResolver.FAILING), "No public key resolved from %s", entry);
+            } catch (Exception e) {
+                System.err.append("Failed (").append(e.getClass().getSimpleName()).append(')')
+                        .append(" to resolve key of entry=").append(entry.toString())
+                        .append(": ").println(e.getMessage());
+                err = e;
+            }
+        }
+
+        if (err != null) {
+            throw err;
+        }
+
+        return entries;
+    }
+
+    private PublickeyAuthenticator testAuthorizedKeysAuth(Collection<AuthorizedKeyEntry> entries) throws Exception {
+        Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(PublicKeyEntryResolver.FAILING, entries);
+        PublickeyAuthenticator auth = AuthorizedKeyEntry.fromAuthorizedEntries(PublicKeyEntryResolver.FAILING, entries);
+        for (PublicKey key : keySet) {
+            assertTrue("Failed to authenticate with key=" + key.getAlgorithm(), auth.authenticate(getCurrentTestName(), key, null));
+        }
+
+        return auth;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java
new file mode 100644
index 0000000..54f9022
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
+import org.apache.sshd.util.test.BaseTestSupport;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AuthorizedKeysTestSupport extends BaseTestSupport {
+    protected AuthorizedKeysTestSupport() {
+        super();
+    }
+
+    protected List<String> writeDefaultSupportedKeys(Path file, OpenOption ... options) throws IOException {
+        List<String> keyLines = loadDefaultSupportedKeys();
+        if (Files.exists(file)) {
+            Files.delete(file);
+        }
+
+        assertHierarchyTargetFolderExists(file.getParent());
+
+        try (Writer w = Files.newBufferedWriter(file, StandardCharsets.UTF_8, options)) {
+            w.append(PublicKeyEntry.COMMENT_CHAR)
+             .append(' ').append(getCurrentTestName())
+             .append(' ').append(String.valueOf(keyLines.size())).append(" remaining keys")
+             .append(IoUtils.EOL)
+             ;
+            for (String l : keyLines) {
+                w.append(l).append(IoUtils.EOL);
+            }
+        }
+
+        return keyLines;
+    }
+
+    protected List<String> loadDefaultSupportedKeys() throws IOException {
+        return loadSupportedKeys(
+                ValidateUtils.checkNotNull(
+                        getClass().getResource(AuthorizedKeysAuthenticator.STD_AUTHORIZED_KEYS_FILENAME),
+                        "Missing resource=" + AuthorizedKeysAuthenticator.STD_AUTHORIZED_KEYS_FILENAME));
+    }
+
+    public static List<String> loadSupportedKeys(URL url) throws IOException {
+        return loadSupportedKeys(url.openStream(), true);
+    }
+
+    public static List<String> loadSupportedKeys(InputStream input, boolean okToClose) throws IOException {
+        try (Reader r = new InputStreamReader(NoCloseInputStream.resolveInputStream(input, okToClose), StandardCharsets.UTF_8)) {
+            return loadSupportedKeys(r, true);
+        }
+    }
+
+    public static List<String> loadSupportedKeys(Reader rdr, boolean okToClose) throws IOException {
+        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+            return loadSupportedKeys(buf);
+        }
+    }
+
+    public static List<String> loadSupportedKeys(BufferedReader rdr) throws IOException {
+        List<String> keyLines = new ArrayList<String>();
+        boolean eccSupported = SecurityUtils.hasEcc();
+        for (String l = rdr.readLine(); l != null; l = rdr.readLine()) {
+            l = GenericUtils.trimToEmpty(l);
+            // filter out empty and comment lines
+            if (GenericUtils.isEmpty(l) || (l.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
+                continue;
+            }
+
+            // skip EC keys if ECC not supported
+            if (l.contains(ECCurves.Constants.ECDSA_SHA2_PREFIX) && (!eccSupported)) {
+                System.out.println("Skip (ECC not supported) " + l);
+                continue;
+            }
+
+            keyLines.add(l);
+        }
+
+        return keyLines;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java b/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java
new file mode 100644
index 0000000..9c2ec8a
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.util;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class Base64Test extends BaseTestSupport {
+    public Base64Test() {
+        super();
+    }
+
+    @Test
+    public void testDiscardWhitespaceNullOrEmptyData() {
+        assertSame("Mismatched result for null data", GenericUtils.EMPTY_BYTE_ARRAY, Base64.discardWhitespace(null));
+        assertSame("Mismatched result for empty data", GenericUtils.EMPTY_BYTE_ARRAY, Base64.discardWhitespace(new byte[0]));
+    }
+
+    @Test
+    public void testDiscardWhitespaceOnNonWhitespace() {
+        byte[] expected = getCurrentTestName().getBytes(StandardCharsets.UTF_8);
+        byte[] actual = Base64.discardWhitespace(expected);
+        assertSame("Mismatched result", expected, actual);
+    }
+
+    @Test
+    public void testDiscardWhitespaceOnExistingWhitespace() {
+        String expected = getCurrentTestName();
+        StringBuilder sb = new StringBuilder(expected.length() * 2);
+        for (int index = 0; index < expected.length(); index++) {
+            sb.append(" \t\r\n".charAt(index % 4)).append(expected.charAt(index));
+        }
+
+        byte[] data = sb.toString().getBytes(StandardCharsets.UTF_8);
+        byte[] result = Base64.discardWhitespace(data);
+        assertEquals("Mismatched cleaned result", expected, new String(result, StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testBasicEncodeDecode() {
+        String expected = getCurrentTestName();
+        String b64 = Base64.encodeToString(expected.getBytes(StandardCharsets.UTF_8));
+        byte[] decoded = Base64.decodeBase64(b64.getBytes(StandardCharsets.UTF_8));
+        String actual = new String(decoded, StandardCharsets.UTF_8);
+        assertEquals("Mismatched decoded result for " + b64, expected, actual);
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
deleted file mode 100644
index 2420f8c..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.sshd.server.config.keys;
-
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.security.PublicKey;
-import java.util.Collection;
-import java.util.List;
-
-import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
-import org.junit.FixMethodOrder;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class AuthorizedKeyEntryTest extends AuthorizedKeysTestSupport {
-    public AuthorizedKeyEntryTest() {
-        super();
-    }
-
-    @Test
-    public void testReadAuthorizedKeysFile() throws Exception {
-        Path file = getTempTargetRelativeFile(getCurrentTestName());
-        writeDefaultSupportedKeys(file);
-        runAuthorizedKeysTests(AuthorizedKeyEntry.readAuthorizedKeys(file));
-    }
-
-    @Test
-    public void testEncodePublicKeyEntry() throws Exception {
-        List<String> keyLines = loadDefaultSupportedKeys();
-        StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
-        for (String line : keyLines) {
-            int pos = line.indexOf(' ');
-            String data = line;
-            String keyType = line.substring(0, pos);
-            // assume this happens if starts with login options
-            if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) {
-                data = line.substring(pos + 1).trim();
-            }
-
-            AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(data);
-            if (sb.length() > 0) {
-                sb.setLength(0);
-            }
-
-            PublicKey key = entry.appendPublicKey(sb, PublicKeyEntryResolver.FAILING);
-            assertNotNull("No key for line=" + line, key);
-
-            String encoded = sb.toString();
-            assertEquals("Mismatched encoded form for line=" + line, data, encoded);
-        }
-    }
-
-    @Test
-    @Ignore("It might cause some exceptions if user's file contains unsupported keys")
-    public void testReadDefaultAuthorizedKeysFile() throws Exception {
-        Path path = AuthorizedKeyEntry.getDefaultAuthorizedKeysFile();
-        assertNotNull("No default location", path);
-
-        LinkOption[] options = IoUtils.getLinkOptions(false);
-        if (!Files.exists(path, options)) {
-            outputDebugMessage("Verify non-existing %s", path);
-            Collection<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readDefaultAuthorizedKeys();
-            assertTrue("Non-empty keys even though file not found: " + entries, GenericUtils.isEmpty(entries));
-        } else {
-            assertFalse("Not a file: " + path, Files.isDirectory(path, options));
-            runAuthorizedKeysTests(AuthorizedKeyEntry.readDefaultAuthorizedKeys());
-        }
-    }
-
-    private void runAuthorizedKeysTests(Collection<AuthorizedKeyEntry> entries) throws Exception {
-        testReadAuthorizedKeys(entries);
-        testAuthorizedKeysAuth(entries);
-    }
-
-    private static Collection<AuthorizedKeyEntry> testReadAuthorizedKeys(Collection<AuthorizedKeyEntry> entries) throws Exception {
-        assertFalse("No entries read", GenericUtils.isEmpty(entries));
-
-        Exception err = null;
-        for (AuthorizedKeyEntry entry : entries) {
-            try {
-                ValidateUtils.checkNotNull(entry.resolvePublicKey(PublicKeyEntryResolver.FAILING), "No public key resolved from %s", entry);
-            } catch (Exception e) {
-                System.err.append("Failed (").append(e.getClass().getSimpleName()).append(')')
-                        .append(" to resolve key of entry=").append(entry.toString())
-                        .append(": ").println(e.getMessage());
-                err = e;
-            }
-        }
-
-        if (err != null) {
-            throw err;
-        }
-
-        return entries;
-    }
-
-    private PublickeyAuthenticator testAuthorizedKeysAuth(Collection<AuthorizedKeyEntry> entries) throws Exception {
-        Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(PublicKeyEntryResolver.FAILING, entries);
-        PublickeyAuthenticator auth = AuthorizedKeyEntry.fromAuthorizedEntries(PublicKeyEntryResolver.FAILING, entries);
-        for (PublicKey key : keySet) {
-            assertTrue("Failed to authenticate with key=" + key.getAlgorithm(), auth.authenticate(getCurrentTestName(), key, null));
-        }
-
-        return auth;
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
index 3ec0e35..33d895e 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
@@ -29,6 +29,8 @@ import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.AuthorizedKeysTestSupport;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.util.io.IoUtils;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java
deleted file mode 100644
index 3b6d0d5..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.sshd.server.config.keys;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.Writer;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.sshd.common.cipher.ECCurves;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.SecurityUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-import org.apache.sshd.common.util.io.NoCloseReader;
-import org.apache.sshd.util.test.BaseTestSupport;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class AuthorizedKeysTestSupport extends BaseTestSupport {
-    protected AuthorizedKeysTestSupport() {
-        super();
-    }
-
-    protected List<String> writeDefaultSupportedKeys(Path file, OpenOption ... options) throws IOException {
-        List<String> keyLines = loadDefaultSupportedKeys();
-        if (Files.exists(file)) {
-            Files.delete(file);
-        }
-
-        assertHierarchyTargetFolderExists(file.getParent());
-
-        try (Writer w = Files.newBufferedWriter(file, StandardCharsets.UTF_8, options)) {
-            w.append(PublicKeyEntry.COMMENT_CHAR)
-             .append(' ').append(getCurrentTestName())
-             .append(' ').append(String.valueOf(keyLines.size())).append(" remaining keys")
-             .append(IoUtils.EOL)
-             ;
-            for (String l : keyLines) {
-                w.append(l).append(IoUtils.EOL);
-            }
-        }
-
-        return keyLines;
-    }
-
-    protected List<String> loadDefaultSupportedKeys() throws IOException {
-        return loadSupportedKeys(
-                ValidateUtils.checkNotNull(
-                        getClass().getResource(AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME),
-                        "Missing resource=" + AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME));
-    }
-
-    public static List<String> loadSupportedKeys(URL url) throws IOException {
-        return loadSupportedKeys(url.openStream(), true);
-    }
-
-    public static List<String> loadSupportedKeys(InputStream input, boolean okToClose) throws IOException {
-        try (Reader r = new InputStreamReader(NoCloseInputStream.resolveInputStream(input, okToClose), StandardCharsets.UTF_8)) {
-            return loadSupportedKeys(r, true);
-        }
-    }
-
-    public static List<String> loadSupportedKeys(Reader rdr, boolean okToClose) throws IOException {
-        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
-            return loadSupportedKeys(buf);
-        }
-    }
-
-    public static List<String> loadSupportedKeys(BufferedReader rdr) throws IOException {
-        List<String> keyLines = new ArrayList<String>();
-        boolean eccSupported = SecurityUtils.hasEcc();
-        for (String l = rdr.readLine(); l != null; l = rdr.readLine()) {
-            l = GenericUtils.trimToEmpty(l);
-            // filter out empty and comment lines
-            if (GenericUtils.isEmpty(l) || (l.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
-                continue;
-            }
-
-            // skip EC keys if ECC not supported
-            if (l.contains(ECCurves.Constants.ECDSA_SHA2_PREFIX) && (!eccSupported)) {
-                System.out.println("Skip (ECC not supported) " + l);
-                continue;
-            }
-
-            keyLines.add(l);
-        }
-
-        return keyLines;
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
index cd6809c..5d6b22a 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
@@ -23,6 +23,8 @@ import java.nio.file.Path;
 import java.security.PublicKey;
 import java.util.Collection;
 
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.AuthorizedKeysTestSupport;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
new file mode 100644
index 0000000..2f8034b
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
@@ -0,0 +1,12 @@
+# multiple names for same host
+server.sshd.apache.org,10.23.222.240 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCMOm8SqJPJqvSa5Q6r/pGhkp3aBc4Evf9wF8DjhSy13m+wwQwQCENQ8V+5bpI58Z0jjB8O3lmuOLils+Nx9AFc= comment-host-list
+
+# comment
+
+10.23.222.127 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg=
+10.163.4.61 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
+
+192.168.174.129 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJvwXQ0Wc7TdhmjaTdkrHJvatWrw/6t6W1Hgh2nTauN+rhsMKYbmQeFTnzsP1NctWzQXwsqIOGcXIMNVhT92jgQ=
+
+# hash of 192.168.1.61 - DO NOT CHANGE IT !!!
+|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2KFr3GqL/3yXY2bAwRGGDxl/qLuE9qdx20+DMh5oAZPpwprlUnlxLm+ikimwn65Z0KeUyfofYKt+vc3rl1k2mDqyG8DqHeH0C+uFBbom0fthX7PRiQr2T9SOzSodjowZuBHlWIfgtcZI0bygX+GlKaAq00l4yCoe1xUTCRd2ZVyNuB1nozcFI+sUzdeKfaxvuyvbccG4tOx06HDryNdxW2e99bsAhLAg7d8xciOeb4PCAI1USg83dt0wVZE9VJbnRnoZ2y/DaQCJtBJ8t8uNLVdggakydDzQuglyd4dYRxeU7t4TEw6wsfXPB0kqdecd0Llspjx0ciEY/BbycdiApw== comment-hashed-host 
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys b/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys
new file mode 100644
index 0000000..3a12f32
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys
@@ -0,0 +1,16 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2KFr3GqL/3yXY2bAwRGGDxl/qLuE9qdx20+DMh5oAZPpwprlUnlxLm+ikimwn65Z0KeUyfofYKt+vc3rl1k2mDqyG8DqHeH0C+uFBbom0fthX7PRiQr2T9SOzSodjowZuBHlWIfgtcZI0bygX+GlKaAq00l4yCoe1xUTCRd2ZVyNuB1nozcFI+sUzdeKfaxvuyvbccG4tOx06HDryNdxW2e99bsAhLAg7d8xciOeb4PCAI1USg83dt0wVZE9VJbnRnoZ2y/DaQCJtBJ8t8uNLVdggakydDzQuglyd4dYRxeU7t4TEw6wsfXPB0kqdecd0Llspjx0ciEY/BbycdiApw== lgoldstein@LGOLDSTEIN-WIN7
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoiPvv8awFmA1iIwNlWD/29gfNyiTnkpjCVtFOqPGM8YTGF2G5FRrAwJyehdJu8qFJSUaNMzrjz4qlP4OyP4qcd16TE3FkIwd22+sXo0K9oEbOF307FVtBGrqnp+m5aFPQIZNhX86Rd9m0zrE4eSkZQ0qOjhp0Q60+G2lleaHSYvdc0IOcOJsWI43+ytlzJ2gKoVwPtttsXVtycjt2ZmD99V/lk3G7sdXQGL5S+lxn7rMtxamOSy+VR1eVu2ZagOCp2XZM1eFNWIRCH0KbRJh2mDrk08pIN9yCh2q/5BF+oh/CQyS8W8754MJuQ+0U0qHBH/wNtogIomedxpW8hG1jQ== lgoldstein@tlv-svn.eng.vmware.com
+
+# some empty lines
+
+
+# no comment for key
+ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBVdEJBWozLd41Huv2LZOglgE3HpRfcxvKBUB/poMAfRY6nMoStxaVCc/dPzpzE6nOAbX+tuRLL611H3Ooby9quSlKsSfx4/Dk76tPokY7T6eLTmoJP6S3c9OMdNFLQ31UKbW4RlBRFYFFMMQGr3PMzXRKDpQhl0Bvts+N5qQ2eaQ==
+
+# dummy login options
+command="/usr/bin/tinyfugue",environment="PATH=/bin:/usr/bin/:/opt/gtm/bin" ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBVdEJBWozLd41Huv2LZOglgE3HpRfcxvKBUB/poMAfRY6nMoStxaVCc/dPzpzE6nOAbX+tuRLL611H3Ooby9quSlKsSfx4/Dk76tPokY7T6eLTmoJP6S3c9OMdNFLQ31UKbW4RlBRFYFFMMQGr3PMzXRKDpQhl0Bvts+N5qQ2eaQ==
+hosts="git.eng.vmware.com" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
+
+# ECDSA keys
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg=
+host=10.23.222.240 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCMOm8SqJPJqvSa5Q6r/pGhkp3aBc4Evf9wF8DjhSy13m+wwQwQCENQ8V+5bpI58Z0jjB8O3lmuOLils+Nx9AFc=

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
index ff8f5ce..2584ba3 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
@@ -31,12 +31,12 @@ import java.util.Objects;
 
 import javax.naming.NamingException;
 
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.server.auth.LdapAuthenticator;
-import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.server.session.ServerSession;
 
 /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
index 7bf273f..c42e8f9 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
@@ -26,13 +26,13 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.directory.server.core.DirectoryService;
 import org.apache.directory.server.ldap.LdapServer;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.Pair;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.server.auth.BaseAuthenticatorTest;
-import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.server.session.ServerSession;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;


[3/3] mina-sshd git commit: [SSHD-266] Support for known hosts

Posted by lg...@apache.org.
[SSHD-266] Support for known hosts


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/509c871e
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/509c871e
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/509c871e

Branch: refs/heads/master
Commit: 509c871eac5f9dff521891971fb24673135b52c3
Parents: 305850c
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Sun Feb 14 16:16:35 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Sun Feb 14 16:16:35 2016 +0200

----------------------------------------------------------------------
 .../java/org/apache/sshd/client/SshClient.java  |   4 +-
 .../DefaultConfigFileHostEntryResolver.java     |  15 +-
 .../client/config/hosts/HostConfigEntry.java    | 448 +++---------
 .../client/config/hosts/HostPatternsHolder.java | 310 ++++++++
 .../client/config/hosts/KnownHostDigest.java    |  65 ++
 .../client/config/hosts/KnownHostEntry.java     | 271 +++++++
 .../client/config/hosts/KnownHostHashValue.java | 166 +++++
 .../DefaultKnownHostsServerKeyVerifier.java     |  89 +++
 .../KnownHostsServerKeyVerifier.java            | 720 +++++++++++++++++++
 .../client/keyverifier/ServerKeyVerifier.java   |  10 +-
 .../client/session/AbstractClientSession.java   |  11 +
 .../sshd/client/session/ClientSession.java      |  12 +
 .../common/config/keys/AuthorizedKeyEntry.java  | 361 ++++++++++
 .../org/apache/sshd/common/mac/BaseMac.java     |  14 +-
 .../java/org/apache/sshd/common/mac/Mac.java    |   2 +
 .../org/apache/sshd/common/util/Base64.java     |  98 ++-
 .../java/org/apache/sshd/common/util/Pair.java  |   2 +-
 .../sshd/common/util/buffer/BufferUtils.java    |  10 +-
 .../common/util/io/ModifiableFileWatcher.java   |   7 +-
 .../sshd/common/util/net/SshdSocketAddress.java |  41 ++
 .../server/config/keys/AuthorizedKeyEntry.java  | 399 ----------
 .../keys/AuthorizedKeysAuthenticator.java       |  41 ++
 .../DefaultAuthorizedKeysAuthenticator.java     |   3 +-
 .../java/org/apache/sshd/client/ClientTest.java |   7 +
 .../hosts/ConfigFileHostEntryResolverTest.java  |  10 +-
 .../hosts/HostConfigEntryResolverTest.java      |  12 +-
 .../config/hosts/HostConfigEntryTest.java       |  50 +-
 .../config/hosts/KnownHostHashValueTest.java    |  73 ++
 .../KnownHostsServerKeyVerifierTest.java        | 358 +++++++++
 .../org/apache/sshd/client/scp/ScpTest.java     |   4 +-
 .../config/keys/AuthorizedKeyEntryTest.java     | 137 ++++
 .../config/keys/AuthorizedKeysTestSupport.java  | 121 ++++
 .../org/apache/sshd/common/util/Base64Test.java |  73 ++
 .../config/keys/AuthorizedKeyEntryTest.java     | 135 ----
 .../keys/AuthorizedKeysAuthenticatorTest.java   |   2 +
 .../config/keys/AuthorizedKeysTestSupport.java  | 120 ----
 .../DefaultAuthorizedKeysAuthenticatorTest.java |   2 +
 .../apache/sshd/client/keyverifier/known_hosts  |  12 +
 .../sshd/common/config/keys/authorized_keys     |  16 +
 .../auth/pubkey/LdapPublickeyAuthenticator.java |   2 +-
 .../pubkey/LdapPublickeyAuthenticatorTest.java  |   2 +-
 41 files changed, 3123 insertions(+), 1112 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 5d567b1..d159fe1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -73,6 +73,7 @@ import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
 import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.client.future.DefaultConnectFuture;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.AbstractClientSession;
 import org.apache.sshd.client.session.ClientConnectionServiceFactory;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionCreator;
@@ -590,8 +591,9 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
 
     protected void onConnectOperationComplete(IoSession ioSession, ConnectFuture connectFuture,
             String username, SocketAddress address, Collection<? extends KeyPair> identities, boolean useDefaultIdentities) {
-        ClientSession session = (ClientSession) AbstractSession.getSession(ioSession);
+        AbstractClientSession session = (AbstractClientSession) AbstractSession.getSession(ioSession);
         session.setUsername(username);
+        session.setConnectAddress(address);
 
         if (useDefaultIdentities) {
             setupDefaultSessionIdentities(session);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
index 6b1e86b..3d39b6d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
@@ -32,9 +32,9 @@ import org.apache.sshd.common.util.io.IoUtils;
 
 /**
  * Monitors the {@code ~/.ssh/config} file of the user currently running
- * the server, re-loading it if necessary. It also (optionally) enforces the same
- * permissions regime as {@code OpenSSH} does for the file permissions.
-
+ * the client, re-loading it if necessary. It also (optionally) enforces
+ * the same permissions regime as {@code OpenSSH}
+ *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResolver {
@@ -48,8 +48,9 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
 
     /**
      * @param strict If {@code true} then makes sure that the containing folder
-     *               has 0700 access and the file 0600. <B>Note:</B> for <I>Windows</I> it
+     *               has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
      *               does not check these permissions
+     * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
      */
     public DefaultConfigFileHostEntryResolver(boolean strict) {
         this(HostConfigEntry.getDefaultHostConfigFile(), strict);
@@ -64,6 +65,12 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
         this.strict = strict;
     }
 
+    /**
+     * @return If {@code true} then makes sure that the containing folder
+     * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
+     * does not check these permissions
+     * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
+     */
     public final boolean isStrict() {
         return strict;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 52a8a83..1d3fc09 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -46,16 +46,12 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
 import org.apache.sshd.common.auth.MutableUserHolder;
 import org.apache.sshd.common.config.SshConfigFileReader;
 import org.apache.sshd.common.config.keys.IdentityUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.Pair;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
@@ -68,34 +64,12 @@ import org.apache.sshd.common.util.io.NoCloseReader;
  * file format</A>
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class HostConfigEntry implements MutableUserHolder {
-
-    /**
-     * Used in a host pattern to denote zero or more consecutive characters
-     */
-    public static final char WILDCARD_PATTERN = '*';
-    public static final String ALL_HOSTS_PATTERN = String.valueOf(HostConfigEntry.WILDCARD_PATTERN);
-
+public class HostConfigEntry extends HostPatternsHolder implements MutableUserHolder {
     /**
      * Standard OpenSSH config file name
      */
     public static final String STD_CONFIG_FILENAME = "config";
 
-    /**
-     * Used in a host pattern to denote any <U>one</U> character
-     */
-    public static final char SINGLE_CHAR_PATTERN = '?';
-
-    /**
-     * Used to negate a host pattern
-     */
-    public static final char NEGATION_CHAR_PATTERN = '!';
-
-    /**
-     * The available pattern characters
-     */
-    public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
-
     public static final String HOST_CONFIG_PROP = "Host";
     public static final String HOST_NAME_CONFIG_PROP = "HostName";
     public static final String PORT_CONFIG_PROP = SshConfigFileReader.PORT_CONFIG_PROP;
@@ -130,7 +104,7 @@ public class HostConfigEntry implements MutableUserHolder {
     public static final char REMOTE_PORT_MACRO = 'p';
 
     private static final class LazyDefaultConfigFileHolder {
-        private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
+        private static final Path CONFIG_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
     }
 
     private String host;
@@ -138,7 +112,6 @@ public class HostConfigEntry implements MutableUserHolder {
     private int port;
     private String username;
     private Boolean exclusiveIdentites;
-    private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
     private Collection<String> identities = Collections.emptyList();
     private Map<String, String> properties = Collections.emptyMap();
 
@@ -162,27 +135,12 @@ public class HostConfigEntry implements MutableUserHolder {
 
     public void setHost(String host) {
         this.host = host;
-        this.patterns = parsePatterns(parseConfigValue(host));
+        setPatterns(parsePatterns(parseConfigValue(host)));
     }
 
     public void setHost(Collection<String> patterns) {
         this.host = GenericUtils.join(ValidateUtils.checkNotNullAndNotEmpty(patterns, "No patterns"), ',');
-        this.patterns = parsePatterns(patterns);
-    }
-
-    public Collection<Pair<Pattern, Boolean>> getPatterns() {
-        return patterns;
-    }
-
-    /**
-     * Checks if a given host name / address matches the entry's host pattern(s)
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @return {@code true} if the name / address matches the pattern(s)
-     * @see #isHostMatch(String, Pattern)
-     */
-    public boolean isHostMatch(String host) {
-        return isHostMatch(host, getPatterns());
+        setPatterns(parsePatterns(patterns));
     }
 
     /**
@@ -777,165 +735,6 @@ public class HostConfigEntry implements MutableUserHolder {
     }
 
     /**
-     * Finds the best match out of the given ones.
-     *
-     * @param matches The available matches - ignored if {@code null}/empty
-     * @return The best match or {@code null} if no matches or no best match found
-     * @see #findBestMatch(Iterator)
-     */
-    public static HostConfigEntry findBestMatch(Collection<? extends HostConfigEntry> matches) {
-        if (GenericUtils.isEmpty(matches)) {
-            return null;
-        } else {
-            return findBestMatch(matches.iterator());
-        }
-    }
-
-    /**
-     * Finds the best match out of the given ones.
-     *
-     * @param matches The available matches - ignored if {@code null}/empty
-     * @return The best match or {@code null} if no matches or no best match found
-     * @see #findBestMatch(Iterator)
-     */
-    public static HostConfigEntry findBestMatch(Iterable<? extends HostConfigEntry> matches) {
-        if (matches == null) {
-            return null;
-        } else {
-            return findBestMatch(matches.iterator());
-        }
-    }
-
-    /**
-     * Finds the best match out of the given ones. The best match is defined as one whose
-     * pattern is as <U>specific</U> as possible (if more than one match is available).
-     * I.e., a non-global match is preferred over global one, and a match with no wildcards
-     * is preferred over one with such a pattern.
-     *
-     * @param matches The available matches - ignored if {@code null}/empty
-     * @return The best match or {@code null} if no matches or no best match found
-     * @see #isSpecificHostPattern(String)
-     */
-    public static HostConfigEntry findBestMatch(Iterator<? extends HostConfigEntry> matches) {
-        if ((matches == null) || (!matches.hasNext())) {
-            return null;
-        }
-
-        HostConfigEntry candidate = matches.next();
-        int wildcardMatches = 0;
-        while (matches.hasNext()) {
-            HostConfigEntry entry = matches.next();
-            String entryPattern = entry.getHost();
-            String candidatePattern = candidate.getHost();
-            // prefer non-global entry over global entry
-            if (ALL_HOSTS_PATTERN.equalsIgnoreCase(candidatePattern)) {
-                // unlikely, but handle it
-                if (ALL_HOSTS_PATTERN.equalsIgnoreCase(entryPattern)) {
-                    wildcardMatches++;
-                } else {
-                    candidate = entry;
-                    wildcardMatches = 0;
-                }
-                continue;
-            }
-
-            if (isSpecificHostPattern(entryPattern)) {
-                // if both are specific then no best match
-                if (isSpecificHostPattern(candidatePattern)) {
-                    return null;
-                }
-
-                candidate = entry;
-                wildcardMatches = 0;
-                continue;
-            }
-
-            wildcardMatches++;
-        }
-
-        String candidatePattern = candidate.getHost();
-        // best match either has specific host or no wildcard matches
-        if ((wildcardMatches <= 0) || (isSpecificHostPattern(candidatePattern))) {
-            return candidate;
-        }
-
-        return null;
-    }
-
-    /**
-     * @param pattern The pattern to check - ignored if {@code null}/empty
-     * @return {@code true} if the pattern is not empty and contains no wildcard characters
-     * @see #WILDCARD_PATTERN
-     * @see #SINGLE_CHAR_PATTERN
-     * @see #SINGLE_CHAR_PATTERN
-     */
-    public static boolean isSpecificHostPattern(String pattern) {
-        if (GenericUtils.isEmpty(pattern)) {
-            return false;
-        }
-
-        for (int index = 0; index < PATTERN_CHARS.length(); index++) {
-            char ch = PATTERN_CHARS.charAt(index);
-            if (pattern.indexOf(ch) >= 0) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Locates all the matching entries for a give host name / address
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
-     * @return A {@link List} of all the matching entries
-     * @see #isHostMatch(String)
-     */
-    public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry ... entries) {
-        // TODO in Java-8 use Stream(s) + predicate
-        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
-            return Collections.emptyList();
-        } else {
-            return findMatchingEntries(host, Arrays.asList(entries));
-        }
-    }
-
-    /**
-     * Locates all the matching entries for a give host name / address
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
-     * @return A {@link List} of all the matching entries
-     * @see #isHostMatch(String)
-     */
-    public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
-        // TODO in Java-8 use Stream(s) + predicate
-        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
-            return Collections.emptyList();
-        }
-
-        List<HostConfigEntry> matches = null;
-        for (HostConfigEntry entry : entries) {
-            if (!entry.isHostMatch(host)) {
-                continue;   // debug breakpoint
-            }
-
-            if (matches == null) {
-                matches = new ArrayList<>(entries.size());  // in case ALL of them match
-            }
-
-            matches.add(entry);
-        }
-
-        if (matches == null) {
-            return Collections.emptyList();
-        } else {
-            return matches;
-        }
-    }
-
-    /**
      * Resolves the effective target host
      *
      * @param originalName The original requested host
@@ -983,154 +782,6 @@ public class HostConfigEntry implements MutableUserHolder {
         }
     }
 
-    public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
-        if (GenericUtils.isEmpty(patterns)) {
-            return false;
-        }
-
-        boolean matchFound = false;
-        for (Pair<Pattern, Boolean> pp : patterns) {
-            Boolean negated = pp.getSecond();
-            /*
-             * If already found a match we are interested only in negations
-             */
-            if (matchFound && (!negated.booleanValue())) {
-                continue;
-            }
-
-            if (!isHostMatch(host, pp.getFirst())) {
-                continue;
-            }
-
-            /*
-             * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
-             *
-             *      If a negated entry is matched, then the Host entry is ignored,
-             *      regardless of whether any other patterns on the line match.
-             */
-            if (negated.booleanValue()) {
-                return false;
-            }
-
-            matchFound = true;
-        }
-
-        return matchFound;
-    }
-
-    /**
-     * Checks if a given host name / address matches a host pattern
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @param pattern The host {@link Pattern} - ignored if {@code null}
-     * @return {@code true} if the name / address matches the pattern
-     */
-    public static boolean isHostMatch(String host, Pattern pattern) {
-        if (GenericUtils.isEmpty(host) || (pattern == null)) {
-            return false;
-        }
-
-        Matcher m = pattern.matcher(host);
-        return m.matches();
-    }
-
-    public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
-        if (GenericUtils.isEmpty(patterns)) {
-            return Collections.emptyList();
-        }
-
-        List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
-        for (CharSequence p : patterns) {
-            result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
-        }
-
-        return result;
-    }
-
-    /**
-     * Converts a host pattern string to a regular expression matcher.
-     * <B>Note:</B> pattern matching is <U>case insensitive</U>
-     *
-     * @param pattern The original pattern string - ignored if {@code null}/empty
-     * @return The regular expression matcher {@link Pattern} and the indication
-     * whether it is a negating pattern or not - {@code null} if no original string
-     * @see #WILDCARD_PATTERN
-     * @see #SINGLE_CHAR_PATTERN
-     * @see #NEGATION_CHAR_PATTERN
-     */
-    public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
-        if (GenericUtils.isEmpty(pattern)) {
-            return null;
-        }
-
-        StringBuilder sb = new StringBuilder(pattern.length());
-        boolean negated = false;
-        for (int curPos = 0; curPos < pattern.length(); curPos++) {
-            char ch = pattern.charAt(curPos);
-            ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
-
-            switch(ch) {
-                case '.':   // need to escape it
-                    sb.append('\\').append(ch);
-                    break;
-                case SINGLE_CHAR_PATTERN:
-                    sb.append('.');
-                    break;
-                case WILDCARD_PATTERN:
-                    sb.append(".*");
-                    break;
-                case NEGATION_CHAR_PATTERN:
-                    ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
-                    ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
-                    negated = true;
-                    break;
-                default:
-                    sb.append(ch);
-            }
-        }
-
-        return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
-    }
-
-    /**
-     * Checks if the given character is valid for a host pattern. Valid
-     * characters are:
-     * <UL>
-     *      <LI>A-Z</LI>
-     *      <LI>a-z</LI>
-     *      <LI>0-9</LI>
-     *      <LI>Underscore (_)</LI>
-     *      <LI>Hyphen (-)</LI>
-     *      <LI>Dot (.)</LI>
-     *      <LI>The {@link #WILDCARD_PATTERN}</LI>
-     *      <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
-     * </UL>
-     *
-     * @param ch The character to validate
-     * @return {@code true} if valid pattern character
-     */
-    public static boolean isValidPatternChar(char ch) {
-        if ((ch <= ' ') || (ch >= 0x7E)) {
-            return false;
-        }
-        if ((ch >= 'a') && (ch <= 'z')) {
-            return true;
-        }
-        if ((ch >= 'A') && (ch <= 'Z')) {
-            return true;
-        }
-        if ((ch >= '0') && (ch <= '9')) {
-            return true;
-        }
-        if ("-_.".indexOf(ch) >= 0) {
-            return true;
-        }
-        if (PATTERN_CHARS.indexOf(ch) >= 0) {
-            return true;
-        }
-        return false;
-    }
-
     public static List<HostConfigEntry> readHostConfigEntries(File file) throws IOException {
         return readHostConfigEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
     }
@@ -1259,6 +910,92 @@ public class HostConfigEntry implements MutableUserHolder {
         }
     }
 
+    /**
+     * Finds the best match out of the given ones.
+     *
+     * @param matches The available matches - ignored if {@code null}/empty
+     * @return The best match or {@code null} if no matches or no best match found
+     * @see #findBestMatch(Iterator)
+     */
+    public static HostConfigEntry findBestMatch(Collection<? extends HostConfigEntry> matches) {
+        if (GenericUtils.isEmpty(matches)) {
+            return null;
+        } else {
+            return findBestMatch(matches.iterator());
+        }
+    }
+
+    /**
+     * Finds the best match out of the given ones.
+     *
+     * @param matches The available matches - ignored if {@code null}/empty
+     * @return The best match or {@code null} if no matches or no best match found
+     * @see #findBestMatch(Iterator)
+     */
+    public static HostConfigEntry findBestMatch(Iterable<? extends HostConfigEntry> matches) {
+        if (matches == null) {
+            return null;
+        } else {
+            return findBestMatch(matches.iterator());
+        }
+    }
+
+    /**
+     * Finds the best match out of the given ones. The best match is defined as one whose
+     * pattern is as <U>specific</U> as possible (if more than one match is available).
+     * I.e., a non-global match is preferred over global one, and a match with no wildcards
+     * is preferred over one with such a pattern.
+     *
+     * @param matches The available matches - ignored if {@code null}/empty
+     * @return The best match or {@code null} if no matches or no best match found
+     * @see #isSpecificHostPattern(String)
+     */
+    public static HostConfigEntry findBestMatch(Iterator<? extends HostConfigEntry> matches) {
+        if ((matches == null) || (!matches.hasNext())) {
+            return null;
+        }
+
+        HostConfigEntry candidate = matches.next();
+        int wildcardMatches = 0;
+        while (matches.hasNext()) {
+            HostConfigEntry entry = matches.next();
+            String entryPattern = entry.getHost();
+            String candidatePattern = candidate.getHost();
+            // prefer non-global entry over global entry
+            if (ALL_HOSTS_PATTERN.equalsIgnoreCase(candidatePattern)) {
+                // unlikely, but handle it
+                if (ALL_HOSTS_PATTERN.equalsIgnoreCase(entryPattern)) {
+                    wildcardMatches++;
+                } else {
+                    candidate = entry;
+                    wildcardMatches = 0;
+                }
+                continue;
+            }
+
+            if (isSpecificHostPattern(entryPattern)) {
+                // if both are specific then no best match
+                if (isSpecificHostPattern(candidatePattern)) {
+                    return null;
+                }
+
+                candidate = entry;
+                wildcardMatches = 0;
+                continue;
+            }
+
+            wildcardMatches++;
+        }
+
+        String candidatePattern = candidate.getHost();
+        // best match either has specific host or no wildcard matches
+        if ((wildcardMatches <= 0) || (isSpecificHostPattern(candidatePattern))) {
+            return candidate;
+        }
+
+        return null;
+    }
+
     public static List<HostConfigEntry> updateEntriesList(List<HostConfigEntry> entries, HostConfigEntry curEntry) {
         if (curEntry == null) {
             return entries;
@@ -1271,6 +1008,7 @@ public class HostConfigEntry implements MutableUserHolder {
         entries.add(curEntry);
         return entries;
     }
+
     public static void writeHostConfigEntries(File file, Collection<? extends HostConfigEntry> entries) throws IOException {
         writeHostConfigEntries(ValidateUtils.checkNotNull(file, "No file").toPath(), entries, IoUtils.EMPTY_OPEN_OPTIONS);
     }
@@ -1420,10 +1158,10 @@ public class HostConfigEntry implements MutableUserHolder {
     }
 
     /**
-     * @return The default {@link Path} location of the OpenSSH authorized keys file
+     * @return The default {@link Path} location of the OpenSSH hosts entries configuration file
      */
     @SuppressWarnings("synthetic-access")
     public static Path getDefaultHostConfigFile() {
-        return LazyDefaultConfigFileHolder.KEYS_FILE;
+        return LazyDefaultConfigFileHolder.CONFIG_FILE;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
new file mode 100644
index 0000000..4878458
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class HostPatternsHolder {
+
+    /**
+     * Used in a host pattern to denote zero or more consecutive characters
+     */
+    public static final char WILDCARD_PATTERN = '*';
+    public static final String ALL_HOSTS_PATTERN = String.valueOf(WILDCARD_PATTERN);
+
+    /**
+     * Used in a host pattern to denote any <U>one</U> character
+     */
+    public static final char SINGLE_CHAR_PATTERN = '?';
+
+    /**
+     * Used to negate a host pattern
+     */
+    public static final char NEGATION_CHAR_PATTERN = '!';
+
+    /**
+     * The available pattern characters
+     */
+    public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
+
+    private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
+
+    protected HostPatternsHolder() {
+        super();
+    }
+
+    public Collection<Pair<Pattern, Boolean>> getPatterns() {
+        return patterns;
+    }
+
+    public void setPatterns(Collection<Pair<Pattern, Boolean>> patterns) {
+        this.patterns = patterns;
+    }
+
+    /**
+     * Checks if a given host name / address matches the entry's host pattern(s)
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @return {@code true} if the name / address matches the pattern(s)
+     * @see #isHostMatch(String, Pattern)
+     */
+    public boolean isHostMatch(String host) {
+        return isHostMatch(host, getPatterns());
+    }
+
+    /**
+     * @param pattern The pattern to check - ignored if {@code null}/empty
+     * @return {@code true} if the pattern is not empty and contains no wildcard characters
+     * @see #WILDCARD_PATTERN
+     * @see #SINGLE_CHAR_PATTERN
+     * @see #SINGLE_CHAR_PATTERN
+     */
+    public static boolean isSpecificHostPattern(String pattern) {
+        if (GenericUtils.isEmpty(pattern)) {
+            return false;
+        }
+
+        for (int index = 0; index < PATTERN_CHARS.length(); index++) {
+            char ch = PATTERN_CHARS.charAt(index);
+            if (pattern.indexOf(ch) >= 0) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Locates all the matching entries for a give host name / address
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
+     * @return A {@link List} of all the matching entries
+     * @see #isHostMatch(String)
+     */
+    public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry ... entries) {
+        // TODO in Java-8 use Stream(s) + predicate
+        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        } else {
+            return findMatchingEntries(host, Arrays.asList(entries));
+        }
+    }
+
+    /**
+     * Locates all the matching entries for a give host name / address
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
+     * @return A {@link List} of all the matching entries
+     * @see #isHostMatch(String)
+     */
+    public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
+        // TODO in Java-8 use Stream(s) + predicate
+        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        }
+
+        List<HostConfigEntry> matches = null;
+        for (HostConfigEntry entry : entries) {
+            if (!entry.isHostMatch(host)) {
+                continue;   // debug breakpoint
+            }
+
+            if (matches == null) {
+                matches = new ArrayList<>(entries.size());  // in case ALL of them match
+            }
+
+            matches.add(entry);
+        }
+
+        if (matches == null) {
+            return Collections.emptyList();
+        } else {
+            return matches;
+        }
+    }
+
+    public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
+        if (GenericUtils.isEmpty(patterns)) {
+            return false;
+        }
+
+        boolean matchFound = false;
+        for (Pair<Pattern, Boolean> pp : patterns) {
+            Boolean negated = pp.getSecond();
+            /*
+             * If already found a match we are interested only in negations
+             */
+            if (matchFound && (!negated.booleanValue())) {
+                continue;
+            }
+
+            if (!isHostMatch(host, pp.getFirst())) {
+                continue;
+            }
+
+            /*
+             * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
+             *
+             *      If a negated entry is matched, then the Host entry is ignored,
+             *      regardless of whether any other patterns on the line match.
+             */
+            if (negated.booleanValue()) {
+                return false;
+            }
+
+            matchFound = true;
+        }
+
+        return matchFound;
+    }
+
+    /**
+     * Checks if a given host name / address matches a host pattern
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @param pattern The host {@link Pattern} - ignored if {@code null}
+     * @return {@code true} if the name / address matches the pattern
+     */
+    public static boolean isHostMatch(String host, Pattern pattern) {
+        if (GenericUtils.isEmpty(host) || (pattern == null)) {
+            return false;
+        }
+
+        Matcher m = pattern.matcher(host);
+        return m.matches();
+    }
+
+    public static List<Pair<Pattern, Boolean>> parsePatterns(CharSequence ... patterns) {
+        return parsePatterns(GenericUtils.isEmpty(patterns) ? Collections.<CharSequence>emptyList() : Arrays.asList(patterns));
+    }
+
+    public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
+        if (GenericUtils.isEmpty(patterns)) {
+            return Collections.emptyList();
+        }
+
+        List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
+        for (CharSequence p : patterns) {
+            result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
+        }
+
+        return result;
+    }
+
+    /**
+     * Converts a host pattern string to a regular expression matcher.
+     * <B>Note:</B> pattern matching is <U>case insensitive</U>
+     *
+     * @param pattern The original pattern string - ignored if {@code null}/empty
+     * @return The regular expression matcher {@link Pattern} and the indication
+     * whether it is a negating pattern or not - {@code null} if no original string
+     * @see #WILDCARD_PATTERN
+     * @see #SINGLE_CHAR_PATTERN
+     * @see #NEGATION_CHAR_PATTERN
+     */
+    public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
+        if (GenericUtils.isEmpty(pattern)) {
+            return null;
+        }
+
+        StringBuilder sb = new StringBuilder(pattern.length());
+        boolean negated = false;
+        for (int curPos = 0; curPos < pattern.length(); curPos++) {
+            char ch = pattern.charAt(curPos);
+            ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
+
+            switch(ch) {
+                case '.':   // need to escape it
+                    sb.append('\\').append(ch);
+                    break;
+                case SINGLE_CHAR_PATTERN:
+                    sb.append('.');
+                    break;
+                case WILDCARD_PATTERN:
+                    sb.append(".*");
+                    break;
+                case NEGATION_CHAR_PATTERN:
+                    ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
+                    ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
+                    negated = true;
+                    break;
+                default:
+                    sb.append(ch);
+            }
+        }
+
+        return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
+    }
+
+    /**
+     * Checks if the given character is valid for a host pattern. Valid
+     * characters are:
+     * <UL>
+     *      <LI>A-Z</LI>
+     *      <LI>a-z</LI>
+     *      <LI>0-9</LI>
+     *      <LI>Underscore (_)</LI>
+     *      <LI>Hyphen (-)</LI>
+     *      <LI>Dot (.)</LI>
+     *      <LI>The {@link #WILDCARD_PATTERN}</LI>
+     *      <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
+     * </UL>
+     *
+     * @param ch The character to validate
+     * @return {@code true} if valid pattern character
+     */
+    public static boolean isValidPatternChar(char ch) {
+        if ((ch <= ' ') || (ch >= 0x7E)) {
+            return false;
+        }
+        if ((ch >= 'a') && (ch <= 'z')) {
+            return true;
+        }
+        if ((ch >= 'A') && (ch <= 'Z')) {
+            return true;
+        }
+        if ((ch >= '0') && (ch <= '9')) {
+            return true;
+        }
+        if ("-_.".indexOf(ch) >= 0) {
+            return true;
+        }
+        if (PATTERN_CHARS.indexOf(ch) >= 0) {
+            return true;
+        }
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
new file mode 100644
index 0000000..3005f47
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.mac.BuiltinMacs;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Available digesters for known hosts entries
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum KnownHostDigest implements NamedFactory<Mac> {
+    SHA1("1", BuiltinMacs.hmacsha1);
+
+    public static final Set<KnownHostDigest> VALUES =
+            Collections.unmodifiableSet(EnumSet.allOf(KnownHostDigest.class));
+
+    private final String name;
+    private final Factory<Mac> factory;
+
+    KnownHostDigest(String name, Factory<Mac> factory) {
+        this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No name");
+        this.factory = ValidateUtils.checkNotNull(factory, "No factory");
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public Mac create() {
+        return factory.create();
+    }
+
+    public static KnownHostDigest fromName(String name) {
+        return NamedResource.Utils.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
new file mode 100644
index 0000000..da77bac
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StreamCorruptedException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+
+/**
+ * Contains a representation of an entry in the <code>known_hosts</code> file
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://www.manpagez.com/man/8/sshd/">sshd(8) man page</A>
+ */
+public class KnownHostEntry extends HostPatternsHolder {
+    /**
+     * Character that denotes that start of a marker
+     */
+    public static final char MARKER_INDICATOR = '@';
+
+    /**
+     * Standard OpenSSH config file name
+     */
+    public static final String STD_HOSTS_FILENAME = "known_hosts";
+
+    private static final class LazyDefaultConfigFileHolder {
+        private static final Path HOSTS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_HOSTS_FILENAME);
+    }
+
+    private String line;
+    private String marker;
+    private AuthorizedKeyEntry keyEntry;
+    private KnownHostHashValue hashedEntry;
+
+    public KnownHostEntry() {
+        super();
+    }
+
+    /**
+     * @param line The original line from which this entry was created
+     */
+    public KnownHostEntry(String line) {
+        this.line = line;
+    }
+
+    /**
+     * @return The original line from which this entry was created
+     */
+    public String getConfigLine() {
+        return line;
+    }
+
+    public void setConfigLine(String line) {
+        this.line = line;
+    }
+
+    public String getMarker() {
+        return marker;
+    }
+
+    public void setMarker(String marker) {
+        this.marker = marker;
+    }
+
+    public AuthorizedKeyEntry getKeyEntry() {
+        return keyEntry;
+    }
+
+    public void setKeyEntry(AuthorizedKeyEntry keyEntry) {
+        this.keyEntry = keyEntry;
+    }
+
+    public KnownHostHashValue getHashedEntry() {
+        return hashedEntry;
+    }
+
+    public void setHashedEntry(KnownHostHashValue hashedEntry) {
+        this.hashedEntry = hashedEntry;
+    }
+
+    @Override
+    public boolean isHostMatch(String host) {
+        if (super.isHostMatch(host)) {
+            return true;
+        }
+
+        KnownHostHashValue hash = getHashedEntry();
+        return (hash != null) && hash.isHostMatch(host);
+    }
+
+    @Override
+    public String toString() {
+        return getConfigLine();
+    }
+
+    /**
+     * @return The default {@link Path} location of the OpenSSH known hosts file
+     */
+    @SuppressWarnings("synthetic-access")
+    public static Path getDefaultKnownHostsFile() {
+        return LazyDefaultConfigFileHolder.HOSTS_FILE;
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(File file) throws IOException {
+        return readKnownHostEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(Path path, OpenOption... options) throws IOException {
+        try (InputStream input = Files.newInputStream(path, options)) {
+            return readKnownHostEntries(input, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(URL url) throws IOException {
+        try (InputStream input = url.openStream()) {
+            return readKnownHostEntries(input, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(String filePath) throws IOException {
+        try (InputStream inStream = new FileInputStream(filePath)) {
+            return readKnownHostEntries(inStream, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(InputStream inStream, boolean okToClose) throws IOException {
+        try (Reader reader = new InputStreamReader(NoCloseInputStream.resolveInputStream(inStream, okToClose), StandardCharsets.UTF_8)) {
+            return readKnownHostEntries(reader, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(Reader rdr, boolean okToClose) throws IOException {
+        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+            return readKnownHostEntries(buf);
+        }
+    }
+
+    /**
+     * Reads configuration entries
+     *
+     * @param rdr The {@link BufferedReader} to use
+     * @return The {@link List} of read {@link KnownHostEntry}-ies
+     * @throws IOException If failed to parse the read configuration
+     */
+    public static List<KnownHostEntry> readKnownHostEntries(BufferedReader rdr) throws IOException {
+        List<KnownHostEntry> entries = null;
+
+        int lineNumber = 1;
+        for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) {
+            line = GenericUtils.trimToEmpty(line);
+            if (GenericUtils.isEmpty(line)) {
+                continue;
+            }
+
+            int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
+            if (pos == 0) {
+                continue;
+            }
+
+            if (pos > 0) {
+                line = line.substring(0, pos);
+                line = line.trim();
+            }
+
+            try {
+                KnownHostEntry entry = parseKnownHostEntry(line);
+                if (entry == null) {
+                    continue;
+                }
+
+                if (entries == null) {
+                    entries = new ArrayList<>();
+                }
+                entries.add(entry);
+            } catch (RuntimeException | Error e) {   // TODO consider consulting a user callback
+                throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+                        + " to parse line #" + lineNumber + " '" + line + "': " + e.getMessage());
+            }
+        }
+
+        if (entries == null) {
+            return Collections.emptyList();
+        } else {
+            return entries;
+        }
+    }
+
+    public static KnownHostEntry parseKnownHostEntry(String line) {
+        return parseKnownHostEntry(GenericUtils.isEmpty(line) ? null : new KnownHostEntry(), line);
+    }
+
+    public static <E extends KnownHostEntry> E parseKnownHostEntry(E entry, String data) {
+        String line = data;
+        if (GenericUtils.isEmpty(line) || (line.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
+            return entry;
+        }
+
+        entry.setConfigLine(line);
+
+        if (line.charAt(0) == MARKER_INDICATOR) {
+            int pos = line.indexOf(' ');
+            ValidateUtils.checkTrue(pos > 0, "Missing marker name end delimiter in line=%s", data);
+            ValidateUtils.checkTrue(pos > 1, "No marker name after indicator in line=%s", data);
+            entry.setMarker(line.substring(1, pos));
+            line = line.substring(pos + 1).trim();
+        } else {
+            entry.setMarker(null);
+        }
+
+        int pos = line.indexOf(' ');
+        ValidateUtils.checkTrue(pos > 0, "Missing host patterns end delimiter in line=%s", data);
+        String hostPattern = line.substring(0, pos);
+        line = line.substring(pos + 1).trim();
+
+        if (hostPattern.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) {
+            KnownHostHashValue hash =
+                    ValidateUtils.checkNotNull(KnownHostHashValue.parse(hostPattern),
+                            "Failed to extract host hash value from line=%s", data);
+            entry.setHashedEntry(hash);
+            entry.setPatterns(null);
+        } else {
+            entry.setHashedEntry(null);
+            entry.setPatterns(parsePatterns(GenericUtils.split(hostPattern, ',')));
+        }
+
+        AuthorizedKeyEntry key =
+                ValidateUtils.checkNotNull(AuthorizedKeyEntry.parseAuthorizedKeyEntry(line),
+                        "No valid key entry recovered from line=%s", data);
+        entry.setKeyEntry(key);
+        return entry;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
new file mode 100644
index 0000000..f5fa825
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.RuntimeSshException;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.Base64;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class KnownHostHashValue {
+    /**
+     * Character used to indicate a hashed host pattern
+     */
+    public static final char HASHED_HOST_DELIMITER = '|';
+
+    public static final NamedFactory<Mac> DEFAULT_DIGEST = KnownHostDigest.SHA1;
+
+    private NamedFactory<Mac> digester = DEFAULT_DIGEST;
+    private byte[] saltValue;
+    private byte[] digestValue;
+
+    public KnownHostHashValue() {
+        super();
+    }
+
+    public NamedFactory<Mac> getDigester() {
+        return digester;
+    }
+
+    public void setDigester(NamedFactory<Mac> digester) {
+        this.digester = digester;
+    }
+
+    public byte[] getSaltValue() {
+        return saltValue;
+    }
+
+    public void setSaltValue(byte[] saltValue) {
+        this.saltValue = saltValue;
+    }
+
+    public byte[] getDigestValue() {
+        return digestValue;
+    }
+
+    public void setDigestValue(byte[] digestValue) {
+        this.digestValue = digestValue;
+    }
+
+    /**
+     * Checks if the host matches the hash
+     *
+     * @param host The host name/address - ignored if {@code null}/empty
+     * @return {@code true} if host matches the hash
+     * @throws RuntimeException If entry not properly initialized
+     */
+    public boolean isHostMatch(String host) {
+        if (GenericUtils.isEmpty(host)) {
+            return false;
+        }
+
+        try {
+            byte[] expected = getDigestValue();
+            byte[] actual = calculateHashValue(host, getDigester(), getSaltValue());
+            return Arrays.equals(expected, actual);
+        } catch (Throwable t) {
+            if (t instanceof RuntimeException) {
+                throw (RuntimeException) t;
+            }
+            throw new RuntimeSshException("Failed (" + t.getClass().getSimpleName() + ")"
+                    + " to calculate hash value: " + t.getMessage(), t);
+        }
+    }
+
+    @Override
+    public String toString() {
+        if ((getDigester() == null) || NumberUtils.isEmpty(getSaltValue()) || NumberUtils.isEmpty(getDigestValue())) {
+            return Objects.toString(getDigester(), null)
+                 + "-" + BufferUtils.toHex(':', getSaltValue())
+                 + "-" + BufferUtils.toHex(':', getDigestValue());
+        }
+
+        try {
+            return append(new StringBuilder(Byte.MAX_VALUE), this).toString();
+        } catch (IOException | RuntimeException e) {    // unexpected
+            return e.getClass().getSimpleName() + ": " + e.getMessage();
+        }
+    }
+
+    // see http://nms.lcs.mit.edu/projects/ssh/README.hashed-hosts
+    public static byte[] calculateHashValue(String host, Factory<? extends Mac> factory, byte[] salt) throws Exception {
+        return calculateHashValue(host, factory.create(), salt);
+    }
+
+    public static byte[] calculateHashValue(String host, Mac mac, byte[] salt) throws Exception {
+        mac.init(salt);
+
+        byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8);
+        mac.update(hostBytes);
+        return mac.doFinal();
+    }
+
+    public static <A extends Appendable> A append(A sb, KnownHostHashValue hashValue) throws IOException {
+        return (hashValue == null) ? sb : append(sb, hashValue.getDigester(), hashValue.getSaltValue(), hashValue.getDigestValue());
+    }
+
+    public static <A extends Appendable> A append(A sb, NamedResource factory, byte[] salt, byte[] digest) throws IOException {
+        sb.append(HASHED_HOST_DELIMITER).append(factory.getName());
+        sb.append(HASHED_HOST_DELIMITER).append(Base64.encodeToString(salt));
+        sb.append(HASHED_HOST_DELIMITER).append(Base64.encodeToString(digest));
+        return sb;
+    }
+
+    public static KnownHostHashValue parse(String pattern) {
+        return parse(pattern, GenericUtils.isEmpty(pattern) ? null : new KnownHostHashValue());
+    }
+
+    public static <V extends KnownHostHashValue> V parse(String pattern, V value) {
+        if (GenericUtils.isEmpty(pattern)) {
+            return value;
+        }
+
+        String[] components = GenericUtils.split(pattern, HASHED_HOST_DELIMITER);
+        ValidateUtils.checkTrue(components.length == 4 /* 1st one is empty */, "Invalid hash pattern (insufficient data): %s", pattern);
+        ValidateUtils.checkTrue(GenericUtils.isEmpty(components[0]), "Invalid hash pattern (unexpected extra data): %s", pattern);
+
+        NamedFactory<Mac> factory =
+                ValidateUtils.checkNotNull(KnownHostDigest.fromName(components[1]),
+                        "Invalid hash pattern (unknwon digest): %s", pattern);
+        value.setDigester(factory);
+        value.setSaltValue(Base64.decodeString(components[2]));
+        value.setDigestValue(Base64.decodeString(components[3]));
+        return value;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
new file mode 100644
index 0000000..f10645b
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.keyverifier;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Monitors the {@code ~/.ssh/known_hosts} file of the user currently running
+ * the client, updating and re-loading it if necessary. It also (optionally)
+ * enforces the same permissions regime as {@code OpenSSH}.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultKnownHostsServerKeyVerifier extends KnownHostsServerKeyVerifier {
+    private final boolean strict;
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate) {
+        this(delegate, true);
+    }
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict) {
+        this(delegate, strict, KnownHostEntry.getDefaultKnownHostsFile(), IoUtils.getLinkOptions(false));
+    }
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict, File file) {
+        this(delegate, strict, ValidateUtils.checkNotNull(file, "No file provided").toPath(), IoUtils.getLinkOptions(false));
+    }
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict, Path file, LinkOption... options) {
+        super(delegate, file, options);
+        this.strict = strict;
+    }
+
+    /**
+     * @return If {@code true} then makes sure that the containing folder
+     * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
+     * does not check these permissions
+     * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
+     */
+    public final boolean isStrict() {
+        return strict;
+    }
+
+    @Override
+    protected List<HostEntryPair> reloadKnownHosts(Path file) throws IOException, GeneralSecurityException {
+        if (isStrict()) {
+            if (log.isDebugEnabled()) {
+                log.debug("reloadKnownHosts({}) check permissions", file);
+            }
+
+            Pair<String, Object> violation = validateStrictConfigFilePermissions(file);
+            if (violation != null) {
+                log.warn("reloadKnownHosts({}) invalid file permissions: {}", file, violation.getFirst());
+                updateReloadAttributes();
+                return Collections.emptyList();
+            }
+        }
+
+        return super.reloadKnownHosts(file);
+    }
+}


[2/3] mina-sshd git commit: [SSHD-266] Support for known hosts

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
new file mode 100644
index 0000000..6961f901
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
@@ -0,0 +1,720 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.keyverifier;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.client.config.hosts.KnownHostHashValue;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class KnownHostsServerKeyVerifier extends ModifiableFileWatcher implements ServerKeyVerifier {
+    /**
+     * Represents an entry in the internal verifier's cach
+     * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+     */
+    public static class HostEntryPair {
+        private KnownHostEntry hostEntry;
+        private PublicKey serverKey;
+
+        public HostEntryPair() {
+            super();
+        }
+
+        public HostEntryPair(KnownHostEntry entry, PublicKey key) {
+            this.hostEntry = ValidateUtils.checkNotNull(entry, "No entry");
+            this.serverKey = ValidateUtils.checkNotNull(key, "No key");
+        }
+
+        public KnownHostEntry getHostEntry() {
+            return hostEntry;
+        }
+
+        public void setHostEntry(KnownHostEntry hostEntry) {
+            this.hostEntry = hostEntry;
+        }
+
+        public PublicKey getServerKey() {
+            return serverKey;
+        }
+
+        public void setServerKey(PublicKey serverKey) {
+            this.serverKey = serverKey;
+        }
+
+        @Override
+        public String toString() {
+            return String.valueOf(getHostEntry());
+        }
+    }
+
+    protected final Object updateLock = new Object();
+    private final ServerKeyVerifier delegate;
+    private final AtomicReference<Collection<HostEntryPair>> keysHolder =
+            new AtomicReference<Collection<HostEntryPair>>(Collections.<HostEntryPair>emptyList());
+
+    public KnownHostsServerKeyVerifier(ServerKeyVerifier delegate, Path file) {
+        this(delegate, file, IoUtils.EMPTY_LINK_OPTIONS);
+    }
+
+    public KnownHostsServerKeyVerifier(ServerKeyVerifier delegate, Path file, LinkOption... options) {
+        super(file, options);
+        this.delegate = ValidateUtils.checkNotNull(delegate, "No delegate");
+    }
+
+    public ServerKeyVerifier getDelegateVerifier() {
+        return delegate;
+    }
+
+    @Override
+    public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+        Collection<HostEntryPair> knownHosts = getLoadedHostsEntries();
+        try {
+            if (checkReloadRequired()) {
+                Path file = getPath();
+                if (exists()) {
+                    knownHosts = reloadKnownHosts(file);
+                } else {
+                    if (log.isDebugEnabled()) {
+                        log.debug("verifyServerKey({})[{}] missing known hosts file {}",
+                                  clientSession, remoteAddress, file);
+                    }
+                    knownHosts = Collections.<HostEntryPair>emptyList();
+                }
+
+                setLoadedHostsEntries(knownHosts);
+            }
+        } catch (Throwable t) {
+            return acceptIncompleteHostKeys(clientSession, remoteAddress, serverKey, t);
+        }
+
+        return acceptKnownHostEntries(clientSession, remoteAddress, serverKey, knownHosts);
+    }
+
+    protected Collection<HostEntryPair> getLoadedHostsEntries() {
+        return keysHolder.get();
+    }
+
+    protected void setLoadedHostsEntries(Collection<HostEntryPair> keys) {
+        keysHolder.set(keys);
+    }
+
+    /**
+     * @param file The {@link Path} to reload from
+     * @return A {@link List} of the loaded {@link HostEntryPair}s - may be {@code null}/empty
+     * @throws IOException If failed to parse the file
+     * @throws GeneralSecurityException If failed to resolve the encoded public keys
+     */
+    protected List<HostEntryPair> reloadKnownHosts(Path file) throws IOException, GeneralSecurityException {
+        Collection<KnownHostEntry> entries = KnownHostEntry.readKnownHostEntries(file);
+        if (log.isDebugEnabled()) {
+            log.debug("reloadKnownHosts({}) loaded {} entries", file, entries.size());
+        }
+        updateReloadAttributes();
+
+        if (GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        }
+
+        List<HostEntryPair> keys = new ArrayList<>(entries.size());
+        PublicKeyEntryResolver resolver = getFallbackPublicKeyEntryResolver();
+        for (KnownHostEntry entry : entries) {
+            try {
+                PublicKey key = resolveHostKey(entry, resolver);
+                if (key != null) {
+                    keys.add(new HostEntryPair(entry, key));
+                }
+            } catch (Throwable t) {
+                log.warn("reloadKnownHosts({}) failed ({}) to load key of {}: {}",
+                         file, t.getClass().getSimpleName(), entry, t.getMessage());
+                if (log.isDebugEnabled()) {
+                    log.debug("reloadKnownHosts(" + file + ") key=" + entry + " load failure details", t);
+                }
+            }
+        }
+
+        return keys;
+    }
+
+    /**
+     * Recover the associated public key from a known host entry
+     *
+     * @param entry The {@link KnownHostEntry} - ignored if {@code null}
+     * @param resolver The {@link PublicKeyEntryResolver} to use if immediate
+     * - decoding does not work - ignored if {@code null}
+     * @return The extracted {@link PublicKey} - {@code null} if none
+     * @throws IOException If failed to decode the key
+     * @throws GeneralSecurityException If failed to generate the key
+     * @see #getFallbackPublicKeyEntryResolver()
+     * @see AuthorizedKeyEntry#resolvePublicKey(PublicKeyEntryResolver)
+     */
+    protected PublicKey resolveHostKey(KnownHostEntry entry, PublicKeyEntryResolver resolver)
+            throws IOException, GeneralSecurityException {
+        if (entry == null) {
+            return null;
+        }
+
+        AuthorizedKeyEntry authEntry = ValidateUtils.checkNotNull(entry.getKeyEntry(), "No key extracted from %s", entry);
+        PublicKey key = authEntry.resolvePublicKey(resolver);
+        if (log.isDebugEnabled()) {
+            log.debug("resolveHostKey({}) loaded {}-{}", entry, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+        }
+
+        return key;
+    }
+
+    protected PublicKeyEntryResolver getFallbackPublicKeyEntryResolver() {
+        return PublicKeyEntryResolver.IGNORING;
+    }
+
+    protected boolean acceptKnownHostEntries(
+            ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, Collection<HostEntryPair> knownHosts) {
+        // TODO allow for several candidates and check if ANY of them matches the key and has 'revoked' marker
+        HostEntryPair match = findKnownHostEntry(clientSession, remoteAddress, knownHosts);
+        if (match == null) {
+            return acceptUnknownHostKey(clientSession, remoteAddress, serverKey);
+        }
+
+        KnownHostEntry entry = match.getHostEntry();
+        PublicKey expected = match.getServerKey();
+        if (KeyUtils.compareKeys(expected, serverKey)) {
+            return acceptKnownHostEntry(clientSession, remoteAddress, serverKey, entry);
+        }
+
+        if (!acceptModifiedServerKey(clientSession, remoteAddress, match, serverKey)) {
+            return false;
+        }
+
+        Path file = getPath();
+        try {
+            updateModifiedServerKey(clientSession, remoteAddress, match, serverKey, file, knownHosts);
+        } catch (Throwable t) {
+            handleModifiedServerKeyUpdateFailure(clientSession, remoteAddress, match, serverKey, file, knownHosts, t);
+        }
+
+        return true;
+    }
+
+    /**
+     * Invoked if a matching host entry was found, but the key did not match and
+     * {@link #acceptModifiedServerKey(ClientSession, SocketAddress, HostEntryPair, PublicKey)}
+     * returned {@code true}. By default it locates the line to be updated and updates only
+     * its key data, marking the file for reload on next verification just to be
+     * on the safe side.
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param match The {@link HostEntryPair} whose key does not match
+     * @param actual The presented server {@link PublicKey} to be updated
+     * @param file The file {@link Path} to be updated
+     * @param knownHosts The currently loaded entries
+     * @throws Exception If failed to update the file - <B>Note:</B> this may mean the
+     * file is now corrupted
+     * @see #handleModifiedServerKeyUpdateFailure(ClientSession, SocketAddress, HostEntryPair, PublicKey, Path, Collection, Throwable)
+     * @see #prepareModifiedServerKeyLine(ClientSession, SocketAddress, KnownHostEntry, String, PublicKey, PublicKey)
+     */
+    protected void updateModifiedServerKey(
+            ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual,
+            Path file, Collection<HostEntryPair> knownHosts)
+                    throws Exception {
+        KnownHostEntry entry = match.getHostEntry();
+        String matchLine = ValidateUtils.checkNotNullAndNotEmpty(entry.getConfigLine(), "No entry config line");
+        String newLine = prepareModifiedServerKeyLine(clientSession, remoteAddress, entry, matchLine, match.getServerKey(), actual);
+        if (GenericUtils.isEmpty(newLine)) {
+            if (log.isDebugEnabled()) {
+                log.debug("updateModifiedServerKey({})[{}] no replacement generated for {}",
+                          clientSession, remoteAddress, matchLine);
+            }
+            return;
+        }
+
+        if (matchLine.equals(newLine)) {
+            if (log.isDebugEnabled()) {
+                log.debug("updateModifiedServerKey({})[{}] unmodified upodated lline for {}",
+                          clientSession, remoteAddress, matchLine);
+            }
+            return;
+        }
+
+        List<String> lines = new ArrayList<>();
+        synchronized (updateLock) {
+            int matchingIndex = -1; // read all lines but replace the
+            try (BufferedReader rdr = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
+                for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
+                    // skip if already replaced the original line
+                    if (matchingIndex >= 0) {
+                        lines.add(line);
+                        continue;
+                    }
+                    line = GenericUtils.trimToEmpty(line);
+                    if (GenericUtils.isEmpty(line)) {
+                        lines.add(line);
+                        continue;
+                    }
+
+                    int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
+                    if (pos == 0) {
+                        lines.add(line);
+                        continue;
+                    }
+
+                    if (pos > 0) {
+                        line = line.substring(0, pos);
+                        line = line.trim();
+                    }
+
+                    if (!matchLine.equals(line)) {
+                        lines.add(line);
+                        continue;
+                    }
+
+                    lines.add(newLine);
+                    matchingIndex = lines.size();
+                }
+            }
+
+            ValidateUtils.checkTrue(matchingIndex >= 0, "No match found for line=%s", matchLine);
+
+            try (Writer w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
+                for (String l : lines) {
+                    w.append(l).append(IoUtils.EOL);
+                }
+            }
+
+            synchronized (match) {
+                match.setServerKey(actual);
+                entry.setConfigLine(newLine);
+            }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("updateModifiedServerKey({}) replaced '{}' with '{}'", file, matchLine, newLine);
+        }
+        resetReloadAttributes();    // force reload on next verification
+    }
+
+    /**
+     * Invoked by {@link #updateModifiedServerKey(ClientSession, SocketAddress, HostEntryPair, PublicKey, Path, Collection)}
+     * in order to prepare the replacement - by default it replaces the key part with the new one
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param entry The {@link KnownHostEntry}
+     * @param curLine The current entry line data
+     * @param expected The expected {@link PublicKey}
+     * @param actual The present key to be update
+     * @return The updated line - ignored if {@code null}/empty or same as original one
+     * @throws Exception if failed to prepare the line
+     */
+    protected String prepareModifiedServerKeyLine(
+            ClientSession clientSession, SocketAddress remoteAddress, KnownHostEntry entry,
+            String curLine, PublicKey expected, PublicKey actual)
+                throws Exception {
+        if ((entry == null) || GenericUtils.isEmpty(curLine)) {
+            return curLine; // just to be on the safe side
+        }
+
+        int pos = curLine.indexOf(' ');
+        if (curLine.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
+            // skip marker till next token
+            for (pos++; pos < curLine.length(); pos++) {
+                if (curLine.charAt(pos) != ' ') {
+                    break;
+                }
+            }
+
+            pos = (pos < curLine.length()) ? curLine.indexOf(' ', pos) : -1;
+        }
+
+        ValidateUtils.checkTrue((pos > 0) && (pos < (curLine.length() - 1)), "Missing encoded key in line=%s", curLine);
+        StringBuilder sb = new StringBuilder(curLine.length());
+        sb.append(curLine.substring(0, pos));   // copy the marker/patterns as-is
+        PublicKeyEntry.appendPublicKeyEntry(sb.append(' '), actual);
+        return sb.toString();
+    }
+
+    /**
+     * Invoked if {@code #updateModifiedServerKey(ClientSession, SocketAddress, HostEntryPair, PublicKey, Path)}
+     * throws an exception. This may mean the file is corrupted, but it can be recovered from the known hosts
+     * that are being provided. By default, it only logs a warning and does not attempt to recover the file
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param match The {@link HostEntryPair} whose key does not match
+     * @param serverKey The presented server {@link PublicKey} to be updated
+     * @param file The file {@link Path} to be updated
+     * @param knownHosts The currently cached entries (may be {@code null}/empty)
+     * @param reason The failure reason
+     */
+    protected void handleModifiedServerKeyUpdateFailure(
+            ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match,
+            PublicKey serverKey, Path file, Collection<HostEntryPair> knownHosts, Throwable reason) {
+        // NOTE !!! this may mean the file is corrupted, but it can be recovered from the known hosts
+        log.warn("acceptKnownHostEntries({})[{}] failed ({}) to update modified server key of {}: {}",
+                 clientSession, remoteAddress, reason.getClass().getSimpleName(), match, reason.getMessage());
+        if (log.isDebugEnabled()) {
+            log.debug("acceptKnownHostEntries(" + clientSession + ")[" + remoteAddress + "]"
+                    + " modified key update failure details", reason);
+        }
+    }
+
+    /**
+     * Invoked <U>after</U> known host entry located and keys match - by default
+     * checks that entry has not been revoked
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param serverKey The presented server {@link PublicKey}
+     * @param entry The {@link KnownHostEntry} value - if {@code null} then no
+     * known matching host entry was found - default will call
+     * {@link #acceptUnknownHostKey(ClientSession, SocketAddress, PublicKey)}
+     * @return {@code true} if OK to accept the server
+     */
+    protected boolean acceptKnownHostEntry(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, KnownHostEntry entry) {
+        if (entry == null) {    // not really expected, but manage it
+            return acceptUnknownHostKey(clientSession, remoteAddress, serverKey);
+        }
+
+        if ("revoked".equals(entry.getMarker())) {
+            log.debug("acceptKnownHostEntry({})[{}] key={}-{} marked as {}",
+                      clientSession, remoteAddress, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey), entry.getMarker());
+            return false;
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("acceptKnownHostEntry({})[{}] matched key={}-{}",
+                      clientSession, remoteAddress, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey));
+        }
+        return true;
+    }
+
+    protected HostEntryPair findKnownHostEntry(
+            ClientSession clientSession, SocketAddress remoteAddress, Collection<HostEntryPair> knownHosts) {
+        if (GenericUtils.isEmpty(knownHosts)) {
+            return null;
+        }
+
+        Collection<String> candidates = resolveHostNetworkIdentities(clientSession, remoteAddress);
+        if (log.isDebugEnabled()) {
+            log.debug("findKnownHostEntry({})[{}] host network identities: {}",
+                      clientSession, remoteAddress, candidates);
+        }
+
+        if (GenericUtils.isEmpty(candidates)) {
+            return null;
+        }
+
+        for (HostEntryPair match : knownHosts) {
+            KnownHostEntry entry = match.getHostEntry();
+            for (String host : candidates) {
+                try {
+                    if (entry.isHostMatch(host)) {
+                        if (log.isDebugEnabled()) {
+                            log.debug("findKnownHostEntry({})[{}] matched host={} for entry={}",
+                                       clientSession, remoteAddress, host, entry);
+                        }
+                        return match;
+                    }
+                } catch (RuntimeException | Error e) {
+                    log.warn("findKnownHostEntry({})[{}] failed ({}) to check host={} for entry={}: {}",
+                             clientSession, remoteAddress, e.getClass().getSimpleName(),
+                             host, entry.getConfigLine(), e.getMessage());
+                    if (log.isDebugEnabled()) {
+                        log.debug("findKnownHostEntry(" + clientSession + ") host=" + host + ", entry=" + entry + " match failure details", e);
+                    }
+                }
+            }
+        }
+
+        return null;    // no match found
+    }
+
+    /**
+     * Called if failed to reload known hosts - by default invokes
+     * {@link #acceptUnknownHostKey(ClientSession, SocketAddress, PublicKey)}
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param serverKey The presented server {@link PublicKey}
+     * @param reason The {@link Throwable} that indicates the reload failure
+     * @return {@code true} if accept the server key anyway
+     * @see #acceptUnknownHostKey(ClientSession, SocketAddress, PublicKey)
+     */
+    protected boolean acceptIncompleteHostKeys(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, Throwable reason) {
+        log.warn("Failed ({}) to reload server keys from {}: {}",
+                reason.getClass().getSimpleName(), getPath(), reason.getMessage());
+        if (log.isDebugEnabled()) {
+            log.debug(getPath() + " reload failure details", reason);
+        }
+        return acceptUnknownHostKey(clientSession, remoteAddress, serverKey);
+    }
+
+    /**
+     * Invoked if none of the known hosts matches the current one - by default invokes the delegate.
+     * If the delegate accepts the key, then it is <U>appended</U> to the currently monitored entries
+     * and the file is updated
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param serverKey The presented server {@link PublicKey}
+     * @return {@code true} if accept the server key
+     * @see #updateKnownHostsFile(ClientSession, SocketAddress, PublicKey, Path, Collection)
+     * @see #handleKnownHostsFileUpdateFailure(ClientSession, SocketAddress, PublicKey, Path, Collection, Throwable)
+     */
+    protected boolean acceptUnknownHostKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+        if (log.isDebugEnabled()) {
+            log.debug("acceptUnknownHostKey({}) host={}, key={}",
+                      clientSession, remoteAddress, KeyUtils.getFingerPrint(serverKey));
+        }
+
+        if (delegate.verifyServerKey(clientSession, remoteAddress, serverKey)) {
+            Path file = getPath();
+            Collection<HostEntryPair> keys = getLoadedHostsEntries();
+            try {
+                updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, keys);
+            } catch (Throwable t) {
+                handleKnownHostsFileUpdateFailure(clientSession, remoteAddress, serverKey, file, keys, t);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Invoked when {@link #updateKnownHostsFile(ClientSession, SocketAddress, PublicKey, Path, Collection)} fails - by
+     * default just issues a warning. <B>Note:</B> there is a chance that the file is now corrupted and
+     * cannot be re-used, so we provide a way to recover it via overriding this method and using the cached
+     * entries to re-created it.
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param serverKey The server {@link PublicKey} that was attempted to update
+     * @param file The file {@link Path} to be updated
+     * @param knownHosts The currently known entries (may be {@code null}/empty
+     * @param reason The failure reason
+     */
+    protected void handleKnownHostsFileUpdateFailure(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+            Path file, Collection<HostEntryPair> knownHosts, Throwable reason) {
+        log.warn("handleKnownHostsFileUpdateFailure({})[{}] failed ({}) to update key={}-{} in {}: {}",
+                 clientSession, remoteAddress, reason.getClass().getSimpleName(),
+                 KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey),
+                 file, reason.getMessage());
+        if (log.isDebugEnabled()) {
+            log.debug("handleKnownHostsFileUpdateFailure(" + clientSession + ")[" + remoteAddress + "]"
+                    + " file update failure details", reason);
+        }
+    }
+
+    /**
+     * Invoked if a new previously unknown host key has been accepted - by default
+     * appends a new entry at the end of the currently monitored known hosts file
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param serverKey The server {@link PublicKey} that to update
+     * @param file The file {@link Path} to be updated
+     * @param knownHosts The currently cached entries (may be {@code null}/empty)
+     * @return The generated {@link KnownHostEntry} or {@code null} if nothing updated.
+     * If anything updated then the file will be re-loaded on next verification
+     * regardless of which server is verified
+     * @throws Exception If failed to update the file - <B>Note:</B> in this case
+     * the file may be corrupted so {@link #handleKnownHostsFileUpdateFailure(ClientSession, SocketAddress, PublicKey, Path, Collection, Throwable)}
+     * will be called in order to enable recovery of its data
+     * @see #resetReloadAttributes()
+     */
+    protected KnownHostEntry updateKnownHostsFile(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+            Path file, Collection<HostEntryPair> knownHosts) throws Exception {
+        KnownHostEntry entry = prepareKnownHostEntry(clientSession, remoteAddress, serverKey);
+        if (entry == null) {
+            if (log.isDebugEnabled()) {
+                log.debug("updateKnownHostsFile({})[{}] no entry generated for key={}",
+                          clientSession, remoteAddress, KeyUtils.getFingerPrint(serverKey));
+            }
+
+            return null;
+        }
+
+        String line = entry.getConfigLine();
+        byte[] lineData = line.getBytes(StandardCharsets.UTF_8);
+        boolean reuseExisting = Files.exists(file) && (Files.size(file) > 0);
+        synchronized (updateLock) {
+            try (OutputStream output = reuseExisting ? Files.newOutputStream(file, StandardOpenOption.APPEND) : Files.newOutputStream(file)) {
+                if (reuseExisting) {
+                    output.write(IoUtils.getEOLBytes());    // separate from previous lines
+                }
+                output.write(lineData);
+            }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("updateKnownHostsFile({}) updated: {}", file, entry);
+        }
+        resetReloadAttributes(); // force reload on next verification
+        return entry;
+    }
+
+    /**
+     * Invoked by {@link #updateKnownHostsFile(ClientSession, SocketAddress, PublicKey, Path, Collection)}
+     * in order to generate the host entry to be written
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param serverKey The server {@link PublicKey} that was attempted to update
+     * @return The {@link KnownHostEntry} to use - if {@code null} then entry is
+     * not updated in the file
+     * @throws Exception If failed to generate the entry - e.g. failed to hash
+     * @see #resolveHostNetworkIdentities(ClientSession, SocketAddress)
+     * @see KnownHostEntry#getConfigLine()
+     */
+    protected KnownHostEntry prepareKnownHostEntry(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) throws Exception {
+        Collection<String> patterns = resolveHostNetworkIdentities(clientSession, remoteAddress);
+        if (GenericUtils.isEmpty(patterns)) {
+            return null;
+        }
+
+        StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+        Random rnd = null;
+        for (String hostIdentity : patterns) {
+            if (sb.length() > 0) {
+                sb.append(',');
+            }
+
+            NamedFactory<Mac> digester = getHostValueDigester(clientSession, remoteAddress, hostIdentity);
+            if (digester != null) {
+                if (rnd == null) {
+                    FactoryManager manager =
+                            ValidateUtils.checkNotNull(clientSession.getFactoryManager(), "No factory manager");
+                    Factory<? extends Random> factory =
+                            ValidateUtils.checkNotNull(manager.getRandomFactory(), "No random factory");
+                    rnd = ValidateUtils.checkNotNull(factory.create(), "No randomizer created");
+                }
+
+                Mac mac = digester.create();
+                int blockSize = mac.getDefaultBlockSize();
+                byte[] salt = new byte[blockSize];
+                rnd.fill(salt);
+
+                byte[] digestValue = KnownHostHashValue.calculateHashValue(hostIdentity, mac, salt);
+                KnownHostHashValue.append(sb, digester, salt, digestValue);
+            } else {
+                sb.append(hostIdentity);
+            }
+        }
+
+        PublicKeyEntry.appendPublicKeyEntry(sb.append(' '), serverKey);
+        return KnownHostEntry.parseKnownHostEntry(sb.toString());
+    }
+
+    /**
+     * Invoked by {@link #prepareKnownHostEntry(ClientSession, SocketAddress, PublicKey)}
+     * in order to query whether to use a hashed value instead of a plain one for the
+     * written host name/address - default returns {@code null} - i.e., no hashing
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param hostIdentity The entry's host name/address
+     * @return The digester {@link NamedFactory} - {@code null} if no hashing is to be made
+     */
+    protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, String hostIdentity) {
+        return null;
+    }
+
+    /**
+     * Retrieves the host identities to be used when matching or updating an entry
+     * for it - by default returns the reported remote address and the original
+     * connection target host name/address (if same, then only one value is returned)
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @return A {@link Collection} of the names/addresses to use - if {@code null}/empty
+     * then ignored (i.e., no matching is done or no entry is generated)
+     * @see ClientSession#getConnectAddress()
+     * @see SshdSocketAddress#toAddressString(SocketAddress)
+     */
+    protected Collection<String> resolveHostNetworkIdentities(ClientSession clientSession, SocketAddress remoteAddress) {
+        /*
+         * NOTE !!! we do not resolve the fully-qualified name to avoid long DNS timeouts.
+         * Instead we use the reported peer address and the original connection target host
+         */
+        Collection<String> candidates = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+        candidates.add(SshdSocketAddress.toAddressString(remoteAddress));
+        SocketAddress connectAddress = clientSession.getConnectAddress();
+        candidates.add(SshdSocketAddress.toAddressString(connectAddress));
+        return candidates;
+    }
+
+    /**
+     * Invoked when a matching known host key was found but it does not match
+     * the presented one.
+     *
+     * @param clientSession The {@link ClientSession}
+     * @param remoteAddress The remote host address
+     * @param match The original {@link HostEntryPair} whose key did not match
+     * @param actual The presented server {@link PublicKey}
+     * @return {@code true} if accept the server key anyway - default={@code false}
+     */
+    protected boolean acceptModifiedServerKey(
+            ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual) {
+        KnownHostEntry entry = match.getHostEntry();
+        PublicKey expected = match.getServerKey();
+        log.warn("acceptModifiedServerKey({}) mismatched keys presented by {} for entry={}: expected={}-{}, actual={}-{}",
+                 clientSession, remoteAddress, entry,
+                 KeyUtils.getKeyType(expected), KeyUtils.getFingerPrint(expected),
+                 KeyUtils.getKeyType(actual), KeyUtils.getFingerPrint(actual));
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
index d0eb3da..5305dab 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
@@ -30,15 +30,13 @@ import org.apache.sshd.client.session.ClientSession;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public interface ServerKeyVerifier {
-
     /**
      * Verify that the server key provided is really the one of the host.
      *
-     * @param sshClientSession the current session
-     * @param remoteAddress    the host
-     * @param serverKey        the presented key
+     * @param clientSession    the current {@link ClientSession}
+     * @param remoteAddress    the host's {@link SocketAddress}
+     * @param serverKey        the presented server {@link PublicKey}
      * @return <code>true</code> if the key is accepted for the host
      */
-    boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey);
-
+    boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey);
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 936866f..195d5ef 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -20,6 +20,7 @@
 package org.apache.sshd.client.session;
 
 import java.io.IOException;
+import java.net.SocketAddress;
 import java.nio.file.FileSystem;
 import java.security.KeyPair;
 import java.util.List;
@@ -67,6 +68,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private PasswordIdentityProvider passwordIdentityProvider;
     private List<NamedFactory<UserAuth>> userAuthFactories;
     private ScpTransferEventListener scpListener;
+    private SocketAddress connectAddress;
 
     protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) {
         super(false, factoryManager, ioSession);
@@ -79,6 +81,15 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public SocketAddress getConnectAddress() {
+        return connectAddress;
+    }
+
+    public void setConnectAddress(SocketAddress connectAddress) {
+        this.connectAddress = connectAddress;
+    }
+
+    @Override
     public ServerKeyVerifier getServerKeyVerifier() {
         return resolveEffectiveProvider(ServerKeyVerifier.class, serverKeyVerifier, getFactoryManager().getServerKeyVerifier());
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index 9d7acbd..db3f10f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -19,6 +19,7 @@
 package org.apache.sshd.client.session;
 
 import java.io.IOException;
+import java.net.SocketAddress;
 import java.nio.file.FileSystem;
 import java.util.Collection;
 import java.util.Map;
@@ -77,6 +78,17 @@ public interface ClientSession extends Session, ClientAuthenticationManager {
     }
 
     /**
+     * Returns the original address (after having been translated through host
+     * configuration entries if any) that was request to connect. It contains the
+     * original host or address string that was used. <B>Note:</B> this may be
+     * different than the result of the {@link #getIoSession()} report of the
+     * remote peer
+     *
+     * @return The original requested address
+     */
+    SocketAddress getConnectAddress();
+
+    /**
      * Starts the authentication process.
      * User identities will be tried until the server successfully authenticate the user.
      * User identities must be provided before calling this method using

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
new file mode 100644
index 0000000..acf7e80
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
@@ -0,0 +1,361 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StreamCorruptedException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
+import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
+import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
+
+/**
+ * Represents an entry in the user's {@code authorized_keys} file according
+ * to the <A HREF="http://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys">OpenSSH format</A>.
+ * <B>Note:</B> {@code equals/hashCode} check only the key type and data - the
+ * comment and/or login options are not considered part of equality
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class AuthorizedKeyEntry extends PublicKeyEntry {
+
+    private static final long serialVersionUID = -9007505285002809156L;
+
+    private String comment;
+    // for options that have no value, "true" is used
+    private Map<String, String> loginOptions = Collections.emptyMap();
+
+    public AuthorizedKeyEntry() {
+        super();
+    }
+
+    public String getComment() {
+        return comment;
+    }
+
+    public void setComment(String value) {
+        this.comment = value;
+    }
+
+    public Map<String, String> getLoginOptions() {
+        return loginOptions;
+    }
+
+    public void setLoginOptions(Map<String, String> value) {
+        if (value == null) {
+            this.loginOptions = Collections.emptyMap();
+        } else {
+            this.loginOptions = value;
+        }
+    }
+
+    @Override
+    public PublicKey appendPublicKey(Appendable sb, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException {
+        Map<String, String> options = getLoginOptions();
+        if (!GenericUtils.isEmpty(options)) {
+            int index = 0;
+            for (Map.Entry<String, String> oe : options.entrySet()) {
+                String key = oe.getKey();
+                String value = oe.getValue();
+                if (index > 0) {
+                    sb.append(',');
+                }
+                sb.append(key);
+                // TODO figure out a way to remember which options where quoted
+                // TODO figure out a way to remember which options had no value
+                if (!Boolean.TRUE.toString().equals(value)) {
+                    sb.append('=').append(value);
+                }
+                index++;
+            }
+
+            if (index > 0) {
+                sb.append(' ');
+            }
+        }
+
+        PublicKey key = super.appendPublicKey(sb, fallbackResolver);
+        String kc = getComment();
+        if (!GenericUtils.isEmpty(kc)) {
+            sb.append(' ').append(kc);
+        }
+
+        return key;
+    }
+
+    @Override
+    public String toString() {
+        String entry = super.toString();
+        String kc = getComment();
+        Map<?, ?> ko = getLoginOptions();
+        return (GenericUtils.isEmpty(ko) ? "" : ko.toString() + " ")
+                + entry
+                + (GenericUtils.isEmpty(kc) ? "" : " " + kc);
+    }
+
+    public static PublickeyAuthenticator fromAuthorizedEntries(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
+            throws IOException, GeneralSecurityException {
+        Collection<PublicKey> keys = resolveAuthorizedKeys(fallbackResolver, entries);
+        if (GenericUtils.isEmpty(keys)) {
+            return RejectAllPublickeyAuthenticator.INSTANCE;
+        } else {
+            return new KeySetPublickeyAuthenticator(keys);
+        }
+    }
+
+    public static List<PublicKey> resolveAuthorizedKeys(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
+            throws IOException, GeneralSecurityException {
+        if (GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        }
+
+        List<PublicKey> keys = new ArrayList<PublicKey>(entries.size());
+        for (AuthorizedKeyEntry e : entries) {
+            PublicKey k = e.resolvePublicKey(fallbackResolver);
+            if (k != null) {
+                keys.add(k);
+            }
+        }
+
+        return keys;
+    }
+
+    /**
+     * Reads read the contents of an <code>authorized_keys</code> file
+     *
+     * @param url The {@link URL} to read from
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #readAuthorizedKeys(InputStream, boolean)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(URL url) throws IOException {
+        try (InputStream in = url.openStream()) {
+            return readAuthorizedKeys(in, true);
+        }
+    }
+
+    /**
+     * Reads read the contents of an <code>authorized_keys</code> file
+     *
+     * @param file The {@link File} to read from
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #readAuthorizedKeys(InputStream, boolean)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(File file) throws IOException {
+        try (InputStream in = new FileInputStream(file)) {
+            return readAuthorizedKeys(in, true);
+        }
+    }
+
+    /**
+     * Reads read the contents of an <code>authorized_keys</code> file
+     *
+     * @param path    {@link Path} to read from
+     * @param options The {@link OpenOption}s to use - if unspecified then appropriate
+     *                defaults assumed
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #readAuthorizedKeys(InputStream, boolean)
+     * @see Files#newInputStream(Path, OpenOption...)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(Path path, OpenOption... options) throws IOException {
+        try (InputStream in = Files.newInputStream(path, options)) {
+            return readAuthorizedKeys(in, true);
+        }
+    }
+
+    /**
+     * Reads read the contents of an <code>authorized_keys</code> file
+     *
+     * @param filePath The file path to read from
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #readAuthorizedKeys(InputStream, boolean)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(String filePath) throws IOException {
+        try (InputStream in = new FileInputStream(filePath)) {
+            return readAuthorizedKeys(in, true);
+        }
+    }
+
+    /**
+     * Reads read the contents of an <code>authorized_keys</code> file
+     *
+     * @param in        The {@link InputStream}
+     * @param okToClose <code>true</code> if method may close the input stream
+     *                  regardless of whether successful or failed
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #readAuthorizedKeys(Reader, boolean)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(InputStream in, boolean okToClose) throws IOException {
+        try (Reader rdr = new InputStreamReader(NoCloseInputStream.resolveInputStream(in, okToClose), StandardCharsets.UTF_8)) {
+            return readAuthorizedKeys(rdr, true);
+        }
+    }
+
+    /**
+     * Reads read the contents of an <code>authorized_keys</code> file
+     *
+     * @param rdr       The {@link Reader}
+     * @param okToClose <code>true</code> if method may close the input stream
+     *                  regardless of whether successful or failed
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #readAuthorizedKeys(BufferedReader)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(Reader rdr, boolean okToClose) throws IOException {
+        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+            return readAuthorizedKeys(buf);
+        }
+    }
+
+    /**
+     * @param rdr The {@link BufferedReader} to use to read the contents of
+     *            an <code>authorized_keys</code> file
+     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+     * @throws IOException If failed to read or parse the entries
+     * @see #parseAuthorizedKeyEntry(String)
+     */
+    public static List<AuthorizedKeyEntry> readAuthorizedKeys(BufferedReader rdr) throws IOException {
+        List<AuthorizedKeyEntry> entries = null;
+
+        for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
+            AuthorizedKeyEntry entry;
+            try {
+                entry = parseAuthorizedKeyEntry(line.trim());
+                if (entry == null) {    // null, empty or comment line
+                    continue;
+                }
+            } catch (RuntimeException | Error e) {
+                throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+                        + " to parse key entry=" + line + ": " + e.getMessage());
+            }
+
+            if (entries == null) {
+                entries = new ArrayList<>();
+            }
+
+            entries.add(entry);
+        }
+
+        if (entries == null) {
+            return Collections.emptyList();
+        } else {
+            return entries;
+        }
+    }
+
+    /**
+     * @param line Original line from an <code>authorized_keys</code> file
+     * @return {@link AuthorizedKeyEntry} or {@code null} if the line is
+     * {@code null}/empty or a comment line
+     * @throws IllegalArgumentException If failed to parse/decode the line
+     * @see #COMMENT_CHAR
+     */
+    public static AuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
+        if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
+            return null;
+        }
+
+        int startPos = line.indexOf(' ');
+        if (startPos <= 0) {
+            throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+        }
+
+        int endPos = line.indexOf(' ', startPos + 1);
+        if (endPos <= startPos) {
+            endPos = line.length();
+        }
+
+        String keyType = line.substring(0, startPos);
+        PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
+        final AuthorizedKeyEntry entry;
+        if (decoder == null) {  // assume this is due to the fact that it starts with login options
+            entry = parseAuthorizedKeyEntry(line.substring(startPos + 1).trim());
+            if (entry == null) {
+                throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
+            }
+
+            entry.setLoginOptions(parseLoginOptions(keyType));
+        } else {
+            String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
+            String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
+            entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData);
+            entry.setComment(comment);
+        }
+
+        return entry;
+    }
+
+    public static Map<String, String> parseLoginOptions(String options) {
+        // TODO add support if quoted values contain ','
+        String[] pairs = GenericUtils.split(options, ',');
+        if (GenericUtils.isEmpty(pairs)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, String> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (String p : pairs) {
+            p = GenericUtils.trimToEmpty(p);
+            if (GenericUtils.isEmpty(p)) {
+                continue;
+            }
+
+            int pos = p.indexOf('=');
+            String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
+            CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
+            value = GenericUtils.stripQuotes(value);
+            if (value == null) {
+                value = Boolean.TRUE.toString();
+            }
+
+            String prev = optsMap.put(name, value.toString());
+            if (prev != null) {
+                throw new IllegalArgumentException("Multiple values for key=" + name + ": old=" + prev + ", new=" + value);
+            }
+        }
+
+        return optsMap;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java b/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
index f5e89c7..72adddd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
@@ -92,15 +92,25 @@ public class BaseMac implements Mac {
     }
 
     @Override   // TODO make this a default method in Java 8
+    public byte[] doFinal() throws Exception {
+        int blockSize = getBlockSize();
+        byte[] buf = new byte[blockSize];
+        doFinal(buf);
+        return buf;
+    }
+
+    @Override   // TODO make this a default method in Java 8
     public void doFinal(byte[] buf) throws Exception {
         doFinal(buf, 0);
     }
 
     @Override
     public void doFinal(byte[] buf, int offset) throws Exception {
-        if (bsize != defbsize) {
+        int blockSize = getBlockSize();
+        int defaultSize = getDefaultBlockSize();
+        if (blockSize != defaultSize) {
             mac.doFinal(tmp, 0);
-            System.arraycopy(tmp, 0, buf, offset, bsize);
+            System.arraycopy(tmp, 0, buf, offset, blockSize);
         } else {
             mac.doFinal(buf, offset);
         }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java b/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
index 0da9472..b69ad34 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
@@ -33,6 +33,8 @@ public interface Mac extends MacInformation {
 
     void updateUInt(long foo);
 
+    byte[] doFinal() throws Exception;
+
     void doFinal(byte[] buf) throws Exception;
 
     void doFinal(byte[] buf, int offset) throws Exception;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java b/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
index c392420..fe7f04d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
@@ -33,7 +33,7 @@ import java.security.InvalidParameterException;
  *         TODO replace this class with {@code java.util.Base64} when upgrading to JDK 1.8
  * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
  */
-public class Base64 {
+public final class Base64 {
 
     /**
      * <P>Chunk size per RFC 2045 section 6.8.</P>
@@ -43,54 +43,54 @@ public class Base64 {
      *
      * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
      */
-    static final int CHUNK_SIZE = 76;
-
-    /**
-     * Chunk separator per RFC 2045 section 2.1.
-     *
-     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
-     */
-    static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes(StandardCharsets.UTF_8);
+    public static final int CHUNK_SIZE = 76;
 
     /**
      * The base length.
      */
-    static final int BASELENGTH = 255;
+    public static final int BASELENGTH = 255;
 
     /**
      * Lookup length.
      */
-    static final int LOOKUPLENGTH = 64;
+    public static final int LOOKUPLENGTH = 64;
 
     /**
      * Used to calculate the number of bits in a byte.
      */
-    static final int EIGHTBIT = 8;
+    public static final int EIGHTBIT = Byte.SIZE;
 
     /**
      * Used when encoding something which has fewer than 24 bits.
      */
-    static final int SIXTEENBIT = 16;
+    public static final int SIXTEENBIT = 2 * EIGHTBIT;
 
     /**
      * Used to determine how many bits data contains.
      */
-    static final int TWENTYFOURBITGROUP = 24;
+    public static final int TWENTYFOURBITGROUP = 3 * EIGHTBIT;
 
     /**
      * Used to get the number of Quadruples.
      */
-    static final int FOURBYTE = 4;
+    public static final int FOURBYTE = 4;
 
     /**
      * Used to test the sign of a byte.
      */
-    static final int SIGN = -128;
+    public static final int SIGN = -128;
 
     /**
      * Byte used to pad output.
      */
-    static final byte PAD = (byte) '=';
+    public static final byte PAD = (byte) '=';
+
+    /**
+     * Chunk separator per RFC 2045 section 2.1.
+     *
+     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
+     */
+    static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes(StandardCharsets.UTF_8);
 
     // Create arrays to hold the base64 characters and a
     // lookup for base64 chars
@@ -132,7 +132,11 @@ public class Base64 {
         lookUpBase64Alphabet[63] = (byte) '/';
     }
 
-    private static boolean isBase64(byte octect) {
+    private Base64() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static boolean isBase64(byte octect) {
         return octect == PAD || base64Alphabet[octect] != -1;
     }
 
@@ -145,10 +149,9 @@ public class Base64 {
      * alphabet or if the byte array is empty; false, otherwise
      */
     public static boolean isArrayByteBase64(byte[] arrayOctect) {
-
         arrayOctect = discardWhitespace(arrayOctect);
 
-        int length = arrayOctect.length;
+        int length = NumberUtils.length(arrayOctect);
         if (length == 0) {
             // shouldn't a 0 length array be valid base64 data?
             return true;
@@ -227,7 +230,8 @@ public class Base64 {
      * @return Base64-encoded data.
      */
     public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
-        int lengthDataBits = binaryData.length * EIGHTBIT;
+        int lengthDataBytes = NumberUtils.length(binaryData);
+        int lengthDataBits = lengthDataBytes * EIGHTBIT;
         int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
         int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
         byte encodedData[];
@@ -246,7 +250,6 @@ public class Base64 {
         // for compliance with RFC 2045 MIME, then it is important to
         // allow for extra length to account for the separator(s)
         if (isChunked) {
-
             nbrChunks = CHUNK_SEPARATOR.length == 0 ? 0 : (int) Math.ceil((float) encodedDataLength / CHUNK_SIZE);
             encodedDataLength += nbrChunks * CHUNK_SEPARATOR.length;
         }
@@ -309,7 +312,6 @@ public class Base64 {
             encodedData[encodedIndex + 2] = PAD;
             encodedData[encodedIndex + 3] = PAD;
         } else if (fewerThan24bits == SIXTEENBIT) {
-
             b1 = binaryData[dataIndex];
             b2 = binaryData[dataIndex + 1];
             l = (byte) (b2 & 0x0f);
@@ -418,28 +420,50 @@ public class Base64 {
      *
      * @param data The base-64 encoded data to discard the whitespace
      *             from.
-     * @return The data, less whitespace (see RFC 2045).
+     * @return The data, less whitespace (see RFC 2045) - may be same
+     * as input if no whitespace found
      */
-    static byte[] discardWhitespace(byte[] data) {
-        byte groomedData[] = new byte[data.length];
+    public static byte[] discardWhitespace(byte[] data) {
+        if (NumberUtils.isEmpty(data)) {
+            return GenericUtils.EMPTY_BYTE_ARRAY;
+        }
+
+        byte groomedData[] = null;
         int bytesCopied = 0;
 
-        for (int i = 0; i < data.length; i++) {
-            switch (data[i]) {
-                case (byte) ' ':
-                case (byte) '\n':
-                case (byte) '\r':
-                case (byte) '\t':
-                    break;
-                default:
-                    groomedData[bytesCopied++] = data[i];
+        for (int index = 0; index < data.length; index++) {
+            byte v = data[index];
+            boolean isWhiteSpace = (v == (byte) ' ') || (v == (byte) '\t') || (v == (byte) '\r') || (v == (byte) '\n');
+            if (groomedData == null) {
+                if (isWhiteSpace) { // all values up to this index were NOT white space
+                    groomedData = new byte[data.length - 1];
+                    if (index > 0) {
+                        System.arraycopy(data, 0, groomedData, 0, index);
+                    }
+                    bytesCopied = index;
+                }
+            } else {
+                if (isWhiteSpace) {
+                    continue;
+                }
+                groomedData[bytesCopied++] = v;
             }
         }
 
-        byte packedData[] = new byte[bytesCopied];
+        if (groomedData == null) {
+            return data;    // all characters where non-whitespace
+        }
 
-        System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
+        if (bytesCopied <= 0) {
+            return GenericUtils.EMPTY_BYTE_ARRAY;
+        }
 
+        if (bytesCopied == groomedData.length) {
+            return groomedData;
+        }
+
+        byte[] packedData = new byte[bytesCopied];
+        System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
         return packedData;
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java b/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
index 4621862..0d34fd2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
@@ -21,7 +21,7 @@ package org.apache.sshd.common.util;
 import java.util.Objects;
 
 /**
- * Represents a pair of values
+ * Represents an un-modifiable pair of values
  *
  * @param <F> First value type
  * @param <S> Second value type

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
index caa9a35..9181b90 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
@@ -314,8 +314,9 @@ public final class BufferUtils {
             // TODO use Integer.BYTES for JDK-8
             IoUtils.readFully(input, buf, offset, Integer.SIZE / Byte.SIZE);
             return getUInt(buf, offset, len);
-        } catch (IllegalArgumentException e) {
-            throw new StreamCorruptedException(e.getMessage());
+        } catch (RuntimeException | Error e) {
+            throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+                    + " to read UINT value: " + e.getMessage());
         }
     }
 
@@ -408,8 +409,9 @@ public final class BufferUtils {
         try {
             int writeLen = putUInt(value, buf, off, len);
             output.write(buf, off, writeLen);
-        } catch (IllegalArgumentException e) {
-            throw new StreamCorruptedException(e.getMessage());
+        } catch (RuntimeException | Error e) {
+            throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+                    + " to write UINT value=" + value + ": " + e.getMessage());
         }
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
index 94b8f28..d51b7f6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
@@ -213,9 +213,10 @@ public class ModifiableFileWatcher extends AbstractLoggingBean {
      * @param path    The {@link Path} to be checked - ignored if {@code null}
      *                or does not exist
      * @param options The {@link LinkOption}s to use to query the file's permissions
-     * @return The violated permission as {@link Pair} first is a message second is
-     * offending object {@link PosixFilePermission} or {@link String} for owner - {@code null} if
-     * no violations detected
+     * @return The violated permission as {@link Pair} where {@link Pair#getClass()}
+     * is a loggable message and {@link Pair#getSecond()} is the offending object
+     * - e.g., {@link PosixFilePermission} or {@link String} for owner. Return
+     * value is {@code null} if no violations detected
      * @throws IOException If failed to retrieve the permissions
      * @see #STRICTLY_PROHIBITED_FILE_PERMISSION
      */

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java b/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
index 7ea3806..9a2778d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
@@ -289,6 +289,47 @@ public class SshdSocketAddress extends SocketAddress {
         return true;
     }
 
+    public static String toAddressString(SocketAddress addr) {
+        if (addr == null) {
+            return null;
+        }
+
+        if (addr instanceof InetSocketAddress) {
+            return ((InetSocketAddress) addr).getHostString();
+        }
+
+        if (addr instanceof SshdSocketAddress) {
+            return ((SshdSocketAddress) addr).getHostName();
+        }
+
+        return addr.toString();
+    }
+
+    /**
+     * <P>Converts a {@code SocketAddress} into an {@link InetSocketAddress} if possible:</P></BR>
+     * <UL>
+     *      <LI>If already an {@link InetSocketAddress} then cast it as such</LI>
+     *      <LI>If an {@code SshdSocketAddress} then invoke {@link #toInetSocketAddress()}</LI>
+     *      <LI>Otherwise, throw an exception</LI>
+     * </UL>
+     *
+     * @param remoteAddress The {@link SocketAddress} - ignored if {@code null}
+     * @return The {@link InetSocketAddress} instance
+     * @throws ClassCastException if argument is not already an {@code InetSocketAddress}
+     * or a {@code SshdSocketAddress}
+     */
+    public static InetSocketAddress toInetSocketAddress(SocketAddress remoteAddress) {
+        if (remoteAddress == null) {
+            return null;
+        } else if (remoteAddress instanceof InetSocketAddress) {
+            return (InetSocketAddress) remoteAddress;
+        } else if (remoteAddress instanceof SshdSocketAddress) {
+            return ((SshdSocketAddress) remoteAddress).toInetSocketAddress();
+        } else {
+            throw new ClassCastException("Unknown remote address type: " + remoteAddress);
+        }
+    }
+
     public static String toAddressString(InetAddress addr) {
         String ip = (addr == null) ? null : addr.toString();
         if (GenericUtils.isEmpty(ip)) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
deleted file mode 100644
index c57cde5..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
+++ /dev/null
@@ -1,399 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.sshd.server.config.keys;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.StreamCorruptedException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.security.GeneralSecurityException;
-import java.security.PublicKey;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
-import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
-import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-import org.apache.sshd.common.util.io.NoCloseReader;
-import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
-import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
-import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
-
-/**
- * Represents an entry in the user's {@code authorized_keys} file according
- * to the <A HREF="http://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys">OpenSSH format</A>.
- * <B>Note:</B> {@code equals/hashCode} check only the key type and data - the
- * comment and/or login options are not considered part of equality
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class AuthorizedKeyEntry extends PublicKeyEntry {
-
-    /**
-     * Standard OpenSSH authorized keys file name
-     */
-    public static final String STD_AUTHORIZED_KEYS_FILENAME = "authorized_keys";
-
-    private static final long serialVersionUID = -9007505285002809156L;
-
-    private static final class LazyDefaultAuthorizedKeysFileHolder {
-        private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_AUTHORIZED_KEYS_FILENAME);
-    }
-
-    private String comment;
-    // for options that have no value, "true" is used
-    private Map<String, String> loginOptions = Collections.emptyMap();
-
-    public AuthorizedKeyEntry() {
-        super();
-    }
-
-    public String getComment() {
-        return comment;
-    }
-
-    public void setComment(String value) {
-        this.comment = value;
-    }
-
-    public Map<String, String> getLoginOptions() {
-        return loginOptions;
-    }
-
-    public void setLoginOptions(Map<String, String> value) {
-        if (value == null) {
-            this.loginOptions = Collections.emptyMap();
-        } else {
-            this.loginOptions = value;
-        }
-    }
-
-    @Override
-    public PublicKey appendPublicKey(Appendable sb, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException {
-        Map<String, String> options = getLoginOptions();
-        if (!GenericUtils.isEmpty(options)) {
-            int index = 0;
-            for (Map.Entry<String, String> oe : options.entrySet()) {
-                String key = oe.getKey();
-                String value = oe.getValue();
-                if (index > 0) {
-                    sb.append(',');
-                }
-                sb.append(key);
-                // TODO figure out a way to remember which options where quoted
-                // TODO figure out a way to remember which options had no value
-                if (!Boolean.TRUE.toString().equals(value)) {
-                    sb.append('=').append(value);
-                }
-                index++;
-            }
-
-            if (index > 0) {
-                sb.append(' ');
-            }
-        }
-
-        PublicKey key = super.appendPublicKey(sb, fallbackResolver);
-        String kc = getComment();
-        if (!GenericUtils.isEmpty(kc)) {
-            sb.append(' ').append(kc);
-        }
-
-        return key;
-    }
-
-    @Override
-    public String toString() {
-        String entry = super.toString();
-        String kc = getComment();
-        Map<?, ?> ko = getLoginOptions();
-        return (GenericUtils.isEmpty(ko) ? "" : ko.toString() + " ")
-                + entry
-                + (GenericUtils.isEmpty(kc) ? "" : " " + kc);
-    }
-
-    public static PublickeyAuthenticator fromAuthorizedEntries(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
-            throws IOException, GeneralSecurityException {
-        Collection<PublicKey> keys = resolveAuthorizedKeys(fallbackResolver, entries);
-        if (GenericUtils.isEmpty(keys)) {
-            return RejectAllPublickeyAuthenticator.INSTANCE;
-        } else {
-            return new KeySetPublickeyAuthenticator(keys);
-        }
-    }
-
-    public static List<PublicKey> resolveAuthorizedKeys(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
-            throws IOException, GeneralSecurityException {
-        if (GenericUtils.isEmpty(entries)) {
-            return Collections.emptyList();
-        }
-
-        List<PublicKey> keys = new ArrayList<PublicKey>(entries.size());
-        for (AuthorizedKeyEntry e : entries) {
-            PublicKey k = e.resolvePublicKey(fallbackResolver);
-            if (k != null) {
-                keys.add(k);
-            }
-        }
-
-        return keys;
-    }
-
-    /**
-     * @return The default {@link Path} location of the OpenSSH authorized keys file
-     */
-    @SuppressWarnings("synthetic-access")
-    public static Path getDefaultAuthorizedKeysFile() {
-        return LazyDefaultAuthorizedKeysFileHolder.KEYS_FILE;
-    }
-
-    /**
-     * Reads read the contents of the default OpenSSH <code>authorized_keys</code> file
-     *
-     * @param options The {@link OpenOption}s to use when reading the file
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there -
-     * or empty if file does not exist
-     * @throws IOException If failed to read keys from file
-     */
-    public static List<AuthorizedKeyEntry> readDefaultAuthorizedKeys(OpenOption ... options) throws IOException {
-        Path keysFile = getDefaultAuthorizedKeysFile();
-        if (Files.exists(keysFile, IoUtils.EMPTY_LINK_OPTIONS)) {
-            return readAuthorizedKeys(keysFile);
-        } else {
-            return Collections.emptyList();
-        }
-    }
-
-    /**
-     * Reads read the contents of an <code>authorized_keys</code> file
-     *
-     * @param url The {@link URL} to read from
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #readAuthorizedKeys(InputStream, boolean)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(URL url) throws IOException {
-        try (InputStream in = url.openStream()) {
-            return readAuthorizedKeys(in, true);
-        }
-    }
-
-    /**
-     * Reads read the contents of an <code>authorized_keys</code> file
-     *
-     * @param file The {@link File} to read from
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #readAuthorizedKeys(InputStream, boolean)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(File file) throws IOException {
-        try (InputStream in = new FileInputStream(file)) {
-            return readAuthorizedKeys(in, true);
-        }
-    }
-
-    /**
-     * Reads read the contents of an <code>authorized_keys</code> file
-     *
-     * @param path    {@link Path} to read from
-     * @param options The {@link OpenOption}s to use - if unspecified then appropriate
-     *                defaults assumed
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #readAuthorizedKeys(InputStream, boolean)
-     * @see Files#newInputStream(Path, OpenOption...)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(Path path, OpenOption... options) throws IOException {
-        try (InputStream in = Files.newInputStream(path, options)) {
-            return readAuthorizedKeys(in, true);
-        }
-    }
-
-    /**
-     * Reads read the contents of an <code>authorized_keys</code> file
-     *
-     * @param filePath The file path to read from
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #readAuthorizedKeys(InputStream, boolean)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(String filePath) throws IOException {
-        try (InputStream in = new FileInputStream(filePath)) {
-            return readAuthorizedKeys(in, true);
-        }
-    }
-
-    /**
-     * Reads read the contents of an <code>authorized_keys</code> file
-     *
-     * @param in        The {@link InputStream}
-     * @param okToClose <code>true</code> if method may close the input stream
-     *                  regardless of whether successful or failed
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #readAuthorizedKeys(Reader, boolean)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(InputStream in, boolean okToClose) throws IOException {
-        try (Reader rdr = new InputStreamReader(NoCloseInputStream.resolveInputStream(in, okToClose), StandardCharsets.UTF_8)) {
-            return readAuthorizedKeys(rdr, true);
-        }
-    }
-
-    /**
-     * Reads read the contents of an <code>authorized_keys</code> file
-     *
-     * @param rdr       The {@link Reader}
-     * @param okToClose <code>true</code> if method may close the input stream
-     *                  regardless of whether successful or failed
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #readAuthorizedKeys(BufferedReader)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(Reader rdr, boolean okToClose) throws IOException {
-        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
-            return readAuthorizedKeys(buf);
-        }
-    }
-
-    /**
-     * @param rdr The {@link BufferedReader} to use to read the contents of
-     *            an <code>authorized_keys</code> file
-     * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
-     * @throws IOException If failed to read or parse the entries
-     * @see #parseAuthorizedKeyEntry(String)
-     */
-    public static List<AuthorizedKeyEntry> readAuthorizedKeys(BufferedReader rdr) throws IOException {
-        List<AuthorizedKeyEntry> entries = null;
-
-        for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
-            AuthorizedKeyEntry entry;
-            try {
-                entry = parseAuthorizedKeyEntry(line.trim());
-                if (entry == null) {    // null, empty or comment line
-                    continue;
-                }
-            } catch (IllegalArgumentException e) {
-                throw new StreamCorruptedException(e.getMessage());
-            }
-
-            if (entries == null) {
-                entries = new ArrayList<>();
-            }
-
-            entries.add(entry);
-        }
-
-        if (entries == null) {
-            return Collections.emptyList();
-        } else {
-            return entries;
-        }
-    }
-
-    /**
-     * @param line Original line from an <code>authorized_keys</code> file
-     * @return {@link AuthorizedKeyEntry} or {@code null} if the line is
-     * {@code null}/empty or a comment line
-     * @throws IllegalArgumentException If failed to parse/decode the line
-     * @see #COMMENT_CHAR
-     */
-    public static AuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
-        if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
-            return null;
-        }
-
-        int startPos = line.indexOf(' ');
-        if (startPos <= 0) {
-            throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
-        }
-
-        int endPos = line.indexOf(' ', startPos + 1);
-        if (endPos <= startPos) {
-            endPos = line.length();
-        }
-
-        String keyType = line.substring(0, startPos);
-        PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
-        final AuthorizedKeyEntry entry;
-        if (decoder == null) {  // assume this is due to the fact that it starts with login options
-            entry = parseAuthorizedKeyEntry(line.substring(startPos + 1).trim());
-            if (entry == null) {
-                throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
-            }
-
-            entry.setLoginOptions(parseLoginOptions(keyType));
-        } else {
-            String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
-            String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
-            entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData);
-            entry.setComment(comment);
-        }
-
-        return entry;
-    }
-
-    public static Map<String, String> parseLoginOptions(String options) {
-        // TODO add support if quoted values contain ','
-        String[] pairs = GenericUtils.split(options, ',');
-        if (GenericUtils.isEmpty(pairs)) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, String> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        for (String p : pairs) {
-            p = GenericUtils.trimToEmpty(p);
-            if (GenericUtils.isEmpty(p)) {
-                continue;
-            }
-
-            int pos = p.indexOf('=');
-            String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
-            CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
-            value = GenericUtils.stripQuotes(value);
-            if (value == null) {
-                value = Boolean.TRUE.toString();
-            }
-
-            String prev = optsMap.put(name, value.toString());
-            if (prev != null) {
-                throw new IllegalArgumentException("Multiple values for key=" + name + ": old=" + prev + ", new=" + value);
-            }
-        }
-
-        return optsMap;
-    }
-}