/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sshd.server.config.keys; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.Collections; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.function.Function; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.config.keys.IdentityUtils; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.server.SshServer; /** * Loads server identity key files - e.g., {@code /etc/ssh/ssh_host_rsa_key} * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> * @see org.apache.sshd.common.util.security.SecurityUtils#getKeyPairResourceParser() */ public final class ServerIdentity { public static final String ID_FILE_PREFIX = "ssh_host_"; public static final String ID_FILE_SUFFIX = "_key"; /** * The server's keys configuration multi-value */ public static final String HOST_KEY_CONFIG_PROP = "HostKey"; public static final Function<String, String> ID_GENERATOR = ServerIdentity::getIdentityFileName; private ServerIdentity() { throw new UnsupportedOperationException("No instance"); } /** * Sets the server's {@link KeyPairProvider} with the loaded identities - if any * * @param <S> The generic server type * @param server The {@link SshServer} to configure * @param props The {@link Properties} holding the server's configuration - ignored * if {@code null}/empty * @param supportedOnly If {@code true} then ignore identities that are not * supported internally * @return The updated server * @throws IOException If failed to access the file system * @throws GeneralSecurityException If failed to load the keys * @see #loadKeyPairProvider(Properties, boolean, LinkOption...) */ public static <S extends SshServer> S setKeyPairProvider(S server, Properties props, boolean supportedOnly) throws IOException, GeneralSecurityException { KeyPairProvider provider = loadKeyPairProvider(props, supportedOnly, IoUtils.getLinkOptions(false)); if (provider != null) { server.setKeyPairProvider(provider); } return server; } /** * @param props The {@link Properties} holding the server's configuration - ignored * if {@code null}/empty * @param supportedOnly If {@code true} then ignore identities that are not * supported internally * @param options The {@link LinkOption}s to use when checking files existence * @return A {@link KeyPair} for the identities - {@code null} if no identities * available (e.g., after filtering unsupported ones) * @throws IOException If failed to access the file system * @throws GeneralSecurityException If failed to load the keys * @see #loadIdentities(Properties, LinkOption...) * @see IdentityUtils#createKeyPairProvider(Map, boolean) */ public static KeyPairProvider loadKeyPairProvider(Properties props, boolean supportedOnly, LinkOption... options) throws IOException, GeneralSecurityException { Map<String, KeyPair> ids = loadIdentities(props, options); return IdentityUtils.createKeyPairProvider(ids, supportedOnly); } /** * @param props The {@link Properties} holding the server's configuration - ignored * if {@code null}/empty * @param options The {@link LinkOption}s to use when checking files existence * @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 #findIdentities(Properties, LinkOption...) * @see IdentityUtils#loadIdentities(Map, org.apache.sshd.common.config.keys.FilePasswordProvider, java.nio.file.OpenOption...) */ public static Map<String, KeyPair> loadIdentities(Properties props, LinkOption... options) throws IOException, GeneralSecurityException { Map<String, Path> ids = findIdentities(props, options); return IdentityUtils.loadIdentities(ids, null /* server key files are never encrypted */, IoUtils.EMPTY_OPEN_OPTIONS); } /** * @param props The {@link Properties} holding the server's configuration - ignored * if {@code null}/empty * @param options The {@link LinkOption}s to use when checking files existence * @return A {@link Map} of the found identities where key=the identity type * (case <U>insensitive</U>) and value=the {@link Path} of the file holding * the specific type key * @throws IOException If failed to access the file system * @see #getIdentityType(String) * @see #HOST_KEY_CONFIG_PROP * @see org.apache.sshd.common.config.SshConfigFileReader#readConfigFile(File) */ public static Map<String, Path> findIdentities(Properties props, LinkOption... options) throws IOException { if (GenericUtils.isEmpty(props)) { return Collections.emptyMap(); } String keyList = props.getProperty(HOST_KEY_CONFIG_PROP); String[] paths = GenericUtils.split(keyList, ','); if (GenericUtils.isEmpty(paths)) { return Collections.emptyMap(); } Map<String, Path> ids = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (String p : paths) { File file = new File(p); Path path = file.toPath(); if (!Files.exists(path, options)) { continue; } String type = getIdentityType(path.getFileName().toString()); if (GenericUtils.isEmpty(type)) { type = p; // just in case the file name does not adhere to the standard naming convention } Path prev = ids.put(type, path); ValidateUtils.checkTrue(prev == null, "Multiple mappings for type=%s", type); } return ids; } /** * @param name The file name - ignored if {@code null}/empty * @return The identity type - {@code null} if cannot determine it - e.g., * does not start/end with the {@link #ID_FILE_PREFIX}/{@link #ID_FILE_SUFFIX} */ public static String getIdentityType(String name) { if (GenericUtils.isEmpty(name) || (name.length() <= (ID_FILE_PREFIX.length() + ID_FILE_SUFFIX.length())) || (!name.startsWith(ID_FILE_PREFIX)) || (!name.endsWith(ID_FILE_SUFFIX))) { return null; } else { return name.substring(ID_FILE_PREFIX.length(), name.length() - ID_FILE_SUFFIX.length()); } } public static String getIdentityFileName(NamedResource r) { return getIdentityFileName((r == null) ? null : r.getName()); } /** * @param type The identity type - e.g., {@code rsa} - ignored * if {@code null}/empty * @return The matching file name for the identity - {@code null} * if no name * @see #ID_FILE_PREFIX * @see #ID_FILE_SUFFIX * @see IdentityUtils#getIdentityFileName(String, String, String) */ public static String getIdentityFileName(String type) { return IdentityUtils.getIdentityFileName(ID_FILE_PREFIX, type, ID_FILE_SUFFIX); } }