http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java new file mode 100644 index 0000000..23937c2 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@ -0,0 +1,937 @@ +/* + * 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.math.BigInteger; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.Factory; +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.impl.DSSPublicKeyEntryDecoder; +import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder; +import org.apache.sshd.common.config.keys.impl.RSAPublicKeyDecoder; +import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.digest.Digest; +import org.apache.sshd.common.digest.DigestFactory; +import org.apache.sshd.common.digest.DigestUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * Utility class for keys + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class KeyUtils { + /** + * Name of algorithm for RSA keys to be used when calling security provider + */ + public static final String RSA_ALGORITHM = "RSA"; + + /** + * The most commonly used RSA public key exponent + */ + public static final BigInteger DEFAULT_RSA_PUBLIC_EXPONENT = new BigInteger("65537"); + + /** + * Name of algorithm for DSS keys to be used when calling security provider + */ + public static final String DSS_ALGORITHM = "DSA"; + + /** + * Name of algorithm for EC keys to be used when calling security provider + */ + public static final String EC_ALGORITHM = "EC"; + + /** + * The {@link Set} of {@link PosixFilePermission} <U>not</U> allowed if strict + * permissions are enforced on key files + */ + public static final Set<PosixFilePermission> STRICTLY_PROHIBITED_FILE_PERMISSION = + Collections.unmodifiableSet( + EnumSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE)); + + /** + * System property that can be used to control the default fingerprint factory used for keys. + * If not set the {@link #DEFAULT_FINGERPRINT_DIGEST_FACTORY} is used + */ + public static final String KEY_FINGERPRINT_FACTORY_PROP = "org.apache.sshd.keyFingerprintFactory"; + + /** + * The default {@link Factory} of {@link Digest}s initialized + * as the value of {@link #getDefaultFingerPrintFactory()} if not + * overridden by {@link #KEY_FINGERPRINT_FACTORY_PROP} or + * {@link #setDefaultFingerPrintFactory(DigestFactory)} + */ + public static final DigestFactory DEFAULT_FINGERPRINT_DIGEST_FACTORY = BuiltinDigests.sha256; + + private static final AtomicReference<DigestFactory> DEFAULT_DIGEST_HOLDER = new AtomicReference<>(); + + private static final Map<String, PublicKeyEntryDecoder<?, ?>> BY_KEY_TYPE_DECODERS_MAP = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private static final Map<Class<?>, PublicKeyEntryDecoder<?, ?>> BY_KEY_CLASS_DECODERS_MAP = + new HashMap<>(); + + static { + registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE); + registerPublicKeyEntryDecoder(DSSPublicKeyEntryDecoder.INSTANCE); + + if (SecurityUtils.isECCSupported()) { + registerPublicKeyEntryDecoder(ECDSAPublicKeyEntryDecoder.INSTANCE); + } + if (SecurityUtils.isEDDSACurveSupported()) { + registerPublicKeyEntryDecoder(SecurityUtils.getEDDSAPublicKeyEntryDecoder()); + } + } + + private KeyUtils() { + throw new UnsupportedOperationException("No instance"); + } + + /** + * <P>Checks if a path has strict permissions</P> + * <UL> + * <LI><P> + * The path may not have {@link PosixFilePermission#OTHERS_EXECUTE} + * permission + * </P></LI> + * + * <LI><P> + * (For {@code Unix}) The path may not have group or others permissions + * </P></LI> + * + * <LI><P> + * (For {@code Unix}) If the path is a file, then its folder may not have + * group or others permissions + * </P></LI> + * + * <LI><P> + * The path must be owned by current user. + * </P></LI> + * + * <LI><P> + * (For {@code Unix}) The path may be owned by root. + * </P></LI> + * + * <LI><P> + * (For {@code Unix}) If the path is a file, then its folder must also + * have valid owner. + * </P></LI> + * + * </UL> + * + * @param path The {@link Path} to be checked - ignored if {@code null} + * or does not exist + * @param options The {@link LinkOption}s to use to query the file's permissions + * @return The violated permission as {@link SimpleImmutableEntry} where key is a message and + * value is the offending object {@link PosixFilePermission} or {@link String} for owner - {@code null} + * if no violations detected + * @throws IOException If failed to retrieve the permissions + * @see #STRICTLY_PROHIBITED_FILE_PERMISSION + */ + public static SimpleImmutableEntry<String, Object> validateStrictKeyFilePermissions(Path path, LinkOption... options) throws IOException { + if ((path == null) || (!Files.exists(path, options))) { + return null; + } + + Collection<PosixFilePermission> perms = IoUtils.getPermissions(path, options); + if (GenericUtils.isEmpty(perms)) { + return null; + } + + if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) { + PosixFilePermission p = PosixFilePermission.OTHERS_EXECUTE; + return new SimpleImmutableEntry<>(String.format("Permissions violation (%s)", p), p); + } + + if (OsUtils.isUNIX()) { + PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, STRICTLY_PROHIBITED_FILE_PERMISSION); + if (p != null) { + return new SimpleImmutableEntry<>(String.format("Permissions violation (%s)", p), p); + } + + if (Files.isRegularFile(path, options)) { + Path parent = path.getParent(); + p = IoUtils.validateExcludedPermissions(IoUtils.getPermissions(parent, options), STRICTLY_PROHIBITED_FILE_PERMISSION); + if (p != null) { + return new SimpleImmutableEntry<>(String.format("Parent permissions violation (%s)", p), p); + } + } + } + + String owner = IoUtils.getFileOwner(path, options); + if (GenericUtils.isEmpty(owner)) { + // we cannot get owner + // general issue: jvm does not support permissions + // security issue: specific filesystem does not support permissions + return null; + } + + String current = OsUtils.getCurrentUser(); + Set<String> expected = new HashSet<>(); + expected.add(current); + if (OsUtils.isUNIX()) { + // Windows "Administrator" was considered however in Windows most likely a group is used. + expected.add(OsUtils.ROOT_USER); + } + + if (!expected.contains(owner)) { + return new SimpleImmutableEntry<>(String.format("Owner violation (%s)", owner), owner); + } + + if (OsUtils.isUNIX()) { + if (Files.isRegularFile(path, options)) { + String parentOwner = IoUtils.getFileOwner(path.getParent(), options); + if ((!GenericUtils.isEmpty(parentOwner)) && (!expected.contains(parentOwner))) { + return new SimpleImmutableEntry<>(String.format("Parent owner violation (%s)", parentOwner), parentOwner); + } + } + } + + return null; + } + + /** + * @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss} + * @param keySize The key size (in bits) + * @return A {@link KeyPair} of the specified type and size + * @throws GeneralSecurityException If failed to generate the key pair + * @see #getPublicKeyEntryDecoder(String) + * @see PublicKeyEntryDecoder#generateKeyPair(int) + */ + public static KeyPair generateKeyPair(String keyType, int keySize) throws GeneralSecurityException { + PublicKeyEntryDecoder<?, ?> decoder = getPublicKeyEntryDecoder(keyType); + if (decoder == null) { + throw new InvalidKeySpecException("No decoder for key type=" + keyType); + } + + return decoder.generateKeyPair(keySize); + } + + /** + * Performs a deep-clone of the original {@link KeyPair} - i.e., creates + * <U>new</U> public/private keys that are clones of the original one + * + * @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss} + * @param kp The {@link KeyPair} to clone - ignored if {@code null} + * @return The cloned instance + * @throws GeneralSecurityException If failed to clone the pair + */ + public static KeyPair cloneKeyPair(String keyType, KeyPair kp) throws GeneralSecurityException { + PublicKeyEntryDecoder<?, ?> decoder = getPublicKeyEntryDecoder(keyType); + if (decoder == null) { + throw new InvalidKeySpecException("No decoder for key type=" + keyType); + } + + return decoder.cloneKeyPair(kp); + } + + /** + * @param decoder The decoder to register + * @throws IllegalArgumentException if no decoder or not key type or no + * supported names for the decoder + * @see PublicKeyEntryDecoder#getPublicKeyType() + * @see PublicKeyEntryDecoder#getSupportedTypeNames() + */ + public static void registerPublicKeyEntryDecoder(PublicKeyEntryDecoder<?, ?> decoder) { + Objects.requireNonNull(decoder, "No decoder specified"); + + Class<?> pubType = Objects.requireNonNull(decoder.getPublicKeyType(), "No public key type declared"); + Class<?> prvType = Objects.requireNonNull(decoder.getPrivateKeyType(), "No private key type declared"); + synchronized (BY_KEY_CLASS_DECODERS_MAP) { + BY_KEY_CLASS_DECODERS_MAP.put(pubType, decoder); + BY_KEY_CLASS_DECODERS_MAP.put(prvType, decoder); + } + + Collection<String> names = ValidateUtils.checkNotNullAndNotEmpty(decoder.getSupportedTypeNames(), "No supported key type"); + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + for (String n : names) { + PublicKeyEntryDecoder<?, ?> prev = BY_KEY_TYPE_DECODERS_MAP.put(n, decoder); + if (prev != null) { + //noinspection UnnecessaryContinue + continue; // debug breakpoint + } + } + } + } + + /** + * @param keyType The {@code OpenSSH} key type string - e.g., {@code ssh-rsa, ssh-dss} + * - ignored if {@code null}/empty + * @return The registered {@link PublicKeyEntryDecoder} or {code null} if not found + */ + public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(String keyType) { + if (GenericUtils.isEmpty(keyType)) { + return null; + } + + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + return BY_KEY_TYPE_DECODERS_MAP.get(keyType); + } + } + + /** + * @param kp The {@link KeyPair} to examine - ignored if {@code null} + * @return The matching {@link PublicKeyEntryDecoder} provided <U>both</U> + * the public and private keys have the same decoder - {@code null} if no + * match found + * @see #getPublicKeyEntryDecoder(Key) + */ + public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(KeyPair kp) { + if (kp == null) { + return null; + } + + PublicKeyEntryDecoder<?, ?> d1 = getPublicKeyEntryDecoder(kp.getPublic()); + PublicKeyEntryDecoder<?, ?> d2 = getPublicKeyEntryDecoder(kp.getPrivate()); + if (d1 == d2) { + return d1; + } else { + return null; // some kind of mixed keys... + } + } + + /** + * @param key The {@link Key} (public or private) - ignored if {@code null} + * @return The registered {@link PublicKeyEntryDecoder} for this key or {code null} if no match found + * @see #getPublicKeyEntryDecoder(Class) + */ + public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(Key key) { + if (key == null) { + return null; + } else { + return getPublicKeyEntryDecoder(key.getClass()); + } + } + + /** + * @param keyType The key {@link Class} - ignored if {@code null} or not a {@link Key} + * compatible type + * @return The registered {@link PublicKeyEntryDecoder} or {code null} if no match found + */ + public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(Class<?> keyType) { + if ((keyType == null) || (!Key.class.isAssignableFrom(keyType))) { + return null; + } + + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + PublicKeyEntryDecoder<?, ?> decoder = BY_KEY_CLASS_DECODERS_MAP.get(keyType); + if (decoder != null) { + return decoder; + } + + // in case it is a derived class + for (PublicKeyEntryDecoder<?, ?> dec : BY_KEY_CLASS_DECODERS_MAP.values()) { + Class<?> pubType = dec.getPublicKeyType(); + Class<?> prvType = dec.getPrivateKeyType(); + if (pubType.isAssignableFrom(keyType) || prvType.isAssignableFrom(keyType)) { + return dec; + } + } + } + + return null; + } + + /** + * @return The default {@link DigestFactory} + * by the {@link #getFingerPrint(PublicKey)} and {@link #getFingerPrint(String)} + * methods + * @see #KEY_FINGERPRINT_FACTORY_PROP + * @see #setDefaultFingerPrintFactory(DigestFactory) + */ + public static DigestFactory getDefaultFingerPrintFactory() { + DigestFactory factory = null; + synchronized (DEFAULT_DIGEST_HOLDER) { + factory = DEFAULT_DIGEST_HOLDER.get(); + if (factory != null) { + return factory; + } + + String propVal = System.getProperty(KEY_FINGERPRINT_FACTORY_PROP); + if (GenericUtils.isEmpty(propVal)) { + factory = DEFAULT_FINGERPRINT_DIGEST_FACTORY; + } else { + factory = ValidateUtils.checkNotNull(BuiltinDigests.fromFactoryName(propVal), "Unknown digest factory: %s", propVal); + } + + ValidateUtils.checkTrue(factory.isSupported(), "Selected fingerprint digest not supported: %s", factory.getName()); + DEFAULT_DIGEST_HOLDER.set(factory); + } + + return factory; + } + + /** + * @param f The {@link DigestFactory} of {@link Digest}s to be used - may + * not be {@code null} + */ + public static void setDefaultFingerPrintFactory(DigestFactory f) { + synchronized (DEFAULT_DIGEST_HOLDER) { + DEFAULT_DIGEST_HOLDER.set(Objects.requireNonNull(f, "No digest factory")); + } + } + + /** + * @param key the public key - ignored if {@code null} + * @return the fingerprint or {@code null} if no key. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see #getFingerPrint(Factory, PublicKey) + */ + public static String getFingerPrint(PublicKey key) { + return getFingerPrint(getDefaultFingerPrintFactory(), key); + } + + /** + * @param password The {@link String} to digest - ignored if {@code null}/empty, + * otherwise its UTF-8 representation is used as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see #getFingerPrint(String, Charset) + */ + public static String getFingerPrint(String password) { + return getFingerPrint(password, StandardCharsets.UTF_8); + } + + /** + * @param password The {@link String} to digest - ignored if {@code null}/empty + * @param charset The {@link Charset} to use in order to convert the + * string to its byte representation to use as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see #getFingerPrint(Factory, String, Charset) + * @see #getDefaultFingerPrintFactory() + */ + public static String getFingerPrint(String password, Charset charset) { + return getFingerPrint(getDefaultFingerPrintFactory(), password, charset); + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param key the public key - ignored if {@code null} + * @return the fingerprint or {@code null} if no key. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see #getFingerPrint(Digest, PublicKey) + */ + public static String getFingerPrint(Factory<? extends Digest> f, PublicKey key) { + return (key == null) ? null : getFingerPrint(Objects.requireNonNull(f, "No digest factory").create(), key); + } + + /** + * @param d The {@link Digest} to use + * @param key the public key - ignored if {@code null} + * @return the fingerprint or {@code null} if no key. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see DigestUtils#getFingerPrint(Digest, byte[], int, int) + */ + public static String getFingerPrint(Digest d, PublicKey key) { + if (key == null) { + return null; + } + + try { + Buffer buffer = new ByteArrayBuffer(); + buffer.putRawPublicKey(key); + return DigestUtils.getFingerPrint(d, buffer.array(), 0, buffer.wpos()); + } catch (Exception e) { + return e.getClass().getSimpleName(); + } + } + + public static byte[] getRawFingerprint(PublicKey key) throws Exception { + return getRawFingerprint(getDefaultFingerPrintFactory(), key); + } + + public static byte[] getRawFingerprint(Factory<? extends Digest> f, PublicKey key) throws Exception { + return (key == null) ? null : getRawFingerprint(Objects.requireNonNull(f, "No digest factory").create(), key); + } + + public static byte[] getRawFingerprint(Digest d, PublicKey key) throws Exception { + if (key == null) { + return null; + } + + Buffer buffer = new ByteArrayBuffer(); + buffer.putRawPublicKey(key); + return DigestUtils.getRawFingerprint(d, buffer.array(), 0, buffer.wpos()); + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty, + * otherwise its UTF-8 representation is used as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see #getFingerPrint(Digest, String, Charset) + */ + public static String getFingerPrint(Factory<? extends Digest> f, String s) { + return getFingerPrint(f, s, StandardCharsets.UTF_8); + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty + * @param charset The {@link Charset} to use in order to convert the + * string to its byte representation to use as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see DigestUtils#getFingerPrint(Digest, String, Charset) + */ + public static String getFingerPrint(Factory<? extends Digest> f, String s, Charset charset) { + return getFingerPrint(f.create(), s, charset); + } + + /** + * @param d The {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty, + * otherwise its UTF-8 representation is used as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see DigestUtils#getFingerPrint(Digest, String, Charset) + */ + public static String getFingerPrint(Digest d, String s) { + return getFingerPrint(d, s, StandardCharsets.UTF_8); + } + + /** + * @param d The {@link Digest} to use to calculate the fingerprint + * @param s The string to digest - ignored if {@code null}/empty + * @param charset The {@link Charset} to use in order to convert the + * string to its byte representation to use as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input. + * <B>Note:</B> if exception encountered then returns the exception's simple class name + * @see DigestUtils#getFingerPrint(Digest, String, Charset) + */ + public static String getFingerPrint(Digest d, String s, Charset charset) { + if (GenericUtils.isEmpty(s)) { + return null; + } + + try { + return DigestUtils.getFingerPrint(d, s, charset); + } catch (Exception e) { + return e.getClass().getSimpleName(); + } + } + + /** + * @param expected The expected fingerprint if {@code null} or empty then returns a failure + * with the default fingerprint. + * @param key the {@link PublicKey} - if {@code null} then returns null. + * @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint, + * {@code null} if no key. + * @see #getDefaultFingerPrintFactory() + * @see #checkFingerPrint(String, Factory, PublicKey) + */ + public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, PublicKey key) { + return checkFingerPrint(expected, getDefaultFingerPrintFactory(), key); + } + + /** + * @param expected The expected fingerprint if {@code null} or empty then returns a failure + * with the default fingerprint. + * @param f The {@link Factory} to be used to generate the default {@link Digest} for the key + * @param key the {@link PublicKey} - if {@code null} then returns null. + * @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint, + * {@code null} if no key. + */ + public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, Factory<? extends Digest> f, PublicKey key) { + return checkFingerPrint(expected, Objects.requireNonNull(f, "No digest factory").create(), key); + } + + /** + * @param expected The expected fingerprint if {@code null} or empty then returns a failure + * with the default fingerprint. + * @param d The {@link Digest} to be used to generate the default fingerprint for the key + * @param key the {@link PublicKey} - if {@code null} then returns null. + * @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint, + * {@code null} if no key. + */ + public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, Digest d, PublicKey key) { + if (key == null) { + return null; + } + + if (GenericUtils.isEmpty(expected)) { + return new SimpleImmutableEntry<>(false, getFingerPrint(d, key)); + } + + // de-construct fingerprint + int pos = expected.indexOf(':'); + if ((pos < 0) || (pos >= (expected.length() - 1))) { + return new SimpleImmutableEntry<>(false, getFingerPrint(d, key)); + } + + String name = expected.substring(0, pos); + String value = expected.substring(pos + 1); + DigestFactory expectedFactory; + // We know that all digest names have a length > 2 - if 2 (or less) then assume a pure HEX value + if (name.length() > 2) { + expectedFactory = BuiltinDigests.fromFactoryName(name); + if (expectedFactory == null) { + return new SimpleImmutableEntry<>(false, getFingerPrint(d, key)); + } + + expected = name.toUpperCase() + ":" + value; + } else { + expectedFactory = BuiltinDigests.md5; + expected = expectedFactory.getName().toUpperCase() + ":" + expected; + } + + String fingerprint = getFingerPrint(expectedFactory, key); + boolean matches = BuiltinDigests.md5.getName().equals(expectedFactory.getName()) + ? expected.equalsIgnoreCase(fingerprint) // HEX is case insensitive + : expected.equals(fingerprint); + return new SimpleImmutableEntry<>(matches, fingerprint); + } + + /** + * @param kp a key pair - ignored if {@code null}. If the private + * key is non-{@code null} then it is used to determine the type, + * otherwise the public one is used. + * @return the key type or {@code null} if cannot determine it + * @see #getKeyType(Key) + */ + public static String getKeyType(KeyPair kp) { + if (kp == null) { + return null; + } + PrivateKey key = kp.getPrivate(); + if (key != null) { + return getKeyType(key); + } else { + return getKeyType(kp.getPublic()); + } + } + + /** + * @param key a public or private key + * @return the key type or {@code null} if cannot determine it + */ + public static String getKeyType(Key key) { + if (key == null) { + return null; + } else if (key instanceof DSAKey) { + return KeyPairProvider.SSH_DSS; + } else if (key instanceof RSAKey) { + return KeyPairProvider.SSH_RSA; + } else if (key instanceof ECKey) { + ECKey ecKey = (ECKey) key; + ECParameterSpec ecSpec = ecKey.getParams(); + ECCurves curve = ECCurves.fromCurveParameters(ecSpec); + if (curve == null) { + return null; // debug breakpoint + } else { + return curve.getKeyType(); + } + } else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { + return KeyPairProvider.SSH_ED25519; + } + + return null; + } + + /** + * Determines the key size in bits + * + * @param key The {@link Key} to examine - ignored if {@code null} + * @return The key size - non-positive value if cannot determine it + */ + public static int getKeySize(Key key) { + if (key == null) { + return -1; + } else if (key instanceof RSAKey) { + BigInteger n = ((RSAKey) key).getModulus(); + return n.bitLength(); + } else if (key instanceof DSAKey) { + DSAParams params = ((DSAKey) key).getParams(); + BigInteger p = params.getP(); + return p.bitLength(); + } else if (key instanceof ECKey) { + ECParameterSpec ecSpec = ((ECKey) key).getParams(); + ECCurves curve = ECCurves.fromCurveParameters(ecSpec); + if (curve != null) { + return curve.getKeySize(); + } + } else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { + return SecurityUtils.getEDDSAKeySize(key); + } + + return -1; + } + + /** + * @param key The {@link PublicKey} to be checked - ignored if {@code null} + * @param keySet The keys to be searched - ignored if {@code null}/empty + * @return The matching {@link PublicKey} from the keys or {@code null} if + * no match found + * @see #compareKeys(PublicKey, PublicKey) + */ + public static PublicKey findMatchingKey(PublicKey key, PublicKey... keySet) { + if (key == null || GenericUtils.isEmpty(keySet)) { + return null; + } else { + return findMatchingKey(key, Arrays.asList(keySet)); + } + } + + /** + * @param key The {@link PublicKey} to be checked - ignored if {@code null} + * @param keySet The keys to be searched - ignored if {@code null}/empty + * @return The matching {@link PublicKey} from the keys or {@code null} if + * no match found + * @see #compareKeys(PublicKey, PublicKey) + */ + public static PublicKey findMatchingKey(PublicKey key, Collection<? extends PublicKey> keySet) { + if (key == null || GenericUtils.isEmpty(keySet)) { + return null; + } + for (PublicKey k : keySet) { + if (compareKeys(key, k)) { + return k; + } + } + return null; + } + + public static boolean compareKeyPairs(KeyPair k1, KeyPair k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if ((k1 == null) || (k2 == null)) { + return false; // both null is covered by Objects#equals + } else { + return compareKeys(k1.getPublic(), k2.getPublic()) + && compareKeys(k1.getPrivate(), k2.getPrivate()); + } + } + + public static boolean compareKeys(PublicKey k1, PublicKey k2) { + if ((k1 instanceof RSAPublicKey) && (k2 instanceof RSAPublicKey)) { + return compareRSAKeys(RSAPublicKey.class.cast(k1), RSAPublicKey.class.cast(k2)); + } else if ((k1 instanceof DSAPublicKey) && (k2 instanceof DSAPublicKey)) { + return compareDSAKeys(DSAPublicKey.class.cast(k1), DSAPublicKey.class.cast(k2)); + } else if ((k1 instanceof ECPublicKey) && (k2 instanceof ECPublicKey)) { + return compareECKeys(ECPublicKey.class.cast(k1), ECPublicKey.class.cast(k2)); + } else if ((k1 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k1.getAlgorithm()) + && (k2 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k2.getAlgorithm())) { + return SecurityUtils.compareEDDSAPPublicKeys(k1, k2); + } else { + return false; // either key is null or not of same class + } + } + + public static PublicKey recoverPublicKey(PrivateKey key) throws GeneralSecurityException { + if (key instanceof RSAPrivateKey) { + return recoverRSAPublicKey((RSAPrivateKey) key); + } else if (key instanceof DSAPrivateKey) { + return recoverDSAPublicKey((DSAPrivateKey) key); + } else if ((key != null) && SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { + return SecurityUtils.recoverEDDSAPublicKey(key); + } else { + return null; + } + } + + public static boolean compareKeys(PrivateKey k1, PrivateKey k2) { + if ((k1 instanceof RSAPrivateKey) && (k2 instanceof RSAPrivateKey)) { + return compareRSAKeys(RSAPrivateKey.class.cast(k1), RSAPrivateKey.class.cast(k2)); + } else if ((k1 instanceof DSAPrivateKey) && (k2 instanceof DSAPrivateKey)) { + return compareDSAKeys(DSAPrivateKey.class.cast(k1), DSAPrivateKey.class.cast(k2)); + } else if ((k1 instanceof ECPrivateKey) && (k2 instanceof ECPrivateKey)) { + return compareECKeys(ECPrivateKey.class.cast(k1), ECPrivateKey.class.cast(k2)); + } else if ((k1 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k1.getAlgorithm()) + && (k2 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k2.getAlgorithm())) { + return SecurityUtils.compareEDDSAPrivateKeys(k1, k2); + } else { + return false; // either key is null or not of same class + } + } + + public static boolean compareRSAKeys(RSAPublicKey k1, RSAPublicKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getPublicExponent(), k2.getPublicExponent()) + && Objects.equals(k1.getModulus(), k2.getModulus()); + } + } + + public static boolean compareRSAKeys(RSAPrivateKey k1, RSAPrivateKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getModulus(), k2.getModulus()) + && Objects.equals(k1.getPrivateExponent(), k2.getPrivateExponent()); + } + } + + public static RSAPublicKey recoverRSAPublicKey(RSAPrivateKey privateKey) throws GeneralSecurityException { + if (privateKey instanceof RSAPrivateCrtKey) { + return recoverFromRSAPrivateCrtKey((RSAPrivateCrtKey) privateKey); + } else { + // Not ideal, but best we can do under the circumstances + return recoverRSAPublicKey(privateKey.getModulus(), DEFAULT_RSA_PUBLIC_EXPONENT); + } + } + + public static RSAPublicKey recoverFromRSAPrivateCrtKey(RSAPrivateCrtKey rsaKey) throws GeneralSecurityException { + return recoverRSAPublicKey(rsaKey.getPrimeP(), rsaKey.getPrimeQ(), rsaKey.getPublicExponent()); + } + + public static RSAPublicKey recoverRSAPublicKey(BigInteger p, BigInteger q, BigInteger publicExponent) throws GeneralSecurityException { + return recoverRSAPublicKey(p.multiply(q), publicExponent); + } + + public static RSAPublicKey recoverRSAPublicKey(BigInteger modulus, BigInteger publicExponent) throws GeneralSecurityException { + KeyFactory kf = SecurityUtils.getKeyFactory(RSA_ALGORITHM); + return (RSAPublicKey) kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); + } + + public static boolean compareDSAKeys(DSAPublicKey k1, DSAPublicKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getY(), k2.getY()) + && compareDSAParams(k1.getParams(), k2.getParams()); + } + } + + public static boolean compareDSAKeys(DSAPrivateKey k1, DSAPrivateKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getX(), k2.getX()) + && compareDSAParams(k1.getParams(), k2.getParams()); + } + } + + public static boolean compareDSAParams(DSAParams p1, DSAParams p2) { + if (Objects.equals(p1, p2)) { + return true; + } else if (p1 == null || p2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(p1.getG(), p2.getG()) + && Objects.equals(p1.getP(), p2.getP()) + && Objects.equals(p1.getQ(), p2.getQ()); + } + } + + // based on code from https://github.com/alexo/SAML-2.0/blob/master/java-opensaml/opensaml-security-api/src/main/java/org/opensaml/xml/security/SecurityHelper.java + public static DSAPublicKey recoverDSAPublicKey(DSAPrivateKey privateKey) throws GeneralSecurityException { + DSAParams keyParams = privateKey.getParams(); + BigInteger p = keyParams.getP(); + BigInteger x = privateKey.getX(); + BigInteger q = keyParams.getQ(); + BigInteger g = keyParams.getG(); + BigInteger y = g.modPow(x, p); + KeyFactory kf = SecurityUtils.getKeyFactory(DSS_ALGORITHM); + return (DSAPublicKey) kf.generatePublic(new DSAPublicKeySpec(y, p, q, g)); + } + + public static boolean compareECKeys(ECPrivateKey k1, ECPrivateKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getS(), k2.getS()) + && compareECParams(k1.getParams(), k2.getParams()); + } + } + + public static boolean compareECKeys(ECPublicKey k1, ECPublicKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getW(), k2.getW()) + && compareECParams(k1.getParams(), k2.getParams()); + } + } + + public static boolean compareECParams(ECParameterSpec s1, ECParameterSpec s2) { + if (Objects.equals(s1, s2)) { + return true; + } else if (s1 == null || s2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(s1.getOrder(), s2.getOrder()) + && (s1.getCofactor() == s2.getCofactor()) + && Objects.equals(s1.getGenerator(), s2.getGenerator()) + && Objects.equals(s1.getCurve(), s2.getCurve()); + } + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java new file mode 100644 index 0000000..6dbeee9 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java @@ -0,0 +1,142 @@ +/* + * 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.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StreamCorruptedException; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Collection; +import java.util.Objects; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * @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 PrivateKeyEntryDecoder<PUB extends PublicKey, PRV extends PrivateKey> + extends KeyEntryResolver<PUB, PRV>, PrivateKeyEntryResolver { + + @Override + default PrivateKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException { + ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type provided"); + Collection<String> supported = getSupportedTypeNames(); + if ((GenericUtils.size(supported) > 0) && supported.contains(keyType)) { + return decodePrivateKey(FilePasswordProvider.EMPTY, keyData); + } + + throw new InvalidKeySpecException("resolve(" + keyType + ") not in listed supported types: " + supported); + } + + /** + * @param passwordProvider The {@link FilePasswordProvider} to use + * in case the data is encrypted - may be {@code null} if no encrypted + * data is expected + * @param keyData The key data bytes in {@code OpenSSH} format (after + * BASE64 decoding) - ignored if {@code null}/empty + * @return The decoded {@link PrivateKey} - or {@code null} if no data + * @throws IOException If failed to decode the key + * @throws GeneralSecurityException If failed to generate the key + */ + default PRV decodePrivateKey(FilePasswordProvider passwordProvider, byte... keyData) + throws IOException, GeneralSecurityException { + return decodePrivateKey(passwordProvider, keyData, 0, NumberUtils.length(keyData)); + } + + default PRV decodePrivateKey(FilePasswordProvider passwordProvider, byte[] keyData, int offset, int length) + throws IOException, GeneralSecurityException { + if (length <= 0) { + return null; + } + + try (InputStream stream = new ByteArrayInputStream(keyData, offset, length)) { + return decodePrivateKey(passwordProvider, stream); + } + } + + default PRV decodePrivateKey(FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException { + // the actual data is preceded by a string that repeats the key type + String type = KeyEntryResolver.decodeString(keyData); + if (GenericUtils.isEmpty(type)) { + throw new StreamCorruptedException("Missing key type string"); + } + + Collection<String> supported = getSupportedTypeNames(); + if (GenericUtils.isEmpty(supported) || (!supported.contains(type))) { + throw new InvalidKeySpecException("Reported key type (" + type + ") not in supported list: " + supported); + } + + return decodePrivateKey(type, passwordProvider, keyData); + } + + /** + * @param keyType The reported / encode key type + * @param passwordProvider The {@link FilePasswordProvider} to use + * in case the data is encrypted - may be {@code null} if no encrypted + * data is expected + * @param keyData The key data bytes stream positioned after the key type decoding + * and making sure it is one of the supported types + * @return The decoded {@link PrivateKey} + * @throws IOException If failed to read from the data stream + * @throws GeneralSecurityException If failed to generate the key + */ + PRV decodePrivateKey(String keyType, FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException; + + /** + * Encodes the {@link PrivateKey} using the {@code OpenSSH} format - same + * one used by the {@code decodePublicKey} method(s) + * + * @param s The {@link OutputStream} to write the data to + * @param key The {@link PrivateKey} - may not be {@code null} + * @return The key type value - one of the {@link #getSupportedTypeNames()} or + * {@code null} if encoding not supported + * @throws IOException If failed to generate the encoding + */ + default String encodePrivateKey(OutputStream s, PRV key) throws IOException { + Objects.requireNonNull(key, "No private key provided"); + return null; + } + + default boolean isPublicKeyRecoverySupported() { + return false; + } + + /** + * Attempts to recover the public key given the private one + * + * @param prvKey The {@link PrivateKey} + * @return The recovered {@link PublicKey} - {@code null} if cannot recover it + * @throws GeneralSecurityException If failed to generate the public key + */ + default PUB recoverPublicKey(PRV prvKey) throws GeneralSecurityException { + return null; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryResolver.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryResolver.java new file mode 100644 index 0000000..1e4c91e --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryResolver.java @@ -0,0 +1,70 @@ +/* + * 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.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface PrivateKeyEntryResolver { + /** + * A resolver that ignores all input + */ + PrivateKeyEntryResolver IGNORING = new PrivateKeyEntryResolver() { + @Override + public PrivateKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException { + return null; + } + + @Override + public String toString() { + return "IGNORING"; + } + }; + + /** + * A resolver that fails on all input + */ + PrivateKeyEntryResolver FAILING = new PrivateKeyEntryResolver() { + @Override + public PrivateKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException { + throw new InvalidKeySpecException("Failing resolver on key type=" + keyType); + } + + @Override + public String toString() { + return "FAILING"; + } + }; + + /** + * @param keyType The {@code OpenSSH} reported key type + * @param keyData The {@code OpenSSH} encoded key data + * @return The extracted {@link PrivateKey} - ignored if {@code null} + * @throws IOException If failed to parse the key data + * @throws GeneralSecurityException If failed to generate the key + */ + PrivateKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java new file mode 100644 index 0000000..1db3d2b --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java @@ -0,0 +1,286 @@ +/* + * 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.Arrays; +import java.util.Base64; +import java.util.Objects; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; + +/** + * <P>Represents a {@link PublicKey} whose data is formatted according to + * the <A HREF="http://en.wikibooks.org/wiki/OpenSSH">OpenSSH</A> format:</P> + * + * <PRE> + * <key-type> <base64-encoded-public-key-data> + * </PRE> + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class PublicKeyEntry implements Serializable { + + /** + * 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 String keyType; + private byte[] keyData; + + public PublicKeyEntry() { + super(); + } + + public PublicKeyEntry(String keyType, byte... keyData) { + this.keyType = keyType; + this.keyData = keyData; + } + + 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; + } + + /** + * @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. <B>Note:</B> 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(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(kt, getKeyData()); + } + + /** + * @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(PublicKeyEntryResolver) + */ + public PublicKey appendPublicKey(Appendable sb, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException { + PublicKey key = resolvePublicKey(fallbackResolver); + if (key != null) { + appendPublicKeyEntry(sb, key); + } + 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() { + byte[] data = getKeyData(); + Base64.Encoder encoder = Base64.getEncoder(); + return getKeyType() + " " + (NumberUtils.isEmpty(data) ? "<no-key>" : encoder.encodeToString(data)); + } + + /** + * @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(PublicKeyEntry, String) + */ + public static PublicKeyEntry parsePublicKeyEntry(String encData) throws IllegalArgumentException { + String data = GenericUtils.replaceWhitespaceAndTrim(encData); + if (GenericUtils.isEmpty(data)) { + return null; + } else { + return parsePublicKeyEntry(new PublicKeyEntry(), data); + } + } + + /** + * @param <E> 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 + */ + public static <E extends PublicKeyEntry> E parsePublicKeyEntry(E entry, String encData) 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 BASE64 encoded data + endPos = data.length(); + } + + String keyType = data.substring(0, startPos); + String b64Data = data.substring(startPos + 1, endPos).trim(); + Base64.Decoder decoder = Base64.getDecoder(); + byte[] keyData = decoder.decode(b64Data); + if (NumberUtils.isEmpty(keyData)) { + throw new IllegalArgumentException("Bad format (no BASE64 key data): " + data); + } + + entry.setKeyType(keyType); + entry.setKeyData(keyData); + return entry; + } + + /** + * @param key The {@link PublicKey} + * @return The {@code OpenSSH} encoded data + * @throws IllegalArgumentException If failed to encode + * @see #appendPublicKeyEntry(Appendable, PublicKey) + */ + public static String toString(PublicKey key) throws IllegalArgumentException { + try { + return appendPublicKeyEntry(new StringBuilder(Byte.MAX_VALUE), key).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 <A> 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 + */ + public static <A extends Appendable> A appendPublicKeyEntry(A sb, PublicKey key) throws IOException { + if (key == null) { + return sb; + } + + @SuppressWarnings("unchecked") + PublicKeyEntryDecoder<PublicKey, ?> decoder = + (PublicKeyEntryDecoder<PublicKey, ?>) 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(); + Base64.Encoder encoder = Base64.getEncoder(); + String b64Data = encoder.encodeToString(bytes); + sb.append(keyType).append(' ').append(b64Data); + } + + 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; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java new file mode 100644 index 0000000..7462e4a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java @@ -0,0 +1,114 @@ +/* + * 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.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StreamCorruptedException; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Collection; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * Represents a decoder of an {@code OpenSSH} encoded key data + * + * @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 PublicKeyEntryDecoder<PUB extends PublicKey, PRV extends PrivateKey> + extends KeyEntryResolver<PUB, PRV>, PublicKeyEntryResolver { + + @Override + default PublicKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException { + ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type provided"); + Collection<String> supported = getSupportedTypeNames(); + if ((GenericUtils.size(supported) > 0) && supported.contains(keyType)) { + return decodePublicKey(keyData); + } + + throw new InvalidKeySpecException("resolve(" + keyType + ") not in listed supported types: " + supported); + } + + /** + * @param keyData The key data bytes in {@code OpenSSH} format (after + * BASE64 decoding) - ignored if {@code null}/empty + * @return The decoded {@link PublicKey} - or {@code null} if no data + * @throws IOException If failed to decode the key + * @throws GeneralSecurityException If failed to generate the key + */ + default PUB decodePublicKey(byte... keyData) throws IOException, GeneralSecurityException { + return decodePublicKey(keyData, 0, NumberUtils.length(keyData)); + } + + default PUB decodePublicKey(byte[] keyData, int offset, int length) throws IOException, GeneralSecurityException { + if (length <= 0) { + return null; + } + + try (InputStream stream = new ByteArrayInputStream(keyData, offset, length)) { + return decodePublicKey(stream); + } + } + + default PUB decodePublicKey(InputStream keyData) throws IOException, GeneralSecurityException { + // the actual data is preceded by a string that repeats the key type + String type = KeyEntryResolver.decodeString(keyData); + if (GenericUtils.isEmpty(type)) { + throw new StreamCorruptedException("Missing key type string"); + } + + Collection<String> supported = getSupportedTypeNames(); + if (GenericUtils.isEmpty(supported) || (!supported.contains(type))) { + throw new InvalidKeySpecException("Reported key type (" + type + ") not in supported list: " + supported); + } + + return decodePublicKey(type, keyData); + } + + /** + * @param keyType The reported / encode key type + * @param keyData The key data bytes stream positioned after the key type decoding + * and making sure it is one of the supported types + * @return The decoded {@link PublicKey} + * @throws IOException If failed to read from the data stream + * @throws GeneralSecurityException If failed to generate the key + */ + PUB decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException; + + /** + * Encodes the {@link PublicKey} using the {@code OpenSSH} format - same + * one used by the {@code decodePublicKey} method(s) + * + * @param s The {@link OutputStream} to write the data to + * @param key The {@link PublicKey} - may not be {@code null} + * @return The key type value - one of the {@link #getSupportedTypeNames()} + * @throws IOException If failed to generate the encoding + */ + String encodePublicKey(OutputStream s, PUB key) throws IOException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryResolver.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryResolver.java new file mode 100644 index 0000000..e5eb0ed --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryResolver.java @@ -0,0 +1,70 @@ +/* + * 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.security.GeneralSecurityException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface PublicKeyEntryResolver { + /** + * A resolver that ignores all input + */ + PublicKeyEntryResolver IGNORING = new PublicKeyEntryResolver() { + @Override + public PublicKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException { + return null; + } + + @Override + public String toString() { + return "IGNORING"; + } + }; + + /** + * A resolver that fails on all input + */ + PublicKeyEntryResolver FAILING = new PublicKeyEntryResolver() { + @Override + public PublicKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException { + throw new InvalidKeySpecException("Failing resolver on key type=" + keyType); + } + + @Override + public String toString() { + return "FAILING"; + } + }; + + /** + * @param keyType The {@code OpenSSH} reported key type + * @param keyData The {@code OpenSSH} encoded key data + * @return The extracted {@link PublicKey} - ignored if {@code null} + * @throws IOException If failed to parse the key data + * @throws GeneralSecurityException If failed to generate the key + */ + PublicKey resolve(String keyType, byte[] keyData) throws IOException, GeneralSecurityException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractIdentityResourceLoader.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractIdentityResourceLoader.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractIdentityResourceLoader.java new file mode 100644 index 0000000..a2412bd --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractIdentityResourceLoader.java @@ -0,0 +1,62 @@ +/* + * 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.impl; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collection; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.IdentityResourceLoader; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * @param <PUB> Generic public key type + * @param <PRV> Generic private key type + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractIdentityResourceLoader<PUB extends PublicKey, PRV extends PrivateKey> + extends AbstractLoggingBean + implements IdentityResourceLoader<PUB, PRV> { + private final Class<PUB> pubType; + private final Class<PRV> prvType; + private final Collection<String> names; + + protected AbstractIdentityResourceLoader(Class<PUB> pubType, Class<PRV> prvType, Collection<String> names) { + this.pubType = Objects.requireNonNull(pubType, "No public key type specified"); + this.prvType = Objects.requireNonNull(prvType, "No private key type specified"); + this.names = ValidateUtils.checkNotNullAndNotEmpty(names, "No type names provided"); + } + + @Override + public final Class<PUB> getPublicKeyType() { + return pubType; + } + + @Override + public final Class<PRV> getPrivateKeyType() { + return prvType; + } + + @Override + public Collection<String> getSupportedTypeNames() { + return names; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractKeyEntryResolver.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractKeyEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractKeyEntryResolver.java new file mode 100644 index 0000000..7afa3a6 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractKeyEntryResolver.java @@ -0,0 +1,59 @@ +/* + * 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.impl; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.KeySpec; +import java.util.Collection; + +import org.apache.sshd.common.config.keys.KeyEntryResolver; + +/** + * @param <PUB> Type of {@link PublicKey} + * @param <PRV> Type of {@link PrivateKey} + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractKeyEntryResolver<PUB extends PublicKey, PRV extends PrivateKey> + extends AbstractIdentityResourceLoader<PUB, PRV> + implements KeyEntryResolver<PUB, PRV> { + protected AbstractKeyEntryResolver(Class<PUB> pubType, Class<PRV> prvType, Collection<String> names) { + super(pubType, prvType, names); + } + + public PUB generatePublicKey(KeySpec keySpec) throws GeneralSecurityException { + KeyFactory factory = getKeyFactoryInstance(); + Class<PUB> keyType = getPublicKeyType(); + return keyType.cast(factory.generatePublic(keySpec)); + } + + public PRV generatePrivateKey(KeySpec keySpec) throws GeneralSecurityException { + KeyFactory factory = getKeyFactoryInstance(); + Class<PRV> keyType = getPrivateKeyType(); + return keyType.cast(factory.generatePrivate(keySpec)); + } + + @Override + public String toString() { + return getPublicKeyType().getSimpleName() + ": " + getSupportedTypeNames(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPrivateKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPrivateKeyEntryDecoder.java new file mode 100644 index 0000000..574412e --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPrivateKeyEntryDecoder.java @@ -0,0 +1,40 @@ +/* + * 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.impl; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collection; + +import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; + +/** + * @param <PUB> Type of {@link PublicKey} + * @param <PRV> Type of {@link PrivateKey} + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractPrivateKeyEntryDecoder<PUB extends PublicKey, PRV extends PrivateKey> + extends AbstractKeyEntryResolver<PUB, PRV> + implements PrivateKeyEntryDecoder<PUB, PRV> { + protected AbstractPrivateKeyEntryDecoder(Class<PUB> pubType, Class<PRV> prvType, Collection<String> names) { + super(pubType, prvType, names); + } + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPublicKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPublicKeyEntryDecoder.java new file mode 100644 index 0000000..59cbf3a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPublicKeyEntryDecoder.java @@ -0,0 +1,41 @@ +/* + * 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.impl; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collection; + +import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder; + +/** + * Useful base class implementation for a decoder of an {@code OpenSSH} encoded key data + * + * @param <PUB> Type of {@link PublicKey} + * @param <PRV> Type of {@link PrivateKey} + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractPublicKeyEntryDecoder<PUB extends PublicKey, PRV extends PrivateKey> + extends AbstractKeyEntryResolver<PUB, PRV> + implements PublicKeyEntryDecoder<PUB, PRV> { + protected AbstractPublicKeyEntryDecoder(Class<PUB> pubType, Class<PRV> prvType, Collection<String> names) { + super(pubType, prvType, names); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/DSSPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/DSSPublicKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/DSSPublicKeyEntryDecoder.java new file mode 100644 index 0000000..464d2b0 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/DSSPublicKeyEntryDecoder.java @@ -0,0 +1,119 @@ +/* + * 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.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DSSPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<DSAPublicKey, DSAPrivateKey> { + public static final DSSPublicKeyEntryDecoder INSTANCE = new DSSPublicKeyEntryDecoder(); + + public DSSPublicKeyEntryDecoder() { + super(DSAPublicKey.class, DSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_DSS))); + } + + @Override + public DSAPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_DSS.equals(keyType)) { // just in case we were invoked directly + throw new InvalidKeySpecException("Unexpected key type: " + keyType); + } + + BigInteger p = KeyEntryResolver.decodeBigInt(keyData); + BigInteger q = KeyEntryResolver.decodeBigInt(keyData); + BigInteger g = KeyEntryResolver.decodeBigInt(keyData); + BigInteger y = KeyEntryResolver.decodeBigInt(keyData); + + return generatePublicKey(new DSAPublicKeySpec(y, p, q, g)); + } + + @Override + public String encodePublicKey(OutputStream s, DSAPublicKey key) throws IOException { + Objects.requireNonNull(key, "No public key provided"); + + DSAParams keyParams = Objects.requireNonNull(key.getParams(), "No DSA params available"); + KeyEntryResolver.encodeString(s, KeyPairProvider.SSH_DSS); + KeyEntryResolver.encodeBigInt(s, keyParams.getP()); + KeyEntryResolver.encodeBigInt(s, keyParams.getQ()); + KeyEntryResolver.encodeBigInt(s, keyParams.getG()); + KeyEntryResolver.encodeBigInt(s, key.getY()); + + return KeyPairProvider.SSH_DSS; + } + + @Override + public DSAPublicKey clonePublicKey(DSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + DSAParams params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePublicKey(new DSAPublicKeySpec(key.getY(), params.getP(), params.getQ(), params.getG())); + } + + @Override + public DSAPrivateKey clonePrivateKey(DSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + DSAParams params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePrivateKey(new DSAPrivateKeySpec(key.getX(), params.getP(), params.getQ(), params.getG())); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(KeyUtils.DSS_ALGORITHM); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(KeyUtils.DSS_ALGORITHM); + } +}
