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:56 UTC

[46/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/common/config/VersionProperties.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/VersionProperties.java b/sshd-common/src/main/java/org/apache/sshd/common/config/VersionProperties.java
new file mode 100644
index 0000000..0e351a8
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/VersionProperties.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.NavigableMap;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class VersionProperties {
+    private static final class LazyVersionPropertiesHolder {
+        private static final NavigableMap<String, String> PROPERTIES =
+            Collections.unmodifiableNavigableMap(loadVersionProperties(LazyVersionPropertiesHolder.class));
+
+        private LazyVersionPropertiesHolder() {
+            throw new UnsupportedOperationException("No instance allowed");
+        }
+
+        private static NavigableMap<String, String> loadVersionProperties(Class<?> anchor) {
+            return loadVersionProperties(anchor, ThreadUtils.resolveDefaultClassLoader(anchor));
+        }
+
+        private static NavigableMap<String, String> loadVersionProperties(Class<?> anchor, ClassLoader loader) {
+            NavigableMap<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+            try {
+                InputStream input = loader.getResourceAsStream("org/apache/sshd/sshd-version.properties");
+                if (input == null) {
+                    throw new FileNotFoundException("Version resource does not exist");
+                }
+
+                Properties props = new Properties();
+                try {
+                    props.load(input);
+                } finally {
+                    input.close();
+                }
+
+                for (String key : props.stringPropertyNames()) {
+                    String propValue = props.getProperty(key);
+                    String value = GenericUtils.trimToEmpty(propValue);
+                    if (GenericUtils.isEmpty(value)) {
+                        continue;   // we have no need for empty values
+                    }
+
+                    String prev = result.put(key, value);
+                    if (prev != null) {
+                        Logger log = LoggerFactory.getLogger(anchor);
+                        log.warn("Multiple values for key=" + key + ": current=" + value + ", previous=" + prev);
+                    }
+                }
+            } catch (Exception e) {
+                Logger log = LoggerFactory.getLogger(anchor);
+                log.warn("Failed (" + e.getClass().getSimpleName() + ") to load version properties: " + e.getMessage());
+            }
+
+            return result;
+        }
+    }
+
+    private VersionProperties() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    /**
+     * @return A case <u>insensitive</u> un-modifiable {@link NavigableMap} of the {@code sshd-version.properties} data
+     */
+    @SuppressWarnings("synthetic-access")
+    public static NavigableMap<String, String> getVersionProperties() {
+        return LazyVersionPropertiesHolder.PROPERTIES;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
new file mode 100644
index 0000000..c03616c
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
@@ -0,0 +1,480 @@
+/*
+ * 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.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+
+/**
+ * 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>
+ * @see <A HREF="http://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT">sshd(8) - AUTHORIZED_KEYS_FILE_FORMAT</A>
+ */
+public class AuthorizedKeyEntry extends PublicKeyEntry {
+    public static final char BOOLEAN_OPTION_NEGATION_INDICATOR = '!';
+
+    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;
+            // Cannot use forEach because the index value is not effectively final
+            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   // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override   // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
+    public boolean equals(Object obj) {
+        return super.equals(obj);
+    }
+
+    @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 List<PublicKey> resolveAuthorizedKeys(
+            PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
+                    throws IOException, GeneralSecurityException {
+        if (GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        }
+
+        List<PublicKey> keys = new ArrayList<>(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);
+                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 value 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 value) throws IllegalArgumentException {
+        String line = GenericUtils.replaceWhitespaceAndTrim(value);
+        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);
+        AuthorizedKeyEntry entry;
+        // assume this is due to the fact that it starts with login options
+        if (decoder == null) {
+            Map.Entry<String, String> comps = resolveEntryComponents(line);
+            entry = parseAuthorizedKeyEntry(comps.getValue());
+            ValidateUtils.checkTrue(entry != null, "Bad format (no key data after login options): %s", line);
+            entry.setLoginOptions(parseLoginOptions(comps.getKey()));
+        } 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;
+    }
+
+    /**
+     * Parses a single line from an <code>authorized_keys</code> file that is <U>known</U>
+     * to contain login options and separates it to the options and the rest of the line.
+     *
+     * @param entryLine The line to be parsed
+     * @return A {@link SimpleImmutableEntry} representing the parsed data where key=login options part
+     * and value=rest of the data - {@code null} if no data in line or line starts with comment character
+     * @see <A HREF="http://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT">sshd(8) - AUTHORIZED_KEYS_FILE_FORMAT</A>
+     */
+    public static SimpleImmutableEntry<String, String> resolveEntryComponents(String entryLine) {
+        String line = GenericUtils.replaceWhitespaceAndTrim(entryLine);
+        if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
+            return null;
+        }
+
+        for (int lastPos = 0; lastPos < line.length();) {
+            int startPos = line.indexOf(' ', lastPos);
+            if (startPos < lastPos) {
+                throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+            }
+
+            int quotePos = line.indexOf('"', startPos + 1);
+            // If found quotes after the space then assume part of a login option
+            if (quotePos > startPos) {
+                lastPos = quotePos + 1;
+                continue;
+            }
+
+            String loginOptions = line.substring(0, startPos).trim();
+            String remainder = line.substring(startPos + 1).trim();
+            return new SimpleImmutableEntry<>(loginOptions, remainder);
+        }
+
+        throw new IllegalArgumentException("Bad format (no key data contents): " + line);
+    }
+
+    /**
+     * <P>
+     * Parses login options line according to
+     * <A HREF="http://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT">sshd(8) - AUTHORIZED_KEYS_FILE_FORMAT</A>
+     * guidelines. <B>Note:</B>
+     * </P>
+     *
+     * <UL>
+     *      <P><LI>
+     *      Options that have a value are automatically stripped of any surrounding double quotes./
+     *      </LI></P>
+     *
+     *      <P><LI>
+     *      Options that have no value are marked as {@code true/false} - according
+     *      to the {@link #BOOLEAN_OPTION_NEGATION_INDICATOR}.
+     *      </LI></P>
+     *
+     *      <P><LI>
+     *      Options that appear multiple times are simply concatenated using comma as separator.
+     *      </LI></P>
+     * </UL>
+     *
+     * @param options The options line to parse - ignored if {@code null}/empty/blank
+     * @return A {@link NavigableMap} where key=case <U>insensitive</U> option name and value=the parsed value.
+     * @see #addLoginOption(Map, String) addLoginOption
+     */
+    public static NavigableMap<String, String> parseLoginOptions(String options) {
+        String line = GenericUtils.replaceWhitespaceAndTrim(options);
+        int len = GenericUtils.length(line);
+        if (len <= 0) {
+            return Collections.emptyNavigableMap();
+        }
+
+        NavigableMap<String, String> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        int lastPos = 0;
+        for (int curPos = 0; curPos < len; curPos++) {
+            int nextPos = line.indexOf(',', curPos);
+            if (nextPos < curPos) {
+                break;
+            }
+
+            // check if "true" comma or one inside quotes
+            int quotePos = line.indexOf('"', curPos);
+            if ((quotePos >= lastPos) && (quotePos < nextPos)) {
+                nextPos = line.indexOf('"', quotePos + 1);
+                if (nextPos <= quotePos) {
+                    throw new IllegalArgumentException("Bad format (imbalanced quoted command): " + line);
+                }
+
+                // Make sure either comma or no more options follow the 2nd quote
+                for (nextPos++; nextPos < len; nextPos++) {
+                    char ch = line.charAt(nextPos);
+                    if (ch == ',') {
+                        break;
+                    }
+
+                    if (ch != ' ') {
+                        throw new IllegalArgumentException("Bad format (incorrect list format): " + line);
+                    }
+                }
+            }
+
+            addLoginOption(optsMap, line.substring(lastPos, nextPos));
+            lastPos = nextPos + 1;
+            curPos = lastPos;
+        }
+
+        // Any leftovers at end of line ?
+        if (lastPos < len) {
+            addLoginOption(optsMap, line.substring(lastPos));
+        }
+
+        return optsMap;
+    }
+
+    /**
+     * Parses and adds a new option to the options map. If a valued option is re-specified then
+     * its value(s) are concatenated using comma as separator.
+     *
+     * @param optsMap Options map to add to
+     * @param option The option data to parse - ignored if {@code null}/empty/blank
+     * @return The updated entry - {@code null} if no option updated in the map
+     * @throws IllegalStateException If a boolean option is re-specified
+     */
+    public static SimpleImmutableEntry<String, String> addLoginOption(Map<String, String> optsMap, String option) {
+        String p = GenericUtils.trimToEmpty(option);
+        if (GenericUtils.isEmpty(p)) {
+            return null;
+        }
+
+        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.toString(name.charAt(0) != BOOLEAN_OPTION_NEGATION_INDICATOR);
+        }
+
+        SimpleImmutableEntry<String, String> entry = new SimpleImmutableEntry<>(name, value.toString());
+        String prev = optsMap.put(entry.getKey(), entry.getValue());
+        if (prev != null) {
+            if (pos < 0) {
+                throw new IllegalStateException("Bad format (boolean option (" + name + ") re-specified): " + p);
+            }
+            optsMap.put(entry.getKey(), prev + "," + entry.getValue());
+        }
+
+        return entry;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java
new file mode 100644
index 0000000..70e5c8b
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java
@@ -0,0 +1,212 @@
+/*
+ * 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.security.Key;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum BuiltinIdentities implements Identity {
+    RSA(Constants.RSA, RSAPublicKey.class, RSAPrivateKey.class),
+    DSA(Constants.DSA, DSAPublicKey.class, DSAPrivateKey.class),
+    ECDSA(Constants.ECDSA, KeyUtils.EC_ALGORITHM, ECPublicKey.class, ECPrivateKey.class) {
+        @Override
+        public boolean isSupported() {
+            return SecurityUtils.isECCSupported();
+        }
+    },
+    ED25119(Constants.ED25519, SecurityUtils.EDDSA, SecurityUtils.getEDDSAPublicKeyType(), SecurityUtils.getEDDSAPrivateKeyType()) {
+        @Override
+        public boolean isSupported() {
+            return SecurityUtils.isEDDSACurveSupported();
+        }
+    };
+
+    public static final Set<BuiltinIdentities> VALUES =
+        Collections.unmodifiableSet(EnumSet.allOf(BuiltinIdentities.class));
+
+    public static final Set<String> NAMES =
+        Collections.unmodifiableSet(
+            GenericUtils.asSortedSet(
+                String.CASE_INSENSITIVE_ORDER, NamedResource.getNameList(VALUES)));
+
+    private final String name;
+    private final String algorithm;
+    private final Class<? extends PublicKey> pubType;
+    private final Class<? extends PrivateKey> prvType;
+
+    BuiltinIdentities(String type, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
+        this(type, type, pubType, prvType);
+    }
+
+    BuiltinIdentities(String name, String algorithm, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
+        this.name = name.toLowerCase();
+        this.algorithm = algorithm.toUpperCase();
+        this.pubType = pubType;
+        this.prvType = prvType;
+    }
+
+    @Override
+    public final String getName() {
+        return name;
+    }
+
+    @Override
+    public boolean isSupported() {
+        return true;
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return algorithm;
+    }
+
+    @Override
+    public final Class<? extends PublicKey> getPublicKeyType() {
+        return pubType;
+    }
+
+    @Override
+    public final Class<? extends PrivateKey> getPrivateKeyType() {
+        return prvType;
+    }
+
+    /**
+     * @param name The identity name - ignored if {@code null}/empty
+     * @return The matching {@link BuiltinIdentities} whose {@link #getName()}
+     * value matches case <U>insensitive</U> or {@code null} if no match found
+     */
+    public static BuiltinIdentities fromName(String name) {
+        return NamedResource.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+    }
+
+    /**
+     * @param algorithm The algorithm  - ignored if {@code null}/empty
+     * @return The matching {@link BuiltinIdentities} whose {@link #getAlgorithm()}
+     * value matches case <U>insensitive</U> or {@code null} if no match found
+     */
+    public static BuiltinIdentities fromAlgorithm(String algorithm) {
+        if (GenericUtils.isEmpty(algorithm)) {
+            return null;
+        }
+
+        for (BuiltinIdentities id : VALUES) {
+            if (algorithm.equalsIgnoreCase(id.getAlgorithm())) {
+                return id;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @param kp The {@link KeyPair} - ignored if {@code null}
+     * @return The matching {@link BuiltinIdentities} provided <U>both</U>
+     * public and public keys are of the same type - {@code null} if no
+     * match could be found
+     * @see #fromKey(Key)
+     */
+    public static BuiltinIdentities fromKeyPair(KeyPair kp) {
+        if (kp == null) {
+            return null;
+        }
+
+        BuiltinIdentities i1 = fromKey(kp.getPublic());
+        BuiltinIdentities i2 = fromKey(kp.getPrivate());
+        if (Objects.equals(i1, i2)) {
+            return i1;
+        } else {
+            return null;    // some kind of mixed keys...
+        }
+    }
+
+    /**
+     * @param key The {@link Key} instance - ignored if {@code null}
+     * @return The matching {@link BuiltinIdentities} whose either public or
+     * private key type matches the requested one or {@code null} if no match found
+     * @see #fromKeyType(Class)
+     */
+    public static BuiltinIdentities fromKey(Key key) {
+        return fromKeyType((key == null) ? null : key.getClass());
+    }
+
+    /**
+     * @param clazz The key type - ignored if {@code null} or not
+     *              a {@link Key} class
+     * @return The matching {@link BuiltinIdentities} whose either public or
+     * private key type matches the requested one or {@code null} if no match found
+     * @see #getPublicKeyType()
+     * @see #getPrivateKeyType()
+     */
+    public static BuiltinIdentities fromKeyType(Class<?> clazz) {
+        if ((clazz == null) || (!Key.class.isAssignableFrom(clazz))) {
+            return null;
+        }
+
+        for (BuiltinIdentities id : VALUES) {
+            Class<?> pubType = id.getPublicKeyType();
+            Class<?> prvType = id.getPrivateKeyType();
+            // Ignore placeholder classes (e.g., if ed25519 is not supported)
+            if ((prvType == null) || (pubType == null)) {
+                continue;
+            }
+            if ((prvType == PrivateKey.class) || (pubType == PublicKey.class)) {
+                continue;
+            }
+            if (pubType.isAssignableFrom(clazz) || prvType.isAssignableFrom(clazz)) {
+                return id;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Contains the names of the identities
+     */
+    public static final class Constants {
+        public static final String RSA = KeyUtils.RSA_ALGORITHM;
+        public static final String DSA = KeyUtils.DSS_ALGORITHM;
+        public static final String ECDSA = "ECDSA";
+        public static final String ED25519 = "ED25519";
+
+        private Constants() {
+            throw new UnsupportedOperationException("No instance allowed");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
new file mode 100644
index 0000000..064f75c
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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.IOException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface FilePasswordProvider {
+    /**
+     * An &quot;empty&quot; provider that returns {@code null} - i.e., unprotected key file
+     */
+    FilePasswordProvider EMPTY = resourceKey -> null;
+
+    /**
+     * @param resourceKey The resource key representing the <U>private</U>
+     *                    file
+     * @return The password - if {@code null}/empty then no password is required
+     * @throws IOException if cannot resolve password
+     */
+    String getPassword(String resourceKey) throws IOException;
+
+    static FilePasswordProvider of(String password) {
+        return r -> password;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/Identity.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/Identity.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/Identity.java
new file mode 100644
index 0000000..eaec413
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/Identity.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.common.config.keys;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.OptionalFeature;
+
+/**
+ * Represents an SSH key type
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface Identity extends NamedResource, OptionalFeature {
+    /**
+     * @return The key algorithm - e.g., RSA, DSA, EC
+     */
+    String getAlgorithm();
+
+    Class<? extends PublicKey> getPublicKeyType();
+
+    Class<? extends PrivateKey> getPrivateKeyType();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityResourceLoader.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityResourceLoader.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityResourceLoader.java
new file mode 100644
index 0000000..d826821
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityResourceLoader.java
@@ -0,0 +1,49 @@
+/*
+ * 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.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Collection;
+
+/**
+ * @param <PUB> Type of {@link PublicKey}
+ * @param <PRV> Type of {@link PrivateKey}
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface IdentityResourceLoader<PUB extends PublicKey, PRV extends PrivateKey> {
+    /**
+     * @return The {@link Class} of the {@link PublicKey} that is the result
+     * of decoding
+     */
+    Class<PUB> getPublicKeyType();
+
+    /**
+     * @return The {@link Class} of the {@link PrivateKey} that matches the
+     * public one
+     */
+    Class<PRV> getPrivateKeyType();
+
+    /**
+     * @return The {@link Collection} of {@code OpenSSH} key type names that
+     * are supported by this decoder - e.g., ECDSA keys have several curve names.
+     * <B>Caveat:</B> this collection may be un-modifiable...
+     */
+    Collection<String> getSupportedTypeNames();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
new file mode 100644
index 0000000..fbc3ce7
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
@@ -0,0 +1,159 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class IdentityUtils {
+    private IdentityUtils() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    private static final class LazyDefaultUserHomeFolderHolder {
+        private static final Path PATH =
+            Paths.get(ValidateUtils.checkNotNullAndNotEmpty(System.getProperty("user.home"), "No user home"))
+                .toAbsolutePath()
+                .normalize();
+
+        private LazyDefaultUserHomeFolderHolder() {
+            throw new UnsupportedOperationException("No instance allowed");
+        }
+    }
+
+    /**
+     * @return The {@link Path} to the currently running user home
+     */
+    @SuppressWarnings("synthetic-access")
+    public static Path getUserHomeFolder() {
+        return LazyDefaultUserHomeFolderHolder.PATH;
+    }
+
+    /**
+     * @param prefix The file name prefix - ignored if {@code null}/empty
+     * @param type   The identity type - ignored if {@code null}/empty
+     * @param suffix The file name suffix - ignored if {@code null}/empty
+     * @return The identity file name or {@code null} if no name
+     */
+    public static String getIdentityFileName(String prefix, String type, String suffix) {
+        if (GenericUtils.isEmpty(type)) {
+            return null;
+        } else {
+            return GenericUtils.trimToEmpty(prefix)
+                    + type.toLowerCase() + GenericUtils.trimToEmpty(suffix);
+        }
+    }
+
+    /**
+     * @param ids           A {@link Map} of the loaded identities where key=the identity type,
+     *                      value=the matching {@link KeyPair} - ignored if {@code null}/empty
+     * @param supportedOnly If {@code true} then ignore identities that are not
+     *                      supported internally
+     * @return A {@link KeyPair} for the identities - {@code null} if no identities
+     * available (e.g., after filtering unsupported ones)
+     * @see BuiltinIdentities
+     */
+    public static KeyPairProvider createKeyPairProvider(Map<String, KeyPair> ids, boolean supportedOnly) {
+        if (GenericUtils.isEmpty(ids)) {
+            return null;
+        }
+
+        Map<String, KeyPair> pairsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        ids.forEach((type, kp) -> {
+            BuiltinIdentities id = BuiltinIdentities.fromName(type);
+            if (id == null) {
+                id = BuiltinIdentities.fromKeyPair(kp);
+            }
+
+            if (supportedOnly && ((id == null) || (!id.isSupported()))) {
+                return;
+            }
+
+            String keyType = KeyUtils.getKeyType(kp);
+            if (GenericUtils.isEmpty(keyType)) {
+                return;
+            }
+
+            KeyPair prev = pairsMap.put(keyType, kp);
+            if (prev != null) {
+                return;   // less of an offense if 2 pairs mapped to same key type
+            }
+        });
+
+        if (GenericUtils.isEmpty(pairsMap)) {
+            return null;
+        } else {
+            return new MappedKeyPairProvider(pairsMap);
+        }
+    }
+
+    /**
+     * @param paths    A {@link Map} of the identities where key=identity type (case
+     *                 <U>insensitive</U>), value=the {@link Path} of file with the identity key
+     * @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 OpenOption}s to use when reading the key data
+     * @return A {@link Map} of the identities 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 SecurityUtils#loadKeyPairIdentity(String, InputStream, FilePasswordProvider)
+     */
+    public static Map<String, KeyPair> loadIdentities(Map<String, ? extends Path> paths, FilePasswordProvider provider, OpenOption... options)
+            throws IOException, GeneralSecurityException {
+        if (GenericUtils.isEmpty(paths)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, KeyPair> ids = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        // Cannot use forEach because the potential for IOExceptions being thrown
+        for (Map.Entry<String, ? extends Path> pe : paths.entrySet()) {
+            String type = pe.getKey();
+            Path path = pe.getValue();
+            try (InputStream inputStream = Files.newInputStream(path, options)) {
+                KeyPair kp = SecurityUtils.loadKeyPairIdentity(path.toString(), inputStream, provider);
+                KeyPair prev = ids.put(type, kp);
+                ValidateUtils.checkTrue(prev == null, "Multiple keys for type=%s", type);
+            }
+        }
+
+        return ids;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
new file mode 100644
index 0000000..4bfbea0
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
@@ -0,0 +1,190 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * @param <PUB> Type of {@link PublicKey}
+ * @param <PRV> Type of {@link PrivateKey}
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface KeyEntryResolver<PUB extends PublicKey, PRV extends PrivateKey> extends IdentityResourceLoader<PUB, PRV> {
+    /**
+     * @param keySize Key size in bits
+     * @return A {@link KeyPair} with the specified key size
+     * @throws GeneralSecurityException if unable to generate the pair
+     */
+    default KeyPair generateKeyPair(int keySize) throws GeneralSecurityException {
+        KeyPairGenerator gen = getKeyPairGenerator();
+        gen.initialize(keySize);
+        return gen.generateKeyPair();
+    }
+
+    /**
+     * @param kp The {@link KeyPair} to be cloned - ignored if {@code null}
+     * @return A cloned pair (or {@code null} if no original pair)
+     * @throws GeneralSecurityException If failed to clone - e.g., provided key
+     *                                  pair does not contain keys of the expected type
+     * @see #getPublicKeyType()
+     * @see #getPrivateKeyType()
+     */
+    default KeyPair cloneKeyPair(KeyPair kp) throws GeneralSecurityException {
+        if (kp == null) {
+            return null;
+        }
+
+        PUB pubCloned = null;
+        PublicKey pubOriginal = kp.getPublic();
+        Class<PUB> pubExpected = getPublicKeyType();
+        if (pubOriginal != null) {
+            Class<?> orgType = pubOriginal.getClass();
+            if (!pubExpected.isAssignableFrom(orgType)) {
+                throw new InvalidKeyException("Mismatched public key types: expected=" + pubExpected.getSimpleName() + ", actual=" + orgType.getSimpleName());
+            }
+
+            pubCloned = clonePublicKey(pubExpected.cast(pubOriginal));
+        }
+
+        PRV prvCloned = null;
+        PrivateKey prvOriginal = kp.getPrivate();
+        Class<PRV> prvExpected = getPrivateKeyType();
+        if (prvOriginal != null) {
+            Class<?> orgType = prvOriginal.getClass();
+            if (!prvExpected.isAssignableFrom(orgType)) {
+                throw new InvalidKeyException("Mismatched private key types: expected=" + prvExpected.getSimpleName() + ", actual=" + orgType.getSimpleName());
+            }
+
+            prvCloned = clonePrivateKey(prvExpected.cast(prvOriginal));
+        }
+
+        return new KeyPair(pubCloned, prvCloned);
+    }
+
+    /**
+     * @param key The {@link PublicKey} to clone - ignored if {@code null}
+     * @return The cloned key (or {@code null} if no original key)
+     * @throws GeneralSecurityException If failed to clone the key
+     */
+    PUB clonePublicKey(PUB key) throws GeneralSecurityException;
+
+    /**
+     * @param key The {@link PrivateKey} to clone - ignored if {@code null}
+     * @return The cloned key (or {@code null} if no original key)
+     * @throws GeneralSecurityException If failed to clone the key
+     */
+    PRV clonePrivateKey(PRV key) throws GeneralSecurityException;
+
+    /**
+     * @return A {@link KeyPairGenerator} suitable for this decoder
+     * @throws GeneralSecurityException If failed to create the generator
+     */
+    KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException;
+
+    /**
+     * @return A {@link KeyFactory} suitable for the specific decoder type
+     * @throws GeneralSecurityException If failed to create one
+     */
+    KeyFactory getKeyFactoryInstance() throws GeneralSecurityException;
+
+    static int encodeString(OutputStream s, String v) throws IOException {
+        return encodeString(s, v, StandardCharsets.UTF_8);
+    }
+
+    static int encodeString(OutputStream s, String v, String charset) throws IOException {
+        return encodeString(s, v, Charset.forName(charset));
+    }
+
+    static int encodeString(OutputStream s, String v, Charset cs) throws IOException {
+        return writeRLEBytes(s, v.getBytes(cs));
+    }
+
+    static int encodeBigInt(OutputStream s, BigInteger v) throws IOException {
+        return writeRLEBytes(s, v.toByteArray());
+    }
+
+    static int writeRLEBytes(OutputStream s, byte... bytes) throws IOException {
+        return writeRLEBytes(s, bytes, 0, bytes.length);
+    }
+
+    static int writeRLEBytes(OutputStream s, byte[] bytes, int off, int len) throws IOException {
+        byte[] lenBytes = encodeInt(s, len);
+        s.write(bytes, off, len);
+        return lenBytes.length + len;
+    }
+
+    static byte[] encodeInt(OutputStream s, int v) throws IOException {
+        byte[] bytes = {
+            (byte) ((v >> 24) & 0xFF),
+            (byte) ((v >> 16) & 0xFF),
+            (byte) ((v >> 8) & 0xFF),
+            (byte) (v & 0xFF)
+        };
+        s.write(bytes);
+        return bytes;
+    }
+
+    static String decodeString(InputStream s) throws IOException {
+        return decodeString(s, StandardCharsets.UTF_8);
+    }
+
+    static String decodeString(InputStream s, String charset) throws IOException {
+        return decodeString(s, Charset.forName(charset));
+    }
+
+    static String decodeString(InputStream s, Charset cs) throws IOException {
+        byte[] bytes = readRLEBytes(s);
+        return new String(bytes, cs);
+    }
+
+    static BigInteger decodeBigInt(InputStream s) throws IOException {
+        return new BigInteger(readRLEBytes(s));
+    }
+
+    static byte[] readRLEBytes(InputStream s) throws IOException {
+        int len = decodeInt(s);
+        byte[] bytes = new byte[len];
+        IoUtils.readFully(s, bytes);
+        return bytes;
+    }
+
+    static int decodeInt(InputStream s) throws IOException {
+        byte[] bytes = {0, 0, 0, 0};
+        IoUtils.readFully(s, bytes);
+        return ((bytes[0] & 0xFF) << 24)
+                | ((bytes[1] & 0xFF) << 16)
+                | ((bytes[2] & 0xFF) << 8)
+                | (bytes[3] & 0xFF);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java
new file mode 100644
index 0000000..b59dbd0
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.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.common.config.keys;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.digest.Digest;
+import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Draw an ASCII-Art representing the fingerprint so human brain can
+ * profit from its built-in pattern recognition ability.
+ * This technique is called "random art" and can be found in some
+ * scientific publications like this original paper:
+ *
+ * &quot;Hash Visualization: a New Technique to improve Real-World Security&quot;,
+ * Perrig A. and Song D., 1999, International Workshop on Cryptographic
+ * Techniques and E-Commerce (CrypTEC '99)
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <a href="http://sparrow.ece.cmu.edu/~adrian/projects/validation/validation.pdf">Original article</a>
+ * @see <a href="http://opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c">C implementation</a>
+ */
+public class KeyRandomArt {
+    public static final int FLDBASE = 8;
+    public static final int FLDSIZE_Y = FLDBASE + 1;
+    public static final int FLDSIZE_X = FLDBASE * 2 + 1;
+    public static final String AUGMENTATION_STRING = " .o+=*BOX@%&#/^SE";
+
+    private final String algorithm;
+    private final int keySize;
+    private final char[][] field = new char[FLDSIZE_X][FLDSIZE_Y];
+
+    public KeyRandomArt(PublicKey key) throws Exception {
+        this(key, KeyUtils.getDefaultFingerPrintFactory());
+    }
+
+    public KeyRandomArt(PublicKey key, Factory<? extends Digest> f) throws Exception {
+        this(key, Objects.requireNonNull(f, "No digest factory").create());
+    }
+
+    public KeyRandomArt(PublicKey key, Digest d) throws Exception {
+        this(Objects.requireNonNull(key, "No key provided").getAlgorithm(),
+             KeyUtils.getKeySize(key),
+             KeyUtils.getRawFingerprint(Objects.requireNonNull(d, "No key digest"), key));
+    }
+
+    /**
+     * @param algorithm The key algorithm
+     * @param keySize The key size in bits
+     * @param digest The key digest
+     */
+    public KeyRandomArt(String algorithm, int keySize, byte[] digest) {
+        this.algorithm = ValidateUtils.checkNotNullAndNotEmpty(algorithm, "No algorithm provided");
+        ValidateUtils.checkTrue(keySize > 0, "Invalid key size: %d", keySize);
+        this.keySize = keySize;
+        Objects.requireNonNull(digest, "No key digest provided");
+
+        int x = FLDSIZE_X / 2;
+        int y = FLDSIZE_Y / 2;
+        int len = AUGMENTATION_STRING.length() - 1;
+        for (int i = 0; i < digest.length; i++) {
+            /* each byte conveys four 2-bit move commands */
+            int input = digest[i] & 0xFF;
+            for (int b = 0; b < 4; b++) {
+                /* evaluate 2 bit, rest is shifted later */
+                x += ((input & 0x1) != 0) ? 1 : -1;
+                y += ((input & 0x2) != 0) ? 1 : -1;
+
+                /* assure we are still in bounds */
+                x = Math.max(x, 0);
+                y = Math.max(y, 0);
+                x = Math.min(x, FLDSIZE_X - 1);
+                y = Math.min(y, FLDSIZE_Y - 1);
+
+                /* augment the field */
+                if (field[x][y] < (len - 2)) {
+                    field[x][y]++;
+                }
+                input = input >> 2;
+            }
+        }
+
+        /* mark starting point and end point*/
+        field[FLDSIZE_X / 2][FLDSIZE_Y / 2] = (char) (len - 1);
+        field[x][y] = (char) len;
+    }
+
+    public String getAlgorithm() {
+        return algorithm;
+    }
+
+    public int getKeySize() {
+        return keySize;
+    }
+
+    /**
+     * Outputs the generated random art
+     *
+     * @param <A> The {@link Appendable} output writer
+     * @param sb The writer
+     * @return The updated writer instance
+     * @throws IOException If failed to write the combined result
+     */
+    public <A extends Appendable> A append(A sb) throws IOException {
+        // Upper border
+        String s = String.format("+--[%4s %4d]", getAlgorithm(), getKeySize());
+        sb.append(s);
+        for (int index = s.length(); index <= FLDSIZE_X; index++) {
+            sb.append('-');
+        }
+        sb.append('+');
+        sb.append('\n');
+
+        // contents
+        int len = AUGMENTATION_STRING.length() - 1;
+        for (int y = 0; y < FLDSIZE_Y; y++) {
+            sb.append('|');
+            for (int x = 0; x < FLDSIZE_X; x++) {
+                char ch = field[x][y];
+                sb.append(AUGMENTATION_STRING.charAt(Math.min(ch, len)));
+            }
+            sb.append('|');
+            sb.append('\n');
+        }
+
+        // lower border
+        sb.append('+');
+        for (int index = 0; index < FLDSIZE_X; index++) {
+            sb.append('-');
+        }
+
+        sb.append('+');
+        sb.append('\n');
+        return sb;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return append(new StringBuilder((FLDSIZE_X + 4) * (FLDSIZE_Y + 3))).toString();
+        } catch (IOException e) {
+            return e.getClass().getSimpleName();    // unexpected
+        }
+    }
+
+    /**
+     * Combines the arts in a user-friendly way so they are aligned with each other
+     *
+     * @param separator The separator to use between the arts - if empty char
+     * ('\0') then no separation is done
+     * @param arts The {@link KeyRandomArt}s to combine - ignored if {@code null}/empty
+     * @return The combined result
+     */
+    public static String combine(char separator, Collection<? extends KeyRandomArt> arts) {
+        if (GenericUtils.isEmpty(arts)) {
+            return "";
+        }
+
+        try {
+            return combine(new StringBuilder(arts.size() * (FLDSIZE_X + 4) * (FLDSIZE_Y + 3)), separator, arts).toString();
+        } catch (IOException e) {
+            return e.getClass().getSimpleName();    // unexpected
+        }
+    }
+
+    /**
+     * Creates the combined representation of the random art entries for the provided keys
+     *
+     * @param separator The separator to use between the arts - if empty char
+     * ('\0') then no separation is done
+     * @param provider The {@link KeyIdentityProvider} - ignored if {@code null}
+     * or has no keys to provide
+     * @return The combined representation
+     * @throws Exception If failed to extract or combine the entries
+     * @see #combine(Appendable, char, KeyIdentityProvider)
+     */
+    public static String combine(char separator, KeyIdentityProvider provider) throws Exception {
+        return combine(new StringBuilder(4 * (FLDSIZE_X + 4) * (FLDSIZE_Y + 3)), separator, provider).toString();
+    }
+
+    /**
+     * Appends the combined random art entries for the provided keys
+     *
+     * @param <A> The {@link Appendable} output writer
+     * @param sb The writer
+     * @param separator The separator to use between the arts - if empty char
+     * ('\0') then no separation is done
+     * @param provider The {@link KeyIdentityProvider} - ignored if {@code null}
+     * or has no keys to provide
+     * @return The updated writer instance
+     * @throws Exception If failed to extract or write the entries
+     * @see #generate(KeyIdentityProvider)
+     * @see #combine(Appendable, char, Collection)
+     */
+    public static <A extends Appendable> A combine(A sb, char separator, KeyIdentityProvider provider) throws Exception {
+        return combine(sb, separator, generate(provider));
+    }
+
+    /**
+     * Extracts and generates random art entries for all key in the provider
+     *
+     * @param provider The {@link KeyIdentityProvider} - ignored if {@code null}
+     * or has no keys to provide
+     * @return The extracted {@link KeyRandomArt}s
+     * @throws Exception If failed to extract the entries
+     * @see KeyIdentityProvider#loadKeys()
+     */
+    public static Collection<KeyRandomArt> generate(KeyIdentityProvider provider) throws Exception {
+        Iterable<KeyPair> keys = (provider == null) ? null : provider.loadKeys();
+        Iterator<KeyPair> iter = (keys == null) ? null : keys.iterator();
+        if ((iter == null) || (!iter.hasNext())) {
+            return Collections.emptyList();
+        }
+
+        Collection<KeyRandomArt> arts = new LinkedList<>();
+        do {
+            KeyPair kp = iter.next();
+            KeyRandomArt a = new KeyRandomArt(kp.getPublic());
+            arts.add(a);
+        } while (iter.hasNext());
+
+        return arts;
+    }
+
+    /**
+     * Combines the arts in a user-friendly way so they are aligned with each other
+     *
+     * @param <A> The {@link Appendable} output writer
+     * @param sb The writer
+     * @param separator The separator to use between the arts - if empty char
+     * ('\0') then no separation is done
+     * @param arts The {@link KeyRandomArt}s to combine - ignored if {@code null}/empty
+     * @return The updated writer instance
+     * @throws IOException If failed to write the combined result
+     */
+    public static <A extends Appendable> A combine(A sb, char separator, Collection<? extends KeyRandomArt> arts) throws IOException {
+        if (GenericUtils.isEmpty(arts)) {
+            return sb;
+        }
+
+        List<String[]> allLines = new ArrayList<>(arts.size());
+        int numLines = -1;
+        for (KeyRandomArt a : arts) {
+            String s = a.toString();
+            String[] lines = GenericUtils.split(s, '\n');
+            if (numLines <= 0) {
+                numLines = lines.length;
+            } else {
+                if (numLines != lines.length) {
+                    throw new StreamCorruptedException("Mismatched lines count: expected=" + numLines + ", actual=" + lines.length);
+                }
+            }
+
+            for (int index = 0; index < lines.length; index++) {
+                String l = lines[index];
+                if ((l.length() > 0) && (l.charAt(l.length() - 1) == '\r')) {
+                    l = l.substring(0, l.length() - 1);
+                    lines[index] = l;
+                }
+            }
+
+            allLines.add(lines);
+        }
+
+        for (int row = 0; row < numLines; row++) {
+            for (int index = 0; index < allLines.size(); index++) {
+                String[] lines = allLines.get(index);
+                String l = lines[row];
+                sb.append(l);
+                if ((index > 0) && (separator != '\0')) {
+                    sb.append(separator);
+                }
+            }
+            sb.append('\n');
+        }
+
+        return sb;
+    }
+}