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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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; + } +}
