http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java new file mode 100644 index 0000000..95db256 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java @@ -0,0 +1,358 @@ +/* + * 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.loader.openssh; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; +import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder; +import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * Basic support for <A HREF="http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?rev=1.1&content-type=text/x-cvsweb-markup">OpenSSH key file(s)</A> + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser { + public static final String BEGIN_MARKER = "BEGIN OPENSSH PRIVATE KEY"; + public static final List<String> BEGINNERS = + Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END OPENSSH PRIVATE KEY"; + public static final List<String> ENDERS = + Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + public static final String AUTH_MAGIC = "openssh-key-v1"; + public static final OpenSSHKeyPairResourceParser INSTANCE = new OpenSSHKeyPairResourceParser(); + + private static final byte[] AUTH_MAGIC_BYTES = AUTH_MAGIC.getBytes(StandardCharsets.UTF_8); + private static final Map<String, PrivateKeyEntryDecoder<?, ?>> BY_KEY_TYPE_DECODERS_MAP = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private static final Map<Class<?>, PrivateKeyEntryDecoder<?, ?>> BY_KEY_CLASS_DECODERS_MAP = + new HashMap<>(); + + static { + registerPrivateKeyEntryDecoder(OpenSSHRSAPrivateKeyDecoder.INSTANCE); + registerPrivateKeyEntryDecoder(OpenSSHDSSPrivateKeyEntryDecoder.INSTANCE); + + if (SecurityUtils.isECCSupported()) { + registerPrivateKeyEntryDecoder(OpenSSHECDSAPrivateKeyEntryDecoder.INSTANCE); + } + if (SecurityUtils.isEDDSACurveSupported()) { + registerPrivateKeyEntryDecoder(SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder()); + } + } + + public OpenSSHKeyPairResourceParser() { + super(BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + stream = validateStreamMagicMarker(resourceKey, stream); + + String cipher = KeyEntryResolver.decodeString(stream); + if (!OpenSSHParserContext.IS_NONE_CIPHER.test(cipher)) { + throw new NoSuchAlgorithmException("Unsupported cipher: " + cipher); + } + + boolean debugEnabled = log.isDebugEnabled(); + if (debugEnabled) { + log.debug("extractKeyPairs({}) cipher={}", resourceKey, cipher); + } + + String kdfName = KeyEntryResolver.decodeString(stream); + if (!OpenSSHParserContext.IS_NONE_KDF.test(kdfName)) { + throw new NoSuchAlgorithmException("Unsupported KDF: " + kdfName); + } + + byte[] kdfOptions = KeyEntryResolver.readRLEBytes(stream); + if (debugEnabled) { + log.debug("extractKeyPairs({}) KDF={}, options={}", + resourceKey, kdfName, BufferUtils.toHex(':', kdfOptions)); + } + + int numKeys = KeyEntryResolver.decodeInt(stream); + if (numKeys <= 0) { + if (debugEnabled) { + log.debug("extractKeyPairs({}) no encoded keys", resourceKey); + } + return Collections.emptyList(); + } + + List<PublicKey> publicKeys = new ArrayList<>(numKeys); + OpenSSHParserContext context = new OpenSSHParserContext(cipher, kdfName, kdfOptions); + boolean traceEnabled = log.isTraceEnabled(); + for (int index = 1; index <= numKeys; index++) { + PublicKey pubKey = readPublicKey(resourceKey, context, stream); + ValidateUtils.checkNotNull(pubKey, "Empty public key #%d in %s", index, resourceKey); + if (traceEnabled) { + log.trace("extractKeyPairs({}) read public key #{}: {} {}", + resourceKey, index, KeyUtils.getKeyType(pubKey), KeyUtils.getFingerPrint(pubKey)); + } + publicKeys.add(pubKey); + } + + byte[] privateData = KeyEntryResolver.readRLEBytes(stream); + try (InputStream bais = new ByteArrayInputStream(privateData)) { + return readPrivateKeys(resourceKey, context, publicKeys, passwordProvider, bais); + } + } + + protected PublicKey readPublicKey( + String resourceKey, OpenSSHParserContext context, InputStream stream) + throws IOException, GeneralSecurityException { + byte[] keyData = KeyEntryResolver.readRLEBytes(stream); + try (InputStream bais = new ByteArrayInputStream(keyData)) { + String keyType = KeyEntryResolver.decodeString(bais); + PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType); + if (decoder == null) { + throw new NoSuchAlgorithmException("Unsupported key type (" + keyType + ") in " + resourceKey); + } + + return decoder.decodePublicKey(keyType, bais); + } + } + + protected List<KeyPair> readPrivateKeys( + String resourceKey, OpenSSHParserContext context, Collection<? extends PublicKey> publicKeys, + FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + if (GenericUtils.isEmpty(publicKeys)) { + return Collections.emptyList(); + } + + boolean traceEnabled = log.isTraceEnabled(); + int check1 = KeyEntryResolver.decodeInt(stream); + int check2 = KeyEntryResolver.decodeInt(stream); + if (traceEnabled) { + log.trace("readPrivateKeys({}) check1=0x{}, check2=0x{}", + resourceKey, Integer.toHexString(check1), Integer.toHexString(check2)); + } + + List<KeyPair> keyPairs = new ArrayList<>(publicKeys.size()); + for (PublicKey pubKey : publicKeys) { + String pubType = KeyUtils.getKeyType(pubKey); + int keyIndex = keyPairs.size() + 1; + if (traceEnabled) { + log.trace("extractKeyPairs({}) read private key #{}: {}", + resourceKey, keyIndex, pubType); + } + + Map.Entry<PrivateKey, String> prvData = readPrivateKey(resourceKey, context, pubType, passwordProvider, stream); + PrivateKey prvKey = (prvData == null) ? null : prvData.getKey(); + ValidateUtils.checkNotNull(prvKey, "Empty private key #%d in %s", keyIndex, resourceKey); + + String prvType = KeyUtils.getKeyType(prvKey); + ValidateUtils.checkTrue(Objects.equals(pubType, prvType), + "Mismatched public (%s) vs. private (%s) key type #%d in %s", + pubType, prvType, keyIndex, resourceKey); + + if (traceEnabled) { + log.trace("extractKeyPairs({}) add private key #{}: {} {}", + resourceKey, keyIndex, prvType, prvData.getValue()); + } + keyPairs.add(new KeyPair(pubKey, prvKey)); + } + + return keyPairs; + } + + protected SimpleImmutableEntry<PrivateKey, String> readPrivateKey( + String resourceKey, OpenSSHParserContext context, String keyType, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + String prvType = KeyEntryResolver.decodeString(stream); + if (!Objects.equals(keyType, prvType)) { + throw new StreamCorruptedException("Mismatched private key type: " + + ", expected=" + keyType + ", actual=" + prvType + + " in " + resourceKey); + } + + PrivateKeyEntryDecoder<?, ?> decoder = getPrivateKeyEntryDecoder(prvType); + if (decoder == null) { + throw new NoSuchAlgorithmException("Unsupported key type (" + prvType + ") in " + resourceKey); + } + + PrivateKey prvKey = decoder.decodePrivateKey(prvType, passwordProvider, stream); + if (prvKey == null) { + throw new InvalidKeyException("Cannot parse key type (" + prvType + ") in " + resourceKey); + } + + String comment = KeyEntryResolver.decodeString(stream); + return new SimpleImmutableEntry<>(prvKey, comment); + } + + protected <S extends InputStream> S validateStreamMagicMarker(String resourceKey, S stream) throws IOException { + byte[] actual = new byte[AUTH_MAGIC_BYTES.length]; + IoUtils.readFully(stream, actual); + if (!Arrays.equals(AUTH_MAGIC_BYTES, actual)) { + throw new StreamCorruptedException(resourceKey + ": Mismatched magic marker value: " + BufferUtils.toHex(':', actual)); + } + + int eos = stream.read(); + if (eos == -1) { + throw new EOFException(resourceKey + ": Premature EOF after magic marker value"); + } + + if (eos != 0) { + throw new StreamCorruptedException(resourceKey + ": Missing EOS after magic marker value: 0x" + Integer.toHexString(eos)); + } + + return stream; + } + + /** + * @param decoder The decoder to register + * @throws IllegalArgumentException if no decoder or not key type or no + * supported names for the decoder + * @see PrivateKeyEntryDecoder#getPublicKeyType() + * @see PrivateKeyEntryDecoder#getSupportedTypeNames() + */ + public static void registerPrivateKeyEntryDecoder(PrivateKeyEntryDecoder<?, ?> 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) { + PrivateKeyEntryDecoder<?, ?> 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 PrivateKeyEntryDecoder} or {code null} if not found + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(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 PrivateKeyEntryDecoder} provided <U>both</U> + * the public and private keys have the same decoder - {@code null} if no + * match found + * @see #getPrivateKeyEntryDecoder(Key) + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(KeyPair kp) { + if (kp == null) { + return null; + } + + PrivateKeyEntryDecoder<?, ?> d1 = getPrivateKeyEntryDecoder(kp.getPublic()); + PrivateKeyEntryDecoder<?, ?> d2 = getPrivateKeyEntryDecoder(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 PrivateKeyEntryDecoder} for this key or {code null} if no match found + * @see #getPrivateKeyEntryDecoder(Class) + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(Key key) { + if (key == null) { + return null; + } else { + return getPrivateKeyEntryDecoder(key.getClass()); + } + } + + /** + * @param keyType The key {@link Class} - ignored if {@code null} or not a {@link Key} + * compatible type + * @return The registered {@link PrivateKeyEntryDecoder} or {code null} if no match found + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(Class<?> keyType) { + if ((keyType == null) || (!Key.class.isAssignableFrom(keyType))) { + return null; + } + + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + PrivateKeyEntryDecoder<?, ?> decoder = BY_KEY_CLASS_DECODERS_MAP.get(keyType); + if (decoder != null) { + return decoder; + } + + // in case it is a derived class + for (PrivateKeyEntryDecoder<?, ?> 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; + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java new file mode 100644 index 0000000..07f2a9a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java @@ -0,0 +1,83 @@ +/* + * 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.loader.openssh; + +import java.util.function.Predicate; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHParserContext { + public static final String NONE_CIPHER = "none"; + public static final Predicate<String> IS_NONE_CIPHER = c -> GenericUtils.isEmpty(c) || NONE_CIPHER.equalsIgnoreCase(c); + + public static final String NONE_KDF = "none"; + public static final Predicate<String> IS_NONE_KDF = c -> GenericUtils.isEmpty(c) || NONE_KDF.equalsIgnoreCase(c); + + private String cipherName; + private String kdfName; + private byte[] kdfOptions; + + public OpenSSHParserContext() { + super(); + } + + public OpenSSHParserContext(String cipherName, String kdfName, byte... kdfOptions) { + this.cipherName = cipherName; + this.kdfName = kdfName; + this.kdfOptions = kdfOptions; + } + + public String getCipherName() { + return cipherName; + } + + public void setCipherName(String cipherName) { + this.cipherName = cipherName; + } + + public String getKdfName() { + return kdfName; + } + + public void setKdfName(String kdfName) { + this.kdfName = kdfName; + } + + public byte[] getKdfOptions() { + return kdfOptions; + } + + public void setKdfOptions(byte[] kdfOptions) { + this.kdfOptions = kdfOptions; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[cipher=" + getCipherName() + + ", kdfName=" + getKdfName() + + ", kdfOptions=" + BufferUtils.toHex(':', getKdfOptions()) + + "]"; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java new file mode 100644 index 0000000..72e003f --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java @@ -0,0 +1,135 @@ +/* + * 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.loader.openssh; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.impl.AbstractPrivateKeyEntryDecoder; +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 OpenSSHRSAPrivateKeyDecoder extends AbstractPrivateKeyEntryDecoder<RSAPublicKey, RSAPrivateKey> { + public static final BigInteger DEFAULT_PUBLIC_EXPONENT = new BigInteger("65537"); + public static final OpenSSHRSAPrivateKeyDecoder INSTANCE = new OpenSSHRSAPrivateKeyDecoder(); + + public OpenSSHRSAPrivateKeyDecoder() { + super(RSAPublicKey.class, RSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_RSA))); + } + + @Override + public RSAPrivateKey decodePrivateKey(String keyType, FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_RSA.equals(keyType)) { // just in case we were invoked directly + throw new InvalidKeySpecException("Unexpected key type: " + keyType); + } + + BigInteger n = KeyEntryResolver.decodeBigInt(keyData); + BigInteger e = KeyEntryResolver.decodeBigInt(keyData); + if (!Objects.equals(e, DEFAULT_PUBLIC_EXPONENT)) { + log.warn("decodePrivateKey({}) non-standard RSA exponent found: {}", keyType, e); + } + + BigInteger d = KeyEntryResolver.decodeBigInt(keyData); + BigInteger inverseQmodP = KeyEntryResolver.decodeBigInt(keyData); + Objects.requireNonNull(inverseQmodP, "Missing iqmodp"); // TODO run some validation on it + BigInteger p = KeyEntryResolver.decodeBigInt(keyData); + BigInteger q = KeyEntryResolver.decodeBigInt(keyData); + BigInteger modulus = p.multiply(q); + if (!Objects.equals(n, modulus)) { + log.warn("decodePrivateKey({}) mismatched modulus values: encoded={}, calculated={}", + keyType, n, modulus); + } + + return generatePrivateKey(new RSAPrivateKeySpec(n, d)); + } + + @Override + public boolean isPublicKeyRecoverySupported() { + return true; + } + + @Override + public RSAPublicKey recoverPublicKey(RSAPrivateKey privateKey) throws GeneralSecurityException { + return KeyUtils.recoverRSAPublicKey(privateKey); + } + + @Override + public RSAPublicKey clonePublicKey(RSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePublicKey(new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent())); + } + } + + @Override + public RSAPrivateKey clonePrivateKey(RSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + if (!(key instanceof RSAPrivateCrtKey)) { + throw new InvalidKeyException("Cannot clone a non-RSAPrivateCrtKey: " + key.getClass().getSimpleName()); + } + + RSAPrivateCrtKey rsaPrv = (RSAPrivateCrtKey) key; + return generatePrivateKey( + new RSAPrivateCrtKeySpec( + rsaPrv.getModulus(), + rsaPrv.getPublicExponent(), + rsaPrv.getPrivateExponent(), + rsaPrv.getPrimeP(), + rsaPrv.getPrimeQ(), + rsaPrv.getPrimeExponentP(), + rsaPrv.getPrimeExponentQ(), + rsaPrv.getCrtCoefficient())); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(KeyUtils.RSA_ALGORITHM); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java new file mode 100644 index 0000000..bee13d6 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java @@ -0,0 +1,167 @@ +/* + * 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.loader.pem; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.security.auth.login.CredentialException; +import javax.security.auth.login.FailedLoginException; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser; +import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser; +import org.apache.sshd.common.config.keys.loader.PrivateKeyEncryptionContext; +import org.apache.sshd.common.config.keys.loader.PrivateKeyObfuscator; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * Base class for PEM file key-pair loaders + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractPEMResourceKeyPairParser + extends AbstractKeyPairResourceParser + implements KeyPairPEMResourceParser { + private final String algo; + private final String algId; + + protected AbstractPEMResourceKeyPairParser(String algo, String algId, List<String> beginners, List<String> enders) { + super(beginners, enders); + this.algo = ValidateUtils.checkNotNullAndNotEmpty(algo, "No encryption algorithm provided"); + this.algId = ValidateUtils.checkNotNullAndNotEmpty(algId, "No algorithm identifier provided"); + } + + @Override + public String getAlgorithm() { + return algo; + } + + @Override + public String getAlgorithmIdentifier() { + return algId; + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException { + if (GenericUtils.isEmpty(lines)) { + return Collections.emptyList(); + } + + Boolean encrypted = null; + byte[] initVector = null; + String algInfo = null; + int dataStartIndex = -1; + for (int index = 0; index < lines.size(); index++) { + String line = GenericUtils.trimToEmpty(lines.get(index)); + if (GenericUtils.isEmpty(line)) { + continue; + } + + // check if header line - if not, assume data lines follow + int headerPos = line.indexOf(':'); + if (headerPos < 0) { + dataStartIndex = index; + break; + } + + if (line.startsWith("Proc-Type:")) { + if (encrypted != null) { + throw new StreamCorruptedException("Multiple encryption indicators in " + resourceKey); + } + + line = line.substring(headerPos + 1).trim(); + line = line.toUpperCase(); + encrypted = Boolean.valueOf(line.contains("ENCRYPTED")); + } else if (line.startsWith("DEK-Info:")) { + if ((initVector != null) || (algInfo != null)) { + throw new StreamCorruptedException("Multiple encryption settings in " + resourceKey); + } + + line = line.substring(headerPos + 1).trim(); + headerPos = line.indexOf(','); + if (headerPos < 0) { + throw new StreamCorruptedException(resourceKey + ": Missing encryption data values separator in line '" + line + "'"); + } + + algInfo = line.substring(0, headerPos).trim(); + + String algInitVector = line.substring(headerPos + 1).trim(); + initVector = BufferUtils.decodeHex(BufferUtils.EMPTY_HEX_SEPARATOR, algInitVector); + } + } + + if (dataStartIndex < 0) { + throw new StreamCorruptedException("No data lines (only headers or empty) found in " + resourceKey); + } + + List<String> dataLines = lines.subList(dataStartIndex, lines.size()); + if ((encrypted != null) || (algInfo != null) || (initVector != null)) { + if (passwordProvider == null) { + throw new CredentialException("Missing password provider for encrypted resource=" + resourceKey); + } + + String password = passwordProvider.getPassword(resourceKey); + if (GenericUtils.isEmpty(password)) { + throw new FailedLoginException("No password data for encrypted resource=" + resourceKey); + } + + PrivateKeyEncryptionContext encContext = new PrivateKeyEncryptionContext(algInfo); + encContext.setPassword(password); + encContext.setInitVector(initVector); + byte[] encryptedData = KeyPairResourceParser.extractDataBytes(dataLines); + byte[] decodedData = applyPrivateKeyCipher(encryptedData, encContext, false); + try (InputStream bais = new ByteArrayInputStream(decodedData)) { + return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, bais); + } + } + + return super.extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, dataLines); + } + + protected byte[] applyPrivateKeyCipher(byte[] bytes, PrivateKeyEncryptionContext encContext, boolean encryptIt) throws GeneralSecurityException { + String cipherName = encContext.getCipherName(); + PrivateKeyObfuscator o = encContext.resolvePrivateKeyObfuscator(); + if (o == null) { + throw new NoSuchAlgorithmException("decryptPrivateKeyData(" + encContext + ")[encrypt=" + encryptIt + "] unknown cipher: " + cipherName); + } + + if (encryptIt) { + byte[] initVector = encContext.getInitVector(); + if (GenericUtils.isEmpty(initVector)) { + initVector = o.generateInitializationVector(encContext); + encContext.setInitVector(initVector); + } + } + + return o.applyPrivateKeyCipher(bytes, encContext, encryptIt); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java new file mode 100644 index 0000000..0c357af --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java @@ -0,0 +1,126 @@ +/* + * 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.loader.pem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.der.ASN1Object; +import org.apache.sshd.common.util.io.der.ASN1Type; +import org.apache.sshd.common.util.io.der.DERParser; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DSSPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { + // Not exactly according to standard but good enough + public static final String BEGIN_MARKER = "BEGIN DSA PRIVATE KEY"; + public static final List<String> BEGINNERS = + Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END DSA PRIVATE KEY"; + public static final List<String> ENDERS = + Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + /** + * @see <A HREF="https://tools.ietf.org/html/rfc3279#section-2.3.2">RFC-3279 section 2.3.2</A> + */ + public static final String DSS_OID = "1.2.840.10040.4.1"; + + public static final DSSPEMResourceKeyPairParser INSTANCE = new DSSPEMResourceKeyPairParser(); + + public DSSPEMResourceKeyPairParser() { + super(KeyUtils.DSS_ALGORITHM, DSS_OID, BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + KeyPair kp = decodeDSSKeyPair(SecurityUtils.getKeyFactory(KeyUtils.DSS_ALGORITHM), stream, false); + return Collections.singletonList(kp); + } + + /** + * <p>The ASN.1 syntax for the private key:</P> + * <pre><code> + * DSAPrivateKey ::= SEQUENCE { + * version Version, + * p INTEGER, + * q INTEGER, + * g INTEGER, + * y INTEGER, + * x INTEGER + * } + * </code></pre> + * @param kf The {@link KeyFactory} To use to generate the keys + * @param s The {@link InputStream} containing the encoded bytes + * @param okToClose <code>true</code> if the method may close the input + * stream regardless of success or failure + * @return The recovered {@link KeyPair} + * @throws IOException If failed to read or decode the bytes + * @throws GeneralSecurityException If failed to generate the keys + */ + public static KeyPair decodeDSSKeyPair(KeyFactory kf, InputStream s, boolean okToClose) + throws IOException, GeneralSecurityException { + ASN1Object sequence; + try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(s, okToClose))) { + sequence = parser.readObject(); + } + + if (!ASN1Type.SEQUENCE.equals(sequence.getObjType())) { + throw new IOException("Invalid DER: not a sequence: " + sequence.getObjType()); + } + + // Parse inside the sequence + try (DERParser parser = sequence.createParser()) { + // Skip version + ASN1Object version = parser.readObject(); + if (version == null) { + throw new StreamCorruptedException("No version"); + } + + BigInteger p = parser.readObject().asInteger(); + BigInteger q = parser.readObject().asInteger(); + BigInteger g = parser.readObject().asInteger(); + BigInteger y = parser.readObject().asInteger(); + BigInteger x = parser.readObject().asInteger(); + PublicKey pubKey = kf.generatePublic(new DSAPublicKeySpec(y, p, q, g)); + PrivateKey prvKey = kf.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)); + return new KeyPair(pubKey, prvKey); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java new file mode 100644 index 0000000..dd8d2ea --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java @@ -0,0 +1,220 @@ +/* + * 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.loader.pem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.der.ASN1Object; +import org.apache.sshd.common.util.io.der.ASN1Type; +import org.apache.sshd.common.util.io.der.DERParser; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { + public static final String BEGIN_MARKER = "BEGIN EC PRIVATE KEY"; + public static final List<String> BEGINNERS = + Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END EC PRIVATE KEY"; + public static final List<String> ENDERS = + Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + /** + * @see <A HREF="https://tools.ietf.org/html/rfc3279#section-2.3.5">RFC-3279 section 2.3.5</A> + */ + public static final String ECDSA_OID = "1.2.840.10045.2.1"; + + public static final ECDSAPEMResourceKeyPairParser INSTANCE = new ECDSAPEMResourceKeyPairParser(); + + public ECDSAPEMResourceKeyPairParser() { + super(KeyUtils.EC_ALGORITHM, ECDSA_OID, BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + Map.Entry<ECPublicKeySpec, ECPrivateKeySpec> spec = decodeECPrivateKeySpec(stream, false); + if (!SecurityUtils.isECCSupported()) { + throw new NoSuchProviderException("ECC not supported"); + } + + KeyFactory kf = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM); + ECPublicKey pubKey = (ECPublicKey) kf.generatePublic(spec.getKey()); + ECPrivateKey prvKey = (ECPrivateKey) kf.generatePrivate(spec.getValue()); + KeyPair kp = new KeyPair(pubKey, prvKey); + return Collections.singletonList(kp); + } + + /** + * <P>ASN.1 syntax according to rfc5915 is:</P></BR> + * <PRE><CODE> + * ECPrivateKey ::= SEQUENCE { + * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + * privateKey OCTET STRING, + * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + * publicKey [1] BIT STRING OPTIONAL + * } + * </CODE></PRE> + * <P><I>ECParameters</I> syntax according to RFC5480:</P></BR> + * <PRE><CODE> + * ECParameters ::= CHOICE { + * namedCurve OBJECT IDENTIFIER + * -- implicitCurve NULL + * -- specifiedCurve SpecifiedECDomain + * } + * </CODE></PRE> + * @param inputStream The {@link InputStream} containing the DER encoded data + * @param okToClose {@code true} if OK to close the DER stream once parsing complete + * @return The decoded {@link SimpleImmutableEntry} of {@link ECPublicKeySpec} and {@link ECPrivateKeySpec} + * @throws IOException If failed to to decode the DER stream + */ + public static SimpleImmutableEntry<ECPublicKeySpec, ECPrivateKeySpec> decodeECPrivateKeySpec(InputStream inputStream, boolean okToClose) throws IOException { + ASN1Object sequence; + try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) { + sequence = parser.readObject(); + } + + if (!ASN1Type.SEQUENCE.equals(sequence.getObjType())) { + throw new IOException("Invalid DER: not a sequence: " + sequence.getObjType()); + } + + // Parse inside the sequence + try (DERParser parser = sequence.createParser()) { + ECPrivateKeySpec prvSpec = decodeECPrivateKeySpec(parser); + ECCurves curve = ECCurves.fromCurveParameters(prvSpec.getParams()); + if (curve == null) { + throw new StreamCorruptedException("Unknown curve"); + } + + ECPoint w = decodeECPublicKeyValue(curve, parser); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, prvSpec.getParams()); + return new SimpleImmutableEntry<>(pubSpec, prvSpec); + } + } + + public static final ECPrivateKeySpec decodeECPrivateKeySpec(DERParser parser) throws IOException { + // see openssl asn1parse -inform PEM -in ...file... -dump + ASN1Object versionObject = parser.readObject(); // Skip version + if (versionObject == null) { + throw new StreamCorruptedException("No version"); + } + + // as per RFC-5915 section 3 + BigInteger version = versionObject.asInteger(); + if (!BigInteger.ONE.equals(version)) { + throw new StreamCorruptedException("Bad version value: " + version); + } + + ASN1Object keyObject = parser.readObject(); + if (keyObject == null) { + throw new StreamCorruptedException("No private key value"); + } + + ASN1Type objType = keyObject.getObjType(); + if (!ASN1Type.OCTET_STRING.equals(objType)) { + throw new StreamCorruptedException("Non-matching private key object type: " + objType); + } + + ASN1Object paramsObject = parser.readObject(); + if (paramsObject == null) { + throw new StreamCorruptedException("No parameters value"); + } + + // TODO make sure params object tag is 0xA0 + + final List<Integer> curveOID; + try (DERParser paramsParser = paramsObject.createParser()) { + ASN1Object namedCurve = paramsParser.readObject(); + if (namedCurve == null) { + throw new StreamCorruptedException("Missing named curve parameter"); + } + + curveOID = namedCurve.asOID(); + } + + ECCurves curve = ECCurves.fromOIDValue(curveOID); + if (curve == null) { + throw new StreamCorruptedException("Unknown curve OID: " + curveOID); + } + + BigInteger s = ECCurves.octetStringToInteger(keyObject.getPureValueBytes()); + return new ECPrivateKeySpec(s, curve.getParameters()); + } + + /** + * <P>ASN.1 syntax according to rfc5915 is:</P></BR> + * <PRE> + * publicKey [1] BIT STRING OPTIONAL + * </PRE> + * @param curve The {@link ECCurves} curve + * @param parser The {@link DERParser} assumed to be positioned at the + * start of the data + * @return The encoded {@link ECPoint} + * @throws IOException If failed to create the point + */ + public static final ECPoint decodeECPublicKeyValue(ECCurves curve, DERParser parser) throws IOException { + // see openssl asn1parse -inform PEM -in ...file... -dump + ASN1Object dataObject = parser.readObject(); + if (dataObject == null) { + throw new StreamCorruptedException("No public key data bytes"); + } + + try (DERParser dataParser = dataObject.createParser()) { + ASN1Object pointData = dataParser.readObject(); + if (pointData == null) { + throw new StreamCorruptedException("Missing public key data parameter"); + } + + ASN1Type objType = pointData.getObjType(); + if (!ASN1Type.BIT_STRING.equals(objType)) { + throw new StreamCorruptedException("Non-matching public key object type: " + objType); + } + + // see https://tools.ietf.org/html/rfc5480#section-2.2 + byte[] octets = pointData.getValue(); + return ECCurves.octetStringToEcPoint(octets); + } + } + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/KeyPairPEMResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/KeyPairPEMResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/KeyPairPEMResourceParser.java new file mode 100644 index 0000000..07c7e44 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/KeyPairPEMResourceParser.java @@ -0,0 +1,37 @@ +/* + * 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.loader.pem; + +import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KeyPairPEMResourceParser extends KeyPairResourceParser { + /** + * @return The encryption algorithm name - e.g., "RSA", "DSA" + */ + String getAlgorithm(); + + /** + * @return The OID used to identify this algorithm in DER encodings - e.g., RSA=1.2.840.113549.1.1.1 + */ + String getAlgorithmIdentifier(); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PEMResourceParserUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PEMResourceParserUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PEMResourceParserUtils.java new file mode 100644 index 0000000..b6749da --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PEMResourceParserUtils.java @@ -0,0 +1,109 @@ +/* + * 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.loader.pem; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class PEMResourceParserUtils { + public static final KeyPairResourceParser PROXY = new KeyPairResourceParser() { + @Override + public Collection<KeyPair> loadKeyPairs( + String resourceKey, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException { + @SuppressWarnings("synthetic-access") + KeyPairResourceParser proxy = PROXY_HOLDER.get(); + return (proxy == null) ? Collections.<KeyPair>emptyList() : proxy.loadKeyPairs(resourceKey, passwordProvider, lines); + } + + @Override + public boolean canExtractKeyPairs(String resourceKey, List<String> lines) + throws IOException, GeneralSecurityException { + @SuppressWarnings("synthetic-access") + KeyPairResourceParser proxy = PROXY_HOLDER.get(); + return (proxy != null) && proxy.canExtractKeyPairs(resourceKey, lines); + } + }; + + private static final Map<String, KeyPairPEMResourceParser> BY_OID_MAP = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private static final Map<String, KeyPairPEMResourceParser> BY_ALGORITHM_MAP = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private static final AtomicReference<KeyPairResourceParser> PROXY_HOLDER = new AtomicReference<>(KeyPairResourceParser.EMPTY); + + static { + registerPEMResourceParser(RSAPEMResourceKeyPairParser.INSTANCE); + registerPEMResourceParser(DSSPEMResourceKeyPairParser.INSTANCE); + registerPEMResourceParser(ECDSAPEMResourceKeyPairParser.INSTANCE); + registerPEMResourceParser(PKCS8PEMResourceKeyPairParser.INSTANCE); + } + + private PEMResourceParserUtils() { + throw new UnsupportedOperationException("No instance"); + } + + public static void registerPEMResourceParser(KeyPairPEMResourceParser parser) { + Objects.requireNonNull(parser, "No parser to register"); + synchronized (BY_OID_MAP) { + BY_OID_MAP.put(ValidateUtils.checkNotNullAndNotEmpty(parser.getAlgorithmIdentifier(), "No OID value"), parser); + } + + synchronized (BY_ALGORITHM_MAP) { + BY_ALGORITHM_MAP.put(ValidateUtils.checkNotNullAndNotEmpty(parser.getAlgorithm(), "No algorithm value"), parser); + // Use a copy in order to avoid concurrent modifications + PROXY_HOLDER.set(KeyPairResourceParser.aggregate(new ArrayList<>(BY_ALGORITHM_MAP.values()))); + } + } + + public static KeyPairPEMResourceParser getPEMResourceParserByOid(String oid) { + if (GenericUtils.isEmpty(oid)) { + return null; + } + + synchronized (BY_OID_MAP) { + return BY_OID_MAP.get(oid); + } + } + + public static KeyPairPEMResourceParser getPEMResourceParserByAlgorithm(String algorithm) { + if (GenericUtils.isEmpty(algorithm)) { + return null; + } + + synchronized (BY_ALGORITHM_MAP) { + return BY_ALGORITHM_MAP.get(algorithm); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java new file mode 100644 index 0000000..b333f23 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java @@ -0,0 +1,156 @@ +/* + * 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.loader.pem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.io.der.ASN1Object; +import org.apache.sshd.common.util.io.der.DERParser; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { + // Not exactly according to standard but good enough + public static final String BEGIN_MARKER = "BEGIN PRIVATE KEY"; + public static final List<String> BEGINNERS = + Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END PRIVATE KEY"; + public static final List<String> ENDERS = + Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + public static final String PKCS8_FORMAT = "PKCS#8"; + + public static final PKCS8PEMResourceKeyPairParser INSTANCE = new PKCS8PEMResourceKeyPairParser(); + + public PKCS8PEMResourceKeyPairParser() { + super(PKCS8_FORMAT, PKCS8_FORMAT, BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + // Save the data before getting the algorithm OID since we will need it + byte[] encBytes = IoUtils.toByteArray(stream); + List<Integer> oidAlgorithm = getPKCS8AlgorithmIdentifier(encBytes); + PrivateKey prvKey = decodePEMPrivateKeyPKCS8(oidAlgorithm, encBytes, passwordProvider); + PublicKey pubKey = ValidateUtils.checkNotNull(KeyUtils.recoverPublicKey(prvKey), + "Failed to recover public key of OID=%s", oidAlgorithm); + KeyPair kp = new KeyPair(pubKey, prvKey); + return Collections.singletonList(kp); + } + + public static PrivateKey decodePEMPrivateKeyPKCS8( + List<Integer> oidAlgorithm, byte[] keyBytes, FilePasswordProvider passwordProvider) + throws GeneralSecurityException { + ValidateUtils.checkNotNullAndNotEmpty(oidAlgorithm, "No PKCS8 algorithm OID"); + return decodePEMPrivateKeyPKCS8(GenericUtils.join(oidAlgorithm, '.'), keyBytes, passwordProvider); + } + + public static PrivateKey decodePEMPrivateKeyPKCS8( + String oid, byte[] keyBytes, FilePasswordProvider passwordProvider) + throws GeneralSecurityException { + KeyPairPEMResourceParser parser = + PEMResourceParserUtils.getPEMResourceParserByOid( + ValidateUtils.checkNotNullAndNotEmpty(oid, "No PKCS8 algorithm OID")); + if (parser == null) { + throw new NoSuchAlgorithmException("decodePEMPrivateKeyPKCS8(" + oid + ") unknown algorithm identifier"); + } + + String algorithm = ValidateUtils.checkNotNullAndNotEmpty(parser.getAlgorithm(), "No parser algorithm"); + KeyFactory factory = SecurityUtils.getKeyFactory(algorithm); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + return factory.generatePrivate(keySpec); + } + + public static List<Integer> getPKCS8AlgorithmIdentifier(byte[] input) throws IOException { + try (DERParser parser = new DERParser(input)) { + return getPKCS8AlgorithmIdentifier(parser); + } + } + + /** + * According to the standard: + * <PRE><CODE> + * PrivateKeyInfo ::= SEQUENCE { + * version Version, + * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + * privateKey PrivateKey, + * attributes [0] IMPLICIT Attributes OPTIONAL + * } + * + * Version ::= INTEGER + * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier + * PrivateKey ::= OCTET STRING + * Attributes ::= SET OF Attribute + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + * </CODE></PRE> + * @param parser The {@link DERParser} to use + * @return The PKCS8 algorithm OID + * @throws IOException If malformed data + * @see #getPKCS8AlgorithmIdentifier(ASN1Object) + */ + public static List<Integer> getPKCS8AlgorithmIdentifier(DERParser parser) throws IOException { + return getPKCS8AlgorithmIdentifier(parser.readObject()); + } + + public static List<Integer> getPKCS8AlgorithmIdentifier(ASN1Object privateKeyInfo) throws IOException { + try (DERParser parser = privateKeyInfo.createParser()) { + // Skip version + ASN1Object versionObject = parser.readObject(); + if (versionObject == null) { + throw new StreamCorruptedException("No version"); + } + + ASN1Object privateKeyAlgorithm = parser.readObject(); + if (privateKeyAlgorithm == null) { + throw new StreamCorruptedException("No private key algorithm"); + } + + try (DERParser oidParser = privateKeyAlgorithm.createParser()) { + ASN1Object oid = oidParser.readObject(); + return oid.asOID(); + } + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java new file mode 100644 index 0000000..d760aaf --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.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.loader.pem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.der.ASN1Object; +import org.apache.sshd.common.util.io.der.ASN1Type; +import org.apache.sshd.common.util.io.der.DERParser; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class RSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { + // Not exactly according to standard but good enough + public static final String BEGIN_MARKER = "BEGIN RSA PRIVATE KEY"; + public static final List<String> BEGINNERS = + Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END RSA PRIVATE KEY"; + public static final List<String> ENDERS = + Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + /** + * @see <A HREF="https://tools.ietf.org/html/rfc3279#section-2.3.1">RFC-3279 section 2.3.1</A> + */ + public static final String RSA_OID = "1.2.840.113549.1.1.1"; + + public static final RSAPEMResourceKeyPairParser INSTANCE = new RSAPEMResourceKeyPairParser(); + + public RSAPEMResourceKeyPairParser() { + super(KeyUtils.RSA_ALGORITHM, RSA_OID, BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + KeyPair kp = decodeRSAKeyPair(SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM), stream, false); + return Collections.singletonList(kp); + } + + /** + * <p>The ASN.1 syntax for the private key as per RFC-3447 section A.1.2:</P> + * <pre><code> + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p-1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * otherPrimeInfos OtherPrimeInfos OPTIONAL + * } + * </code></pre> + * @param kf The {@link KeyFactory} To use to generate the keys + * @param s The {@link InputStream} containing the encoded bytes + * @param okToClose <code>true</code> if the method may close the input + * stream regardless of success or failure + * @return The recovered {@link KeyPair} + * @throws IOException If failed to read or decode the bytes + * @throws GeneralSecurityException If failed to generate the keys + */ + public static KeyPair decodeRSAKeyPair(KeyFactory kf, InputStream s, boolean okToClose) + throws IOException, GeneralSecurityException { + ASN1Object sequence; + try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(s, okToClose))) { + sequence = parser.readObject(); + } + + if (!ASN1Type.SEQUENCE.equals(sequence.getObjType())) { + throw new IOException("Invalid DER: not a sequence: " + sequence.getObjType()); + } + + try (DERParser parser = sequence.createParser()) { + // Skip version + ASN1Object versionObject = parser.readObject(); + if (versionObject == null) { + throw new StreamCorruptedException("No version"); + } + + // as per RFC-3447 section A.1.2 + BigInteger version = versionObject.asInteger(); + if (!BigInteger.ZERO.equals(version)) { + throw new StreamCorruptedException("Multi-primes N/A"); + } + + BigInteger modulus = parser.readObject().asInteger(); + BigInteger publicExp = parser.readObject().asInteger(); + PublicKey pubKey = kf.generatePublic(new RSAPublicKeySpec(modulus, publicExp)); + + BigInteger privateExp = parser.readObject().asInteger(); + BigInteger primeP = parser.readObject().asInteger(); + BigInteger primeQ = parser.readObject().asInteger(); + BigInteger primeExponentP = parser.readObject().asInteger(); + BigInteger primeExponentQ = parser.readObject().asInteger(); + BigInteger crtCoef = parser.readObject().asInteger(); + RSAPrivateKeySpec prvSpec = new RSAPrivateCrtKeySpec( + modulus, publicExp, privateExp, primeP, primeQ, primeExponentP, primeExponentQ, crtCoef); + PrivateKey prvKey = kf.generatePrivate(prvSpec); + return new KeyPair(pubKey, prvKey); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/BaseDigest.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/BaseDigest.java b/sshd-common/src/main/java/org/apache/sshd/common/digest/BaseDigest.java new file mode 100644 index 0000000..a5ef6f9 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/BaseDigest.java @@ -0,0 +1,156 @@ +/* + * 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.digest; + +import java.security.MessageDigest; +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; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * Base class for Digest algorithms based on the JCE provider. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class BaseDigest implements Digest { + + private final String algorithm; + private final int bsize; + private int h; + private String s; + private MessageDigest md; + + /** + * Create a new digest using the given algorithm and block size. + * The initialization and creation of the underlying {@link MessageDigest} + * object will be done in the {@link #init()} method. + * + * @param algorithm the JCE algorithm to use for this digest + * @param bsize the block size of this digest + */ + public BaseDigest(String algorithm, int bsize) { + this.algorithm = ValidateUtils.checkNotNullAndNotEmpty(algorithm, "No algorithm"); + ValidateUtils.checkTrue(bsize > 0, "Invalid block size: %d", bsize); + this.bsize = bsize; + } + + @Override + public final String getAlgorithm() { + return algorithm; + } + + @Override + public int getBlockSize() { + return bsize; + } + + @Override + public void init() throws Exception { + this.md = SecurityUtils.getMessageDigest(getAlgorithm()); + } + + @Override + public void update(byte[] data) throws Exception { + update(data, 0, NumberUtils.length(data)); + } + + @Override + public void update(byte[] data, int start, int len) throws Exception { + Objects.requireNonNull(md, "Digest not initialized").update(data, start, len); + } + + /** + * @return The current {@link MessageDigest} - may be {@code null} if {@link #init()} not called + */ + protected MessageDigest getMessageDigest() { + return md; + } + + @Override + public byte[] digest() throws Exception { + return Objects.requireNonNull(md, "Digest not initialized").digest(); + } + + @Override + public int hashCode() { + synchronized (this) { + if (h == 0) { + h = Objects.hashCode(getAlgorithm()) + getBlockSize(); + if (h == 0) { + h = 1; + } + } + } + + return h; + } + + @Override + public int compareTo(Digest that) { + if (that == null) { + return -1; // push null(s) to end + } else if (this == that) { + return 0; + } + + String thisAlg = getAlgorithm(); + String thatAlg = that.getAlgorithm(); + int nRes = GenericUtils.safeCompare(thisAlg, thatAlg, false); + if (nRes != 0) { + return nRes; // debug breakpoint + } + + nRes = Integer.compare(this.getBlockSize(), that.getBlockSize()); + if (nRes != 0) { + return nRes; // debug breakpoint + } + + return 0; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + + int nRes = compareTo((Digest) obj); + return nRes == 0; + } + + @Override + public String toString() { + synchronized (this) { + if (s == null) { + s = getClass().getSimpleName() + "[" + getAlgorithm() + ":" + getBlockSize() + "]"; + } + } + + return s; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/BuiltinDigests.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/BuiltinDigests.java b/sshd-common/src/main/java/org/apache/sshd/common/digest/BuiltinDigests.java new file mode 100644 index 0000000..f469583 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/BuiltinDigests.java @@ -0,0 +1,166 @@ +/* + * 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.digest; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.util.GenericUtils; + +/** + * Provides easy access to the currently implemented digests + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public enum BuiltinDigests implements DigestFactory { + md5(Constants.MD5, "MD5", 16), + sha1(Constants.SHA1, "SHA-1", 20), + sha224(Constants.SHA224, "SHA-224", 28), + sha256(Constants.SHA256, "SHA-256", 32), + sha384(Constants.SHA384, "SHA-384", 48), + sha512(Constants.SHA512, "SHA-512", 64); + + public static final Set<BuiltinDigests> VALUES = + Collections.unmodifiableSet(EnumSet.allOf(BuiltinDigests.class)); + + private final String algorithm; + private final int blockSize; + private final String factoryName; + private final boolean supported; + + BuiltinDigests(String factoryName, String algorithm, int blockSize) { + this.factoryName = factoryName; + this.algorithm = algorithm; + this.blockSize = blockSize; + /* + * This can be done once since in order to change the support the JVM + * needs to be stopped, some unlimited-strength files need be installed + * and then the JVM re-started. Therefore, the answer is not going to + * change while the JVM is running + */ + this.supported = DigestUtils.checkSupported(algorithm); + } + + @Override + public final String getName() { + return factoryName; + } + + @Override + public final String getAlgorithm() { + return algorithm; + } + + @Override + public final int getBlockSize() { + return blockSize; + } + + @Override + public final String toString() { + return getName(); + } + + @Override + public final Digest create() { + return new BaseDigest(getAlgorithm(), getBlockSize()); + } + + @Override + public final boolean isSupported() { + return supported; + } + + /** + * @param s The {@link Enum}'s name - ignored if {@code null}/empty + * @return The matching {@link org.apache.sshd.common.digest.BuiltinDigests} whose {@link Enum#name()} matches + * (case <U>insensitive</U>) the provided argument - {@code null} if no match + */ + public static BuiltinDigests fromString(String s) { + if (GenericUtils.isEmpty(s)) { + return null; + } + + for (BuiltinDigests c : VALUES) { + if (s.equalsIgnoreCase(c.name())) { + return c; + } + } + + return null; + } + + /** + * @param factory The {@link org.apache.sshd.common.NamedFactory} for the cipher - ignored if {@code null} + * @return The matching {@link org.apache.sshd.common.digest.BuiltinDigests} whose factory name matches + * (case <U>insensitive</U>) the digest factory name + * @see #fromFactoryName(String) + */ + public static BuiltinDigests fromFactory(NamedFactory<? extends Digest> factory) { + if (factory == null) { + return null; + } else { + return fromFactoryName(factory.getName()); + } + } + + /** + * @param name The factory name - ignored if {@code null}/empty + * @return The matching {@link org.apache.sshd.common.digest.BuiltinDigests} whose factory name matches + * (case <U>insensitive</U>) the provided name - {@code null} if no match + */ + public static BuiltinDigests fromFactoryName(String name) { + return NamedResource.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES); + } + + /** + * @param d The {@link Digest} instance - ignored if {@code null} + * @return The matching {@link org.apache.sshd.common.digest.BuiltinDigests} whose algorithm matches + * (case <U>insensitive</U>) the digets's algorithm - {@code null} if no match + */ + public static BuiltinDigests fromDigest(Digest d) { + return fromAlgorithm((d == null) ? null : d.getAlgorithm()); + } + + /** + * @param algo The algorithm to find - ignored if {@code null}/empty + * @return The matching {@link org.apache.sshd.common.digest.BuiltinDigests} whose algorithm matches + * (case <U>insensitive</U>) the provided name - {@code null} if no match + */ + public static BuiltinDigests fromAlgorithm(String algo) { + return DigestUtils.findFactoryByAlgorithm(algo, String.CASE_INSENSITIVE_ORDER, VALUES); + } + + public static final class Constants { + public static final String MD5 = "md5"; + public static final String SHA1 = "sha1"; + public static final String SHA224 = "sha224"; + public static final String SHA256 = "sha256"; + public static final String SHA384 = "sha384"; + public static final String SHA512 = "sha512"; + + private Constants() { + throw new UnsupportedOperationException("No instance allowed"); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/Digest.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/Digest.java b/sshd-common/src/main/java/org/apache/sshd/common/digest/Digest.java new file mode 100644 index 0000000..27204ff --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/Digest.java @@ -0,0 +1,36 @@ +/* + * 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.digest; + +/** + * Interface used to compute digests, based on algorithms such as MD5 or SHA1. + * The digest implementation are compared first by the algorithm name (case + * <U>insensitive</U> and second according to the block size + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface Digest extends DigestInformation, Comparable<Digest> { + void init() throws Exception; + + void update(byte[] data) throws Exception; + + void update(byte[] data, int start, int len) throws Exception; + + byte[] digest() throws Exception; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestFactory.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestFactory.java b/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestFactory.java new file mode 100644 index 0000000..f00e7ba --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestFactory.java @@ -0,0 +1,32 @@ +/* + * 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.digest; + +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.OptionalFeature; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +// CHECKSTYLE:OFF +public interface DigestFactory extends DigestInformation, NamedFactory<Digest>, OptionalFeature { + // nothing extra +} +// CHECKSTYLE:ON http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestInformation.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestInformation.java b/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestInformation.java new file mode 100644 index 0000000..ba181dc --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestInformation.java @@ -0,0 +1,36 @@ +/* + * 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.digest; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface DigestInformation { + /** + * @return The digest algorithm name + */ + String getAlgorithm(); + + /** + * @return The number of bytes in the digest's output + */ + int getBlockSize(); + +}
