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 2018/09/06 16:03:59 UTC

[49/51] [abbrv] mina-sshd git commit: [SSHD-842] Split common utilities code from sshd-core into sshd-common (new artifact)

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
new file mode 100644
index 0000000..28a3d1f
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
@@ -0,0 +1,170 @@
+/*
+ * 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.Base64;
+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.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 {
+        Base64.Encoder encoder = Base64.getEncoder();
+        sb.append(HASHED_HOST_DELIMITER).append(factory.getName());
+        sb.append(HASHED_HOST_DELIMITER).append(encoder.encodeToString(salt));
+        sb.append(HASHED_HOST_DELIMITER).append(encoder.encodeToString(digest));
+        return sb;
+    }
+
+    public static KnownHostHashValue parse(String patternString) {
+        String pattern = GenericUtils.replaceWhitespaceAndTrim(patternString);
+        return parse(pattern, GenericUtils.isEmpty(pattern) ? null : new KnownHostHashValue());
+    }
+
+    public static <V extends KnownHostHashValue> V parse(String patternString, V value) {
+        String pattern = GenericUtils.replaceWhitespaceAndTrim(patternString);
+        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 (unknown digest): %s", pattern);
+        Base64.Decoder decoder = Base64.getDecoder();
+        value.setDigester(factory);
+        value.setSaltValue(decoder.decode(components[2]));
+        value.setDigestValue(decoder.decode(components[3]));
+        return value;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/BuiltinClientIdentitiesWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/BuiltinClientIdentitiesWatcher.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/BuiltinClientIdentitiesWatcher.java
new file mode 100644
index 0000000..ab9929b
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/BuiltinClientIdentitiesWatcher.java
@@ -0,0 +1,106 @@
+/*
+ * 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.keys;
+
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class BuiltinClientIdentitiesWatcher extends ClientIdentitiesWatcher {
+    private final boolean supportedOnly;
+
+    public BuiltinClientIdentitiesWatcher(Path keysFolder, boolean supportedOnly,
+            ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        this(keysFolder, NamedResource.getNameList(BuiltinIdentities.VALUES), supportedOnly, loader, provider, strict);
+    }
+
+    public BuiltinClientIdentitiesWatcher(Path keysFolder, Collection<String> ids, boolean supportedOnly,
+            ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        this(keysFolder, ids, supportedOnly,
+             GenericUtils.supplierOf(Objects.requireNonNull(loader, "No client identity loader")),
+             GenericUtils.supplierOf(Objects.requireNonNull(provider, "No password provider")),
+             strict);
+    }
+
+    public BuiltinClientIdentitiesWatcher(Path keysFolder, boolean supportedOnly,
+            Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        this(keysFolder, NamedResource.getNameList(BuiltinIdentities.VALUES), supportedOnly, loader, provider, strict);
+    }
+
+    public BuiltinClientIdentitiesWatcher(Path keysFolder, Collection<String> ids, boolean supportedOnly,
+            Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        super(getBuiltinIdentitiesPaths(keysFolder, ids), loader, provider, strict);
+        this.supportedOnly = supportedOnly;
+    }
+
+    public final boolean isSupportedOnly() {
+        return supportedOnly;
+    }
+
+    @Override
+    public Iterable<KeyPair> loadKeys() {
+        return isSupportedOnly() ? loadKeys(this::isSupported) : super.loadKeys();
+    }
+
+    private boolean isSupported(KeyPair kp) {
+        BuiltinIdentities id = BuiltinIdentities.fromKeyPair(kp);
+        if ((id != null) && id.isSupported()) {
+            return true;
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("loadKeys - remove unsupported identity={}, key-type={}, key={}",
+                    id, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic()));
+        }
+        return false;
+    }
+
+    public static List<Path> getDefaultBuiltinIdentitiesPaths(Path keysFolder) {
+        return getBuiltinIdentitiesPaths(keysFolder, NamedResource.getNameList(BuiltinIdentities.VALUES));
+    }
+
+    public static List<Path> getBuiltinIdentitiesPaths(Path keysFolder, Collection<String> ids) {
+        Objects.requireNonNull(keysFolder, "No keys folder");
+        if (GenericUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+
+        List<Path> paths = new ArrayList<>(ids.size());
+        for (String id : ids) {
+            String fileName = ClientIdentity.getIdentityFileName(id);
+            paths.add(keysFolder.resolve(fileName));
+        }
+
+        return paths;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentitiesWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentitiesWatcher.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentitiesWatcher.java
new file mode 100644
index 0000000..094766f
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentitiesWatcher.java
@@ -0,0 +1,139 @@
+/*
+ * 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.keys;
+
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * Watches over a group of files that contains client identities
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ClientIdentitiesWatcher extends AbstractKeyPairProvider implements KeyPairProvider {
+    private final Collection<ClientIdentityProvider> providers;
+
+    public ClientIdentitiesWatcher(Collection<? extends Path> paths,
+            ClientIdentityLoader loader, FilePasswordProvider provider) {
+        this(paths, loader, provider, true);
+    }
+
+    public ClientIdentitiesWatcher(Collection<? extends Path> paths,
+            ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        this(paths,
+             GenericUtils.supplierOf(Objects.requireNonNull(loader, "No client identity loader")),
+             GenericUtils.supplierOf(Objects.requireNonNull(provider, "No password provider")),
+             strict);
+    }
+
+    public ClientIdentitiesWatcher(Collection<? extends Path> paths,
+            Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider) {
+        this(paths, loader, provider, true);
+    }
+
+    public ClientIdentitiesWatcher(Collection<? extends Path> paths,
+            Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        this(buildProviders(paths, loader, provider, strict));
+    }
+
+    public ClientIdentitiesWatcher(Collection<ClientIdentityProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Iterable<KeyPair> loadKeys() {
+        return loadKeys(null);
+    }
+
+    protected Iterable<KeyPair> loadKeys(Predicate<? super KeyPair> filter) {
+        return () -> {
+            Stream<KeyPair> stream = safeMap(GenericUtils.stream(providers), this::doGetKeyPair);
+            if (filter != null) {
+                stream = stream.filter(filter);
+            }
+            return stream.iterator();
+        };
+    }
+
+    /**
+     * Performs a mapping operation on the stream, discarding any null values
+     * returned by the mapper.
+     *
+     * @param <U> Original type
+     * @param <V> Mapped type
+     * @param stream Original values stream
+     * @param mapper Mapper to target type
+     * @return Mapped stream
+     */
+    protected <U, V> Stream<V> safeMap(Stream<U> stream, Function<? super U, ? extends V> mapper) {
+        return stream.map(u -> Optional.ofNullable(mapper.apply(u)))
+                .filter(Optional::isPresent)
+                .map(Optional::get);
+    }
+
+    protected KeyPair doGetKeyPair(ClientIdentityProvider p) {
+        try {
+            KeyPair kp = p.getClientIdentity();
+            if (kp == null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("loadKeys({}) no key loaded", p);
+                }
+            }
+            return kp;
+        } catch (Throwable e) {
+            log.warn("loadKeys({}) failed ({}) to load key: {}", p, e.getClass().getSimpleName(), e.getMessage());
+            if (log.isDebugEnabled()) {
+                log.debug("loadKeys(" + p + ") key load failure details", e);
+            }
+            return null;
+        }
+    }
+
+    public static List<ClientIdentityProvider> buildProviders(
+            Collection<? extends Path> paths, ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        return buildProviders(paths,
+                GenericUtils.supplierOf(Objects.requireNonNull(loader, "No client identity loader")),
+                GenericUtils.supplierOf(Objects.requireNonNull(provider, "No password provider")),
+                strict);
+    }
+
+    public static List<ClientIdentityProvider> buildProviders(
+            Collection<? extends Path> paths, Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        if (GenericUtils.isEmpty(paths)) {
+            return Collections.emptyList();
+        }
+
+        return GenericUtils.map(paths, p -> new ClientIdentityFileWatcher(p, loader, provider, strict));
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java
new file mode 100644
index 0000000..931d96f
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java
@@ -0,0 +1,268 @@
+/*
+ * 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.keys;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Function;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.IdentityUtils;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.FileInfoExtractor;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Provides keys loading capability from the user's keys folder - e.g., {@code id_rsa}
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see org.apache.sshd.common.util.security.SecurityUtils#getKeyPairResourceParser()
+ */
+public final class ClientIdentity {
+
+    public static final String ID_FILE_PREFIX = "id_";
+
+    public static final String ID_FILE_SUFFIX = "";
+
+    public static final Function<String, String> ID_GENERATOR =
+            ClientIdentity::getIdentityFileName;
+
+    private ClientIdentity() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    /**
+     * @param name The file name - ignored if {@code null}/empty
+     * @return The identity type - {@code null} if cannot determine it - e.g.,
+     * does not start with the {@link #ID_FILE_PREFIX}
+     */
+    public static String getIdentityType(String name) {
+        if (GenericUtils.isEmpty(name)
+                || (name.length() <= ID_FILE_PREFIX.length())
+                || (!name.startsWith(ID_FILE_PREFIX))) {
+            return null;
+        } else {
+            return name.substring(ID_FILE_PREFIX.length());
+        }
+    }
+
+    public static String getIdentityFileName(NamedResource r) {
+        return getIdentityFileName((r == null) ? null : r.getName());
+    }
+
+    /**
+     * @param type The identity type - e.g., {@code rsa} - ignored
+     *             if {@code null}/empty
+     * @return The matching file name for the identity - {@code null}
+     * if no name
+     * @see #ID_FILE_PREFIX
+     * @see #ID_FILE_SUFFIX
+     * @see IdentityUtils#getIdentityFileName(String, String, String)
+     */
+    public static String getIdentityFileName(String type) {
+        return IdentityUtils.getIdentityFileName(ID_FILE_PREFIX, type, ID_FILE_SUFFIX);
+    }
+
+    /**
+     * @param strict        If {@code true} then files that do not have the required
+     *                      access rights are excluded from consideration
+     * @param supportedOnly If {@code true} then ignore identities that are not
+     *                      supported internally
+     * @param provider      A {@link FilePasswordProvider} - may be {@code null}
+     *                      if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+     *                      to {@link FilePasswordProvider#getPassword(String)} is the path of the
+     *                      file whose key is to be loaded
+     * @param options       The {@link LinkOption}s to apply when checking
+     *                      for existence
+     * @return A {@link KeyPair} for the identities - {@code null} if no identities
+     * available (e.g., after filtering unsupported ones or strict permissions)
+     * @throws IOException              If failed to access the file system
+     * @throws GeneralSecurityException If failed to load the keys
+     * @see PublicKeyEntry#getDefaultKeysFolderPath()
+     * @see #loadDefaultIdentities(Path, boolean, FilePasswordProvider, LinkOption...)
+     */
+    public static KeyPairProvider loadDefaultKeyPairProvider(
+            boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption... options)
+            throws IOException, GeneralSecurityException {
+        return loadDefaultKeyPairProvider(PublicKeyEntry.getDefaultKeysFolderPath(), strict, supportedOnly, provider, options);
+    }
+
+    /**
+     * @param dir           The folder to scan for the built-in identities
+     * @param strict        If {@code true} then files that do not have the required
+     *                      access rights are excluded from consideration
+     * @param supportedOnly If {@code true} then ignore identities that are not
+     *                      supported internally
+     * @param provider      A {@link FilePasswordProvider} - may be {@code null}
+     *                      if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+     *                      to {@link FilePasswordProvider#getPassword(String)} is the path of the
+     *                      file whose key is to be loaded
+     * @param options       The {@link LinkOption}s to apply when checking
+     *                      for existence
+     * @return A {@link KeyPair} for the identities - {@code null} if no identities
+     * available (e.g., after filtering unsupported ones or strict permissions)
+     * @throws IOException              If failed to access the file system
+     * @throws GeneralSecurityException If failed to load the keys
+     * @see #loadDefaultIdentities(Path, boolean, FilePasswordProvider, LinkOption...)
+     * @see IdentityUtils#createKeyPairProvider(Map, boolean)
+     */
+    public static KeyPairProvider loadDefaultKeyPairProvider(
+            Path dir, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption... options)
+            throws IOException, GeneralSecurityException {
+        Map<String, KeyPair> ids = loadDefaultIdentities(dir, strict, provider, options);
+        return IdentityUtils.createKeyPairProvider(ids, supportedOnly);
+    }
+
+    /**
+     * @param strict   If {@code true} then files that do not have the required
+     *                 access rights are excluded from consideration
+     * @param provider A {@link FilePasswordProvider} - may be {@code null}
+     *                 if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+     *                 to {@link FilePasswordProvider#getPassword(String)} is the path of the
+     *                 file whose key is to be loaded
+     * @param options  The {@link LinkOption}s to apply when checking
+     *                 for existence
+     * @return A {@link Map} of the found files where key=identity type (case
+     * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+     * @throws IOException              If failed to access the file system
+     * @throws GeneralSecurityException If failed to load the keys
+     * @see PublicKeyEntry#getDefaultKeysFolderPath()
+     * @see #loadDefaultIdentities(Path, boolean, FilePasswordProvider, LinkOption...)
+     */
+    public static Map<String, KeyPair> loadDefaultIdentities(boolean strict, FilePasswordProvider provider, LinkOption... options)
+            throws IOException, GeneralSecurityException {
+        return loadDefaultIdentities(PublicKeyEntry.getDefaultKeysFolderPath(), strict, provider, options);
+    }
+
+    /**
+     * @param dir      The folder to scan for the built-in identities
+     * @param strict   If {@code true} then files that do not have the required
+     *                 access rights are excluded from consideration
+     * @param provider A {@link FilePasswordProvider} - may be {@code null}
+     *                 if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+     *                 to {@link FilePasswordProvider#getPassword(String)} is the path of the
+     *                 file whose key is to be loaded
+     * @param options  The {@link LinkOption}s to apply when checking
+     *                 for existence
+     * @return A {@link Map} of the found files where key=identity type (case
+     * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+     * @throws IOException              If failed to access the file system
+     * @throws GeneralSecurityException If failed to load the keys
+     * @see #loadIdentities(Path, boolean, Collection, Function, FilePasswordProvider, LinkOption...)
+     * @see BuiltinIdentities
+     */
+    public static Map<String, KeyPair> loadDefaultIdentities(Path dir, boolean strict, FilePasswordProvider provider, LinkOption... options)
+            throws IOException, GeneralSecurityException {
+        return loadIdentities(dir, strict, BuiltinIdentities.NAMES, ID_GENERATOR, provider, options);
+    }
+
+    /**
+     * Scans a folder and loads all available identity files
+     *
+     * @param dir         The {@link Path} of the folder to scan - ignored if not exists
+     * @param strict      If {@code true} then files that do not have the required
+     *                    access rights are excluded from consideration
+     * @param types       The identity types - ignored if {@code null}/empty
+     * @param idGenerator A {@link Function} to derive the file name
+     *                    holding the specified type
+     * @param provider    A {@link FilePasswordProvider} - may be {@code null}
+     *                    if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+     *                    to {@link FilePasswordProvider#getPassword(String)} is the path of the
+     *                    file whose key is to be loaded
+     * @param options     The {@link LinkOption}s to apply when checking
+     *                    for existence
+     * @return A {@link Map} of the found files where key=identity type (case
+     * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+     * @throws IOException              If failed to access the file system
+     * @throws GeneralSecurityException If failed to load the keys
+     * @see #scanIdentitiesFolder(Path, boolean, Collection, Function, LinkOption...)
+     * @see IdentityUtils#loadIdentities(Map, FilePasswordProvider, java.nio.file.OpenOption...)
+     */
+    public static Map<String, KeyPair> loadIdentities(
+            Path dir, boolean strict, Collection<String> types, Function<String, String> idGenerator, FilePasswordProvider provider, LinkOption... options)
+            throws IOException, GeneralSecurityException {
+        Map<String, Path> paths = scanIdentitiesFolder(dir, strict, types, idGenerator, options);
+        return IdentityUtils.loadIdentities(paths, provider, IoUtils.EMPTY_OPEN_OPTIONS);
+    }
+
+    /**
+     * Scans a folder for possible identity files
+     *
+     * @param dir         The {@link Path} of the folder to scan - ignored if not exists
+     * @param strict      If {@code true} then files that do not have the required
+     *                    access rights are excluded from consideration
+     * @param types       The identity types - ignored if {@code null}/empty
+     * @param idGenerator A {@link Function} to derive the file name
+     *                    holding the specified type
+     * @param options     The {@link LinkOption}s to apply when checking
+     *                    for existence
+     * @return A {@link Map} of the found files where key=identity type (case
+     * <U>insensitive</U>), value=the {@link Path} of the file holding the key
+     * @throws IOException If failed to access the file system
+     * @see KeyUtils#validateStrictKeyFilePermissions(Path, LinkOption...)
+     */
+    public static Map<String, Path> scanIdentitiesFolder(
+            Path dir, boolean strict, Collection<String> types, Function<String, String> idGenerator, LinkOption... options)
+            throws IOException {
+        if (GenericUtils.isEmpty(types)) {
+            return Collections.emptyMap();
+        }
+
+        if (!Files.exists(dir, options)) {
+            return Collections.emptyMap();
+        }
+
+        ValidateUtils.checkTrue(FileInfoExtractor.ISDIR.infoOf(dir, options), "Not a directory: %s", dir);
+
+        Map<String, Path> paths = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (String t : types) {
+            String fileName = idGenerator.apply(t);
+            Path p = dir.resolve(fileName);
+            if (!Files.exists(p, options)) {
+                continue;
+            }
+
+            if (strict) {
+                if (KeyUtils.validateStrictKeyFilePermissions(p, options) != null) {
+                    continue;
+                }
+            }
+
+            Path prev = paths.put(t, p);
+            ValidateUtils.checkTrue(prev == null, "Multiple mappings for type=%s", t);
+        }
+
+        return paths;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
new file mode 100644
index 0000000..a00cb24
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
@@ -0,0 +1,141 @@
+/*
+ * 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.keys;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+
+/**
+ * A {@link ClientIdentityProvider} that watches a given key file re-loading
+ * its contents if it is ever modified, deleted or (re-)created
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ClientIdentityFileWatcher extends ModifiableFileWatcher implements ClientIdentityProvider {
+    private final AtomicReference<KeyPair> identityHolder = new AtomicReference<>(null);
+    private final Supplier<ClientIdentityLoader> loaderHolder;
+    private final Supplier<FilePasswordProvider> providerHolder;
+    private final boolean strict;
+
+    public ClientIdentityFileWatcher(Path path, ClientIdentityLoader loader, FilePasswordProvider provider) {
+        this(path, loader, provider, true);
+    }
+
+    public ClientIdentityFileWatcher(Path path, ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        this(path,
+             GenericUtils.supplierOf(Objects.requireNonNull(loader, "No client identity loader")),
+             GenericUtils.supplierOf(Objects.requireNonNull(provider, "No password provider")),
+             strict);
+    }
+
+    public ClientIdentityFileWatcher(Path path, Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider) {
+        this(path, loader, provider, true);
+    }
+
+    public ClientIdentityFileWatcher(Path path, Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        super(path);
+        this.loaderHolder = Objects.requireNonNull(loader, "No client identity loader");
+        this.providerHolder = Objects.requireNonNull(provider, "No password provider");
+        this.strict = strict;
+    }
+
+    public final boolean isStrict() {
+        return strict;
+    }
+
+    public final ClientIdentityLoader getClientIdentityLoader() {
+        return loaderHolder.get();
+    }
+
+    public final FilePasswordProvider getFilePasswordProvider() {
+        return providerHolder.get();
+    }
+
+    @Override
+    public KeyPair getClientIdentity() throws IOException, GeneralSecurityException {
+        if (checkReloadRequired()) {
+            KeyPair kp = identityHolder.getAndSet(null);     // start fresh
+            Path path = getPath();
+
+            if (exists()) {
+                KeyPair id = reloadClientIdentity(path);
+                if (!KeyUtils.compareKeyPairs(kp, id)) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("getClientIdentity({}) identity {}", path, (kp == null) ? "loaded" : "re-loaded");
+                    }
+                }
+
+                updateReloadAttributes();
+                identityHolder.set(id);
+            }
+        }
+
+        return identityHolder.get();
+    }
+
+    protected KeyPair reloadClientIdentity(Path path) throws IOException, GeneralSecurityException {
+        if (isStrict()) {
+            Map.Entry<String, Object> violation = KeyUtils.validateStrictKeyFilePermissions(path, IoUtils.EMPTY_LINK_OPTIONS);
+            if (violation != null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("reloadClientIdentity({}) ignore due to {}", path, violation.getKey());
+                }
+                return null;
+            }
+        }
+
+        String location = path.toString();
+        ClientIdentityLoader idLoader = Objects.requireNonNull(getClientIdentityLoader(), "No client identity loader");
+        if (idLoader.isValidLocation(location)) {
+            KeyPair kp = idLoader.loadClientIdentity(location, Objects.requireNonNull(getFilePasswordProvider(), "No file password provider"));
+            if (log.isTraceEnabled()) {
+                PublicKey key = (kp == null) ? null : kp.getPublic();
+                if (key != null) {
+                    log.trace("reloadClientIdentity({}) loaded {}-{}",
+                              location, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+
+                } else {
+                    log.trace("reloadClientIdentity({}) no key loaded", location);
+                }
+            }
+
+            return kp;
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("reloadClientIdentity({}) invalid location", location);
+        }
+
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
new file mode 100644
index 0000000..8b3a295
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
@@ -0,0 +1,95 @@
+/*
+ * 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.keys;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ClientIdentityLoader {
+    /**
+     * <P>A default implementation that assumes a file location that <U>must</U> exist.</P>
+     *
+     * <P>
+     * <B>Note:</B> It calls {@link SecurityUtils#loadKeyPairIdentity(String, InputStream, FilePasswordProvider)}
+     * </P>
+     */
+    ClientIdentityLoader DEFAULT = new ClientIdentityLoader() {
+        @Override
+        public boolean isValidLocation(String location) throws IOException {
+            Path path = toPath(location);
+            return Files.exists(path, IoUtils.EMPTY_LINK_OPTIONS);
+        }
+
+        @Override
+        public KeyPair loadClientIdentity(String location, FilePasswordProvider provider) throws IOException, GeneralSecurityException {
+            Path path = toPath(location);
+            try (InputStream inputStream = Files.newInputStream(path, IoUtils.EMPTY_OPEN_OPTIONS)) {
+                return SecurityUtils.loadKeyPairIdentity(path.toString(), inputStream, provider);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "DEFAULT";
+        }
+
+        private Path toPath(String location) {
+            Path path = Paths.get(ValidateUtils.checkNotNullAndNotEmpty(location, "No location"));
+            path = path.toAbsolutePath();
+            path = path.normalize();
+            return path;
+        }
+    };
+
+    /**
+     * @param location The identity key-pair location - the actual meaning (file, URL, etc.)
+     * depends on the implementation.
+     * @return {@code true} if it represents a valid location - the actual meaning of
+     * the validity depends on the implementation
+     * @throws IOException If failed to validate the location
+     */
+    boolean isValidLocation(String location) throws IOException;
+
+    /**
+     * @param location The identity key-pair location - the actual meaning (file, URL, etc.)
+     * depends on the implementation.
+     * @param provider The {@link FilePasswordProvider} to consult if the location contains
+     * an encrypted identity
+     * @return The loaded {@link KeyPair} - {@code null} if location is empty
+     * and it is OK that it does not exist
+     * @throws IOException If failed to access / process the remote location
+     * @throws GeneralSecurityException If failed to convert the contents into
+     * a valid identity
+     */
+    KeyPair loadClientIdentity(String location, FilePasswordProvider provider) throws IOException, GeneralSecurityException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityProvider.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityProvider.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityProvider.java
new file mode 100644
index 0000000..bda4700
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.keys;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface ClientIdentityProvider {
+    /**
+     * Provides a {@link KeyPair} representing the client identity
+     *
+     * @return The client identity - may be {@code null} if no currently
+     * available identity from this provider. <B>Note:</B> the provider
+     * may return a <U>different</U> value every time this method is called
+     * - e.g., if it is (re-)loading contents from a file.
+     * @throws IOException If failed to load the identity
+     * @throws GeneralSecurityException If failed to parse the identity
+     */
+    KeyPair getClientIdentity() throws IOException, GeneralSecurityException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/keys/DefaultClientIdentitiesWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/DefaultClientIdentitiesWatcher.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/DefaultClientIdentitiesWatcher.java
new file mode 100644
index 0000000..3afa129
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/DefaultClientIdentitiesWatcher.java
@@ -0,0 +1,66 @@
+/*
+ * 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.keys;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultClientIdentitiesWatcher extends BuiltinClientIdentitiesWatcher {
+    public DefaultClientIdentitiesWatcher(ClientIdentityLoader loader, FilePasswordProvider provider) {
+        this(loader, provider, true);
+    }
+
+    public DefaultClientIdentitiesWatcher(ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        this(true, loader, provider, strict);
+    }
+
+    public DefaultClientIdentitiesWatcher(boolean supportedOnly, ClientIdentityLoader loader, FilePasswordProvider provider, boolean strict) {
+        this(supportedOnly,
+             GenericUtils.supplierOf(Objects.requireNonNull(loader, "No client identity loader")),
+             GenericUtils.supplierOf(Objects.requireNonNull(provider, "No password provider")),
+             strict);
+    }
+
+    public DefaultClientIdentitiesWatcher(Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider) {
+        this(loader, provider, true);
+    }
+
+    public DefaultClientIdentitiesWatcher(Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        this(true, loader, provider, strict);
+    }
+
+    public DefaultClientIdentitiesWatcher(boolean supportedOnly,
+            Supplier<ClientIdentityLoader> loader, Supplier<FilePasswordProvider> provider, boolean strict) {
+        super(PublicKeyEntry.getDefaultKeysFolderPath(), supportedOnly, loader, provider, strict);
+    }
+
+    public static List<Path> getDefaultBuiltinIdentitiesPaths() {
+        return getDefaultBuiltinIdentitiesPaths(PublicKeyEntry.getDefaultKeysFolderPath());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/AttributeStore.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/AttributeStore.java b/sshd-common/src/main/java/org/apache/sshd/common/AttributeStore.java
new file mode 100644
index 0000000..96f6ab1
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/AttributeStore.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+/**
+ * Provides the capability to attach in-memory attributes to the entity
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface AttributeStore {
+    /**
+     * <P>
+     * Type safe key for storage of user attributes. Typically it is used as a static
+     * variable that is shared between the producer and the consumer. To further
+     * restrict access the setting or getting it from the store one can add static
+     * {@code get/set methods} e.g:
+     * </P>
+     *
+     * <pre>
+     * public static final AttributeKey&lt;MyValue&gt; MY_KEY = new AttributeKey&lt;MyValue&gt;();
+     *
+     * public static MyValue getMyValue(Session s) {
+     *   return s.getAttribute(MY_KEY);
+     * }
+     *
+     * public static void setMyValue(Session s, MyValue value) {
+     *   s.setAttribute(MY_KEY, value);
+     * }
+     * </pre>
+     *
+     * @param <T> type of value stored in the attribute.
+     * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+     */
+    // CHECKSTYLE:OFF
+    class AttributeKey<T> {
+        public AttributeKey() {
+            super();
+        }
+    }
+    // CHECKSTYLE:ON
+
+    /**
+     * Returns the value of the user-defined attribute.
+     *
+     * @param <T> The generic attribute type
+     * @param key The key of the attribute; must not be {@code null}.
+     * @return {@code null} if there is no value associated with the specified key
+     */
+    <T> T getAttribute(AttributeKey<T> key);
+
+    /**
+     * Sets a user-defined attribute.
+     *
+     * @param <T>   The generic attribute type
+     * @param key   The key of the attribute; must not be {@code null}.
+     * @param value The value of the attribute; must not be {@code null}.
+     * @return The old value of the attribute; {@code null} if it is new.
+     */
+    <T> T setAttribute(AttributeKey<T> key, T value);
+
+    /**
+     * Removes the user-defined attribute
+     *
+     * @param <T> The generic attribute type
+     * @param key The key of the attribute; must not be {@code null}.
+     * @return The removed value; {@code null} if no previous value
+     */
+    <T> T removeAttribute(AttributeKey<T> key);
+
+    /**
+     * Attempts to resolve the associated value by going up the store's
+     * hierarchy (if any)
+     *
+     * @param <T> The generic attribute type
+     * @param key The key of the attribute; must not be {@code null}.
+     * @return {@code null} if there is no value associated with the specified key
+     */
+    <T> T resolveAttribute(AttributeKey<T> key);
+}
+

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/BuiltinFactory.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/BuiltinFactory.java b/sshd-common/src/main/java/org/apache/sshd/common/BuiltinFactory.java
new file mode 100644
index 0000000..33b53a9
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/BuiltinFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * A named optional factory.
+ *
+ * @param <T> The create object instance type
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface BuiltinFactory<T> extends NamedFactory<T>, OptionalFeature {
+    static <T, E extends BuiltinFactory<T>> List<NamedFactory<T>> setUpFactories(
+            boolean ignoreUnsupported, Collection<? extends E> preferred) {
+        return GenericUtils.stream(preferred)
+                .filter(f -> ignoreUnsupported || f.isSupported())
+                .collect(Collectors.toList());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/Closeable.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/Closeable.java b/sshd-common/src/main/java/org/apache/sshd/common/Closeable.java
new file mode 100644
index 0000000..6a6f622
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/Closeable.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.nio.channels.Channel;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.SshFutureListener;
+
+/**
+ * A {@code Closeable} is a resource that can be closed.
+ * The close method is invoked to release resources that the object is
+ * holding. The user can pre-register listeners to be notified
+ * when resource close is completed (successfully or otherwise)
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface Closeable extends Channel {
+
+    /**
+     * Timeout (milliseconds) for waiting on a {@link CloseFuture} to successfully
+     * complete its action.
+     * @see #DEFAULT_CLOSE_WAIT_TIMEOUT
+     */
+    String CLOSE_WAIT_TIMEOUT = "sshd-close-wait-time";
+
+    /**
+     * Default value for {@link #CLOSE_WAIT_TIMEOUT} if none specified
+     */
+    long DEFAULT_CLOSE_WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(15L);
+
+    /**
+     * Close this resource asynchronously and return a future.
+     * Resources support two closing modes: a graceful mode
+     * which will cleanly close the resource and an immediate mode
+     * which will close the resources abruptly.
+     *
+     * @param immediately <code>true</code> if the resource should be shut down abruptly,
+     *                    <code>false</code> for a graceful close
+     * @return a {@link CloseFuture} representing the close request
+     */
+    CloseFuture close(boolean immediately);
+
+    /**
+     * Pre-register a listener to be informed when resource is closed. If
+     * resource is already closed, the listener will be invoked immediately
+     * and not registered for future notification
+     *
+     * @param listener The notification {@link SshFutureListener} - never {@code null}
+     */
+    void addCloseFutureListener(SshFutureListener<CloseFuture> listener);
+
+    /**
+     * Remove a pre-registered close event listener
+     *
+     * @param listener The register {@link SshFutureListener} - never {@code null}.
+     * Ignored if not registered or resource already closed
+     */
+    void removeCloseFutureListener(SshFutureListener<CloseFuture> listener);
+
+    /**
+     * Returns <code>true</code> if this object has been closed.
+     *
+     * @return <code>true</code> if closing
+     */
+    boolean isClosed();
+
+    /**
+     * Returns <code>true</code> if the {@link #close(boolean)} method
+     * has been called. Note that this method will return <code>true</code>
+     * even if this {@link #isClosed()} returns <code>true</code>.
+     *
+     * @return <code>true</code> if closing
+     */
+    boolean isClosing();
+
+    @Override
+    default boolean isOpen() {
+        return !(isClosed() || isClosing());
+    }
+
+    @Override
+    default void close() throws IOException {
+        Closeable.close(this);
+    }
+
+    static long getMaxCloseWaitTime(PropertyResolver resolver) {
+        return (resolver == null) ? DEFAULT_CLOSE_WAIT_TIMEOUT
+                : resolver.getLongProperty(CLOSE_WAIT_TIMEOUT, DEFAULT_CLOSE_WAIT_TIMEOUT);
+    }
+
+    static void close(Closeable closeable) throws IOException {
+        if (closeable == null) {
+            return;
+        }
+
+        if ((!closeable.isClosed()) && (!closeable.isClosing())) {
+            CloseFuture future = closeable.close(true);
+            long maxWait = (closeable instanceof PropertyResolver)
+                    ? getMaxCloseWaitTime((PropertyResolver) closeable) : DEFAULT_CLOSE_WAIT_TIMEOUT;
+            boolean successful = future.await(maxWait);
+            if (!successful) {
+                throw new SocketTimeoutException("Failed to receive closure confirmation within " + maxWait + " millis");
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/Factory.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/Factory.java b/sshd-common/src/main/java/org/apache/sshd/common/Factory.java
new file mode 100644
index 0000000..93b351c
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/Factory.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+import java.util.function.Supplier;
+
+/**
+ * Factory is a simple interface that is used to create other objects.
+ *
+ * @param <T> type of object this factory will create
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface Factory<T> extends Supplier<T> {
+
+    @Override
+    default T get() {
+        return create();
+    }
+
+    /**
+     * @return A new instance
+     */
+    T create();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/NamedFactory.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/NamedFactory.java b/sshd-common/src/main/java/org/apache/sshd/common/NamedFactory.java
new file mode 100644
index 0000000..5386447
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/NamedFactory.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * A named factory is a factory identified by a name.
+ * Such names are used mainly in the algorithm negotiation at the beginning of the SSH connection.
+ *
+ * @param <T> The create object instance type
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface NamedFactory<T> extends Factory<T>, NamedResource {
+    /**
+     * Create an instance of the specified name by looking up the needed factory
+     * in the list.
+     *
+     * @param factories list of available factories
+     * @param name      the factory name to use
+     * @param <T>       type of object to create
+     * @return a newly created object or {@code null} if the factory is not in the list
+     */
+    static <T> T create(Collection<? extends NamedFactory<? extends T>> factories, String name) {
+        NamedFactory<? extends T> f = NamedResource.findByName(name, String.CASE_INSENSITIVE_ORDER, factories);
+        if (f != null) {
+            return f.create();
+        } else {
+            return null;
+        }
+    }
+
+    static <S extends OptionalFeature, T, E extends NamedFactory<T>> List<NamedFactory<T>> setUpTransformedFactories(
+            boolean ignoreUnsupported, Collection<? extends S> preferred, Function<? super S, ? extends E> xform) {
+        return preferred.stream()
+            .filter(f -> ignoreUnsupported || f.isSupported())
+            .map(xform)
+            .collect(Collectors.toList());
+    }
+
+    static <T, E extends NamedFactory<T> & OptionalFeature> List<NamedFactory<T>> setUpBuiltinFactories(
+            boolean ignoreUnsupported, Collection<? extends E> preferred) {
+        return preferred.stream()
+            .filter(f -> ignoreUnsupported || f.isSupported())
+            .collect(Collectors.toList());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/NamedResource.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/NamedResource.java b/sshd-common/src/main/java/org/apache/sshd/common/NamedResource.java
new file mode 100644
index 0000000..813f53d
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/NamedResource.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface NamedResource {
+
+    /**
+     * Returns the value of {@link #getName()} - or {@code null} if argument is {@code null}
+     */
+    Function<NamedResource, String> NAME_EXTRACTOR = input -> input == null ? null : input.getName();
+
+    /**
+     * Compares 2 {@link NamedResource}s according to their {@link #getName()}
+     * value case <U>insensitive</U>
+     */
+    Comparator<NamedResource> BY_NAME_COMPARATOR = Comparator.comparing(NAME_EXTRACTOR, String.CASE_INSENSITIVE_ORDER);
+
+    /**
+     * @return The resource name
+     */
+    String getName();
+
+    /**
+     * @param resources The named resources
+     * @return A {@link List} of all the factories names - in same order
+     * as they appear in the input collection
+     */
+    static List<String> getNameList(Collection<? extends NamedResource> resources) {
+        return GenericUtils.map(resources, NamedResource::getName);
+    }
+
+    /**
+     * @param resources list of available resources
+     * @return A comma separated list of factory names
+     */
+    static String getNames(Collection<? extends NamedResource> resources) {
+        Collection<String> nameList = getNameList(resources);
+        return GenericUtils.join(nameList, ',');
+    }
+
+    /**
+     * Remove the resource identified by the name from the list.
+     *
+     * @param <R>       The generic resource type
+     * @param name      Name of the resource - ignored if {@code null}/empty
+     * @param c         The {@link Comparator} to decide whether the {@link NamedResource#getName()}
+     *                  matches the <tt>name</tt> parameter
+     * @param resources The {@link NamedResource} to check - ignored if {@code null}/empty
+     * @return the removed resource from the list or {@code null} if not in the list
+     */
+    static <R extends NamedResource> R removeByName(String name, Comparator<? super String> c, Collection<? extends R> resources) {
+        R r = findByName(name, c, resources);
+        if (r != null) {
+            resources.remove(r);
+        }
+        return r;
+    }
+
+    /**
+     * @param <R>       The generic resource type
+     * @param name      Name of the resource - ignored if {@code null}/empty
+     * @param c         The {@link Comparator} to decide whether the {@link NamedResource#getName()}
+     *                  matches the <tt>name</tt> parameter
+     * @param resources The {@link NamedResource} to check - ignored if {@code null}/empty
+     * @return The <U>first</U> resource whose name matches the parameter (by invoking
+     * {@link Comparator#compare(Object, Object)} - {@code null} if no match found
+     */
+    static <R extends NamedResource> R findByName(String name, Comparator<? super String> c, Collection<? extends R> resources) {
+        return GenericUtils.isEmpty(name)
+            ? null
+            : GenericUtils.stream(resources)
+                .filter(r -> c.compare(name, r.getName()) == 0)
+                .findFirst()
+                .orElse(null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/OptionalFeature.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/OptionalFeature.java b/sshd-common/src/main/java/org/apache/sshd/common/OptionalFeature.java
new file mode 100644
index 0000000..1afa864
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/OptionalFeature.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import java.util.Collection;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface OptionalFeature {
+    OptionalFeature TRUE = new OptionalFeature() {
+        @Override
+        public boolean isSupported() {
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "TRUE";
+        }
+    };
+
+    OptionalFeature FALSE = new OptionalFeature() {
+        @Override
+        public boolean isSupported() {
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "FALSE";
+        }
+    };
+
+    boolean isSupported();
+
+    static OptionalFeature of(boolean supported) {
+        return supported ? TRUE : FALSE;
+    }
+
+    static OptionalFeature all(Collection<? extends OptionalFeature> features) {
+        return () -> {
+            if (GenericUtils.isEmpty(features)) {
+                return false;
+            }
+
+            for (OptionalFeature f : features) {
+                if (!f.isSupported()) {
+                    return false;
+                }
+            }
+
+            return true;
+        };
+    }
+
+    static OptionalFeature any(Collection<? extends OptionalFeature> features) {
+        return () -> {
+            if (GenericUtils.isEmpty(features)) {
+                return false;
+            }
+
+            for (OptionalFeature f : features) {
+                if (f.isSupported()) {
+                    return true;
+                }
+            }
+
+            return false;
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
new file mode 100644
index 0000000..c333c7f
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Indicates an entity that can be configured using properties. The properties
+ * are simple name-value pairs where the actual value type depends on the
+ * property. Some automatic conversions may be available - e.g., from a string
+ * to a numeric or {@code boolean} value, or from {@code int} to {@code long},
+ * etc.. <B>Note:</B> implementations may decide to use case <U>insensitive</U>
+ * property names, therefore it is <U><B>highly discouraged</B></U> to use names
+ * that differ from each other only in case sensitivity. Also, implementations
+ * may choose to trim whitespaces, thus such are also highly discouraged.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PropertyResolver {
+    /**
+     * An &quot;empty&quot; resolver with no properties and no parent
+     */
+    PropertyResolver EMPTY = new PropertyResolver() {
+        @Override
+        public PropertyResolver getParentPropertyResolver() {
+            return null;
+        }
+
+        @Override
+        public Map<String, Object> getProperties() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public String toString() {
+            return "EMPTY";
+        }
+    };
+
+    /**
+     * @return The parent resolver that can be used to query for missing
+     *         properties - {@code null} if no parent
+     */
+    PropertyResolver getParentPropertyResolver();
+
+    /**
+     * <P>
+     * A map of properties that can be used to configure the SSH server or
+     * client. This map will never be changed by either the server or client and
+     * is not supposed to be changed at runtime (changes are not bound to have
+     * any effect on a running client or server), though it may affect the
+     * creation of sessions later as these values are usually not cached.
+     * </P>
+     *
+     * <P>
+     * <B>Note:</B> the <U>type</U> of the mapped property should match the
+     * expected configuration value type - {@code Long, Integer, Boolean,
+     * String}, etc.... If it doesn't, the {@code toString()} result of the
+     * mapped value is used to convert it to the required type. E.g., if the
+     * mapped value is the <U>string</U> &quot;1234&quot; and the expected value
+     * is a {@code long} then it will be parsed into one. Also, if the mapped
+     * value is an {@code Integer} but a {@code long} is expected, then it will
+     * be converted into one.
+     * </P>
+     *
+     * @return a valid <code>Map</code> containing configuration values, never
+     *         {@code null}
+     */
+    Map<String, Object> getProperties();
+
+    default long getLongProperty(String name, long def) {
+        return PropertyResolverUtils.getLongProperty(this, name, def);
+    }
+
+    default Long getLong(String name) {
+        return PropertyResolverUtils.getLong(this, name);
+    }
+
+    default int getIntProperty(String name, int def) {
+        return PropertyResolverUtils.getIntProperty(this, name, def);
+    }
+
+    default Integer getInteger(String name) {
+        return PropertyResolverUtils.getInteger(this, name);
+    }
+
+    default boolean getBooleanProperty(String name, boolean def) {
+        return PropertyResolverUtils.getBooleanProperty(this, name, def);
+    }
+
+    default Boolean getBoolean(String name) {
+        return PropertyResolverUtils.getBoolean(this, name);
+    }
+
+    default String getStringProperty(String name, String def) {
+        return PropertyResolverUtils.getStringProperty(this, name, def);
+    }
+
+    default String getString(String name) {
+        return PropertyResolverUtils.getString(this, name);
+    }
+
+    default Object getObject(String name) {
+        return PropertyResolverUtils.getObject(this, name);
+    }
+}