All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sshd.common.config.keys.PublicKeyEntry Maven / Gradle / Ivy

There is a newer version: 2.4.1.Final
Show newest version
/*
 * 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.io.StreamCorruptedException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;

import org.apache.sshd.common.keyprovider.KeyTypeIndicator;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ValidateUtils;

/**
 * 

* Represents a {@link PublicKey} whose data is formatted according to the * OpenSSH format: *

* *
 * <key-type> <base64-encoded-public-key-data>
 * 
* * @author Apache MINA SSHD Project */ public class PublicKeyEntry implements Serializable, KeyTypeIndicator { /** * Character used to denote a comment line in the keys file */ public static final char COMMENT_CHAR = '#'; /** * Standard folder name used by OpenSSH to hold key files */ public static final String STD_KEYFILE_FOLDER_NAME = ".ssh"; private static final long serialVersionUID = -585506072687602760L; private static final NavigableMap KEY_DATA_RESOLVERS = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); private String keyType; private byte[] keyData; private PublicKeyEntryDataResolver keyDataResolver = PublicKeyEntryDataResolver.DEFAULT; public PublicKeyEntry() { super(); } public PublicKeyEntry(String keyType, byte... keyData) { this.keyType = keyType; this.keyData = keyData; } @Override public String getKeyType() { return keyType; } public void setKeyType(String value) { this.keyType = value; } public byte[] getKeyData() { return keyData; } public void setKeyData(byte[] value) { this.keyData = value; } public PublicKeyEntryDataResolver getKeyDataResolver() { return keyDataResolver; } public void setKeyDataResolver(PublicKeyEntryDataResolver keyDataResolver) { this.keyDataResolver = keyDataResolver; } /** * If a {@link PublicKeyEntryDataResolver} has been set, then uses it - otherwise uses the * {@link PublicKeyEntryDataResolver#DEFAULT default one}. * * @return The resolved instance */ public PublicKeyEntryDataResolver resolvePublicKeyEntryDataResolver() { PublicKeyEntryDataResolver resolver = getKeyDataResolver(); return (resolver == null) ? PublicKeyEntryDataResolver.DEFAULT : resolver; } /** * @param session The {@link SessionContext} for invoking this load command - may be {@code null} * if not invoked within a session context (e.g., offline tool or session unknown). * @param headers Any headers that may have been available when data was read * @param fallbackResolver The {@link PublicKeyEntryResolver} to consult if none of the built-in ones can * be used. If {@code null} and no built-in resolver can be used then an * {@link InvalidKeySpecException} is thrown. * @return The resolved {@link PublicKey} - or {@code null} if could not be resolved. * Note: may be called only after key type and data bytes have been set or * exception(s) may be thrown * @throws IOException If failed to decode the key * @throws GeneralSecurityException If failed to generate the key */ public PublicKey resolvePublicKey( SessionContext session, Map headers, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException { String kt = getKeyType(); PublicKeyEntryResolver decoder = KeyUtils.getPublicKeyEntryDecoder(kt); if (decoder == null) { decoder = fallbackResolver; } if (decoder == null) { throw new InvalidKeySpecException("No decoder available for key type=" + kt); } return decoder.resolve(session, kt, getKeyData(), headers); } /** * @param session The {@link SessionContext} for invoking this command - may be {@code null} if * not invoked within a session context (e.g., offline tool or session unknown). * @param sb The {@link Appendable} instance to encode the data into * @param fallbackResolver The {@link PublicKeyEntryResolver} to consult if none of the built-in ones can * be used. If {@code null} and no built-in resolver can be used then an * {@link InvalidKeySpecException} is thrown. * @return The {@link PublicKey} or {@code null} if could not resolve it * @throws IOException If failed to decode/encode the key * @throws GeneralSecurityException If failed to generate the key * @see #resolvePublicKey(SessionContext, Map, PublicKeyEntryResolver) */ public PublicKey appendPublicKey( SessionContext session, Appendable sb, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException { PublicKey key = resolvePublicKey(session, Collections.emptyMap(), fallbackResolver); if (key != null) { appendPublicKeyEntry(sb, key, resolvePublicKeyEntryDataResolver()); } return key; } @Override public int hashCode() { return Objects.hashCode(getKeyType()) + Arrays.hashCode(getKeyData()); } /* * In case some derived class wants to define some "extended" equality without having to repeat this code */ protected boolean isEquivalent(PublicKeyEntry e) { if (this == e) { return true; } return Objects.equals(getKeyType(), e.getKeyType()) && Arrays.equals(getKeyData(), e.getKeyData()); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (getClass() != obj.getClass()) { return false; } return isEquivalent((PublicKeyEntry) obj); } @Override public String toString() { PublicKeyEntryDataResolver resolver = resolvePublicKeyEntryDataResolver(); String encData = resolver.encodeEntryKeyData(getKeyData()); return getKeyType() + " " + (GenericUtils.isEmpty(encData) ? "" : encData); } /** * @param session The {@link SessionContext} for invoking this command - may be {@code null} if * not invoked within a session context (e.g., offline tool or session unknown). * @param entries The entries to convert - ignored if {@code null}/empty * @param fallbackResolver The {@link PublicKeyEntryResolver} to consult if none of the built-in ones can * be used. If {@code null} and no built-in resolver can be used then an * {@link InvalidKeySpecException} is thrown. * @return The {@link List} of all {@link PublicKey}-s that have been resolved * @throws IOException If failed to decode the key data * @throws GeneralSecurityException If failed to generate the {@link PublicKey} from the decoded data * @see #resolvePublicKey(SessionContext, Map, PublicKeyEntryResolver) */ public static List resolvePublicKeyEntries( SessionContext session, Collection entries, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException { int numEntries = GenericUtils.size(entries); if (numEntries <= 0) { return Collections.emptyList(); } List keys = new ArrayList<>(numEntries); for (PublicKeyEntry e : entries) { Map headers = (e instanceof AuthorizedKeyEntry) ? ((AuthorizedKeyEntry) e).getLoginOptions() : Collections.emptyMap(); PublicKey k = e.resolvePublicKey(session, headers, fallbackResolver); if (k != null) { keys.add(k); } } return keys; } /** * Registers a specialized decoder for the public key entry data bytes instead of the * {@link PublicKeyEntryDataResolver#DEFAULT default} one. * * @param keyType The key-type value (case insensitive) that will trigger the usage of this decoder - e.g., * "ssh-rsa", "pgp-sign-dss", etc. * @param resolver The decoder to use */ public static void registerKeyDataEntryResolver(String keyType, PublicKeyEntryDataResolver resolver) { ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type provided"); Objects.requireNonNull(resolver, "No resolver provided"); synchronized (KEY_DATA_RESOLVERS) { KEY_DATA_RESOLVERS.put(keyType, resolver); } } /** * @param keyType The key-type value (case insensitive) that may have been previously * {@link #registerKeyDataEntryResolver(String, PublicKeyEntryDataResolver) registered} - e.g., * "ssh-rsa", "pgp-sign-dss", etc. * @return The registered resolver instance - {@code null} if none was registered */ public static PublicKeyEntryDataResolver getKeyDataEntryResolver(String keyType) { keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type provided"); synchronized (KEY_DATA_RESOLVERS) { return KEY_DATA_RESOLVERS.get(keyType); } } /** * @param keyType The key-type value (case insensitive) that may have been previously * {@link #registerKeyDataEntryResolver(String, PublicKeyEntryDataResolver) registered} - e.g., * "ssh-rsa", "pgp-sign-dss", etc. * @return The un-registered resolver instance - {@code null} if none was registered */ public static PublicKeyEntryDataResolver unregisterKeyDataEntryResolver(String keyType) { keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type provided"); synchronized (KEY_DATA_RESOLVERS) { return KEY_DATA_RESOLVERS.remove(keyType); } } /** * @param keyType keyType The key-type value (case insensitive) whose data is to be resolved - e.g., * "ssh-rsa", "pgp-sign-dss", etc. * @return If a specific resolver has been previously * {@link #registerKeyDataEntryResolver(String, PublicKeyEntryDataResolver) registered} then uses * it, otherwise the {@link PublicKeyEntryDataResolver#DEFAULT default} one. */ public static PublicKeyEntryDataResolver resolveKeyDataEntryResolver(String keyType) { keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type provided"); PublicKeyEntryDataResolver resolver = getKeyDataEntryResolver(keyType); if (resolver != null) { return resolver; // debug breakpoint } return PublicKeyEntryDataResolver.DEFAULT; } /** * @return A snapshot of the currently registered specialized {@link PublicKeyEntryDataResolver}-s, where key=the * key-type value (case insensitive) - e.g., "ssh-rsa", "pgp-sign-dss", etc., * value=the associated {@link PublicKeyEntryDataResolver} for the key type */ public static NavigableMap getRegisteredKeyDataEntryResolvers() { NavigableMap decoders; synchronized (KEY_DATA_RESOLVERS) { if (KEY_DATA_RESOLVERS.isEmpty()) { return Collections.emptyNavigableMap(); } decoders = new TreeMap<>(KEY_DATA_RESOLVERS.comparator()); decoders.putAll(KEY_DATA_RESOLVERS); } return decoders; } /** * @param encData Assumed to contain at least {@code key-type base64-data} (anything beyond the * BASE64 data is ignored) - ignored if {@code null}/empty * @return A {@link PublicKeyEntry} or {@code null} if no data * @throws IllegalArgumentException if bad format found * @see #parsePublicKeyEntry(String, PublicKeyEntryDataResolver) */ public static PublicKeyEntry parsePublicKeyEntry(String encData) throws IllegalArgumentException { return parsePublicKeyEntry(encData, (PublicKeyEntryDataResolver) null); } /** * @param encData Assumed to contain at least {@code key-type base64-data} (anything beyond the * BASE64 data is ignored) - ignored if {@code null}/empty * @param decoder The {@link PublicKeyEntryDataResolver} to use in order to decode the key data * string into its bytes - if {@code null} then one is automatically * {@link #resolveKeyDataEntryResolver(String) resolved} * @return A {@link PublicKeyEntry} or {@code null} if no data * @throws IllegalArgumentException if bad format found * @see #parsePublicKeyEntry(PublicKeyEntry, String, PublicKeyEntryDataResolver) */ public static PublicKeyEntry parsePublicKeyEntry( String encData, PublicKeyEntryDataResolver decoder) throws IllegalArgumentException { String data = GenericUtils.replaceWhitespaceAndTrim(encData); if (GenericUtils.isEmpty(data)) { return null; } else { return parsePublicKeyEntry(new PublicKeyEntry(), data, decoder); } } /** * @param The generic entry type * @param entry The {@link PublicKeyEntry} whose contents are to be updated - ignored if * {@code null} * @param encData Assumed to contain at least {@code key-type base64-data} (anything beyond the * BASE64 data is ignored) - ignored if {@code null}/empty * @return The updated entry instance * @throws IllegalArgumentException if bad format found * @see #parsePublicKeyEntry(PublicKeyEntry, String, PublicKeyEntryDataResolver) */ public static E parsePublicKeyEntry(E entry, String encData) throws IllegalArgumentException { return parsePublicKeyEntry(entry, encData, null); } /** * @param The generic entry type * @param entry The {@link PublicKeyEntry} whose contents are to be updated - ignored if * {@code null} * @param encData Assumed to contain at least {@code key-type base64-data} (anything beyond the * BASE64 data is ignored) - ignored if {@code null}/empty * @param decoder The {@link PublicKeyEntryDataResolver} to use in order to decode the key data * string into its bytes - if {@code null} then one is automatically * {@link #resolveKeyDataEntryResolver(String) resolved} * @return The updated entry instance * @throws IllegalArgumentException if bad format found */ public static E parsePublicKeyEntry( E entry, String encData, PublicKeyEntryDataResolver decoder) throws IllegalArgumentException { String data = GenericUtils.replaceWhitespaceAndTrim(encData); if (GenericUtils.isEmpty(data) || (entry == null)) { return entry; } int startPos = data.indexOf(' '); if (startPos <= 0) { throw new IllegalArgumentException("Bad format (no key data delimiter): " + data); } int endPos = data.indexOf(' ', startPos + 1); if (endPos <= startPos) { // OK if no continuation beyond the encoded key data endPos = data.length(); } String keyType = data.substring(0, startPos); if (decoder == null) { decoder = resolveKeyDataEntryResolver(keyType); } String b64Data = data.substring(startPos + 1, endPos).trim(); byte[] keyData = decoder.decodeEntryKeyData(b64Data); if (NumberUtils.isEmpty(keyData)) { throw new IllegalArgumentException("Bad format (no BASE64 key data): " + data); } entry.setKeyType(keyType); entry.setKeyDataResolver(decoder); entry.setKeyData(keyData); return entry; } /** * @param key The {@link PublicKey} * @return The {@code OpenSSH} encoded data * @throws IllegalArgumentException If failed to encode * @see #toString(PublicKey, PublicKeyEntryDataResolver) */ public static String toString(PublicKey key) throws IllegalArgumentException { return toString(key, null); } /** * @param key The {@link PublicKey} * @param encoder The {@link PublicKeyEntryDataResolver} to use in order to encode the key data * bytes into a string representation - if {@code null} then one is automatically * {@link #resolveKeyDataEntryResolver(String) resolved} * @return The {@code OpenSSH} encoded data * @throws IllegalArgumentException If failed to encode * @see #appendPublicKeyEntry(Appendable, PublicKey, PublicKeyEntryDataResolver) */ public static String toString( PublicKey key, PublicKeyEntryDataResolver encoder) throws IllegalArgumentException { try { return appendPublicKeyEntry(new StringBuilder(Byte.MAX_VALUE), key, encoder).toString(); } catch (IOException e) { throw new IllegalArgumentException("Failed (" + e.getClass().getSimpleName() + ") to encode: " + e.getMessage(), e); } } /** * Encodes a public key data the same way as the {@link #parsePublicKeyEntry(String)} expects it * * @param The generic appendable class * @param sb The {@link Appendable} instance to encode the data into * @param key The {@link PublicKey} - ignored if {@code null} * @return The updated appendable instance * @throws IOException If failed to append the data * @see #appendPublicKeyEntry(Appendable, PublicKey, PublicKeyEntryDataResolver) */ public static A appendPublicKeyEntry(A sb, PublicKey key) throws IOException { return appendPublicKeyEntry(sb, key, null); } /** * @param The generic appendable class * @param sb The {@link Appendable} instance to encode the data into * @param key The {@link PublicKey} - ignored if {@code null} * @param encoder The {@link PublicKeyEntryDataResolver} to use in order to encode the key data bytes into a * string representation - if {@code null} then one is automatically * {@link #resolveKeyDataEntryResolver(String) resolved} * @return The updated appendable instance * @throws IOException If failed to append the data */ public static A appendPublicKeyEntry( A sb, PublicKey key, PublicKeyEntryDataResolver encoder) throws IOException { if (key == null) { return sb; } @SuppressWarnings("unchecked") PublicKeyEntryDecoder decoder = (PublicKeyEntryDecoder) KeyUtils.getPublicKeyEntryDecoder(key); if (decoder == null) { throw new StreamCorruptedException("Cannot retrieve decoder for key=" + key.getAlgorithm()); } try (ByteArrayOutputStream s = new ByteArrayOutputStream(Byte.MAX_VALUE)) { String keyType = decoder.encodePublicKey(s, key); byte[] bytes = s.toByteArray(); if (encoder == null) { encoder = resolveKeyDataEntryResolver(keyType); } String encData = encoder.encodeEntryKeyData(bytes); sb.append(keyType).append(' ').append(encData); } return sb; } private static final class LazyDefaultKeysFolderHolder { private static final Path PATH = IdentityUtils.getUserHomeFolder().resolve(STD_KEYFILE_FOLDER_NAME); private LazyDefaultKeysFolderHolder() { throw new UnsupportedOperationException("No instance allowed"); } } /** * @return The default OpenSSH folder used to hold key files - e.g., {@code known_hosts}, {@code authorized_keys}, * etc. */ @SuppressWarnings("synthetic-access") public static Path getDefaultKeysFolderPath() { return LazyDefaultKeysFolderHolder.PATH; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy