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 "empty" 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:
+ *
+ * "Hash Visualization: a New Technique to improve Real-World Security",
+ * 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;
+ }
+}