[SSHD-482] Add support for authorized_keys file
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/e12434d0 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/e12434d0 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/e12434d0 Branch: refs/heads/master Commit: e12434d0d42a24a312b924de7d3731606377c40a Parents: 330d17c Author: Lyor Goldstein <[email protected]> Authored: Wed Jun 3 14:54:50 2015 +0300 Committer: Lyor Goldstein <[email protected]> Committed: Wed Jun 3 14:54:50 2015 +0300 ---------------------------------------------------------------------- .../main/java/org/apache/sshd/SshBuilder.java | 27 ++ .../sshd/agent/common/AbstractAgentClient.java | 2 +- .../sshd/client/auth/UserAuthPublicKey.java | 2 +- .../org/apache/sshd/client/kex/DHGClient.java | 2 +- .../org/apache/sshd/client/kex/DHGEXClient.java | 2 +- .../keyverifier/StaticServerKeyVerifier.java | 2 +- .../org/apache/sshd/common/cipher/ECCurves.java | 23 ++ .../keys/AbstractPublicKeyEntryDecoder.java | 150 +++++++++ .../config/keys/DSSPublicKeyEntryDecoder.java | 63 ++++ .../config/keys/ECDSAPublicKeyEntryDecoder.java | 236 +++++++++++++ .../sshd/common/config/keys/KeyUtils.java | 314 +++++++++++++++++ .../sshd/common/config/keys/PublicKeyEntry.java | 218 ++++++++++++ .../config/keys/PublicKeyEntryDecoder.java | 56 ++++ .../common/config/keys/RSAPublicKeyDecoder.java | 61 ++++ .../sshd/common/kex/BuiltinDHFactories.java | 6 +- .../java/org/apache/sshd/common/kex/ECDH.java | 6 +- .../keyprovider/AbstractKeyPairProvider.java | 2 +- .../common/keyprovider/KeyPairProvider.java | 8 +- .../org/apache/sshd/common/util/Base64.java | 64 +++- .../apache/sshd/common/util/GenericUtils.java | 44 +++ .../org/apache/sshd/common/util/IoUtils.java | 62 ++++ .../org/apache/sshd/common/util/KeyUtils.java | 100 ------ .../common/util/io/ModifiableFileWatcher.java | 169 ++++++++++ .../sshd/server/PublickeyAuthenticator.java | 41 ++- .../auth/CachingPublicKeyAuthenticator.java | 3 +- .../server/config/keys/AuthorizedKeyEntry.java | 335 +++++++++++++++++++ .../keys/AuthorizedKeysAuthenticator.java | 126 +++++++ .../DefaultAuthorizedKeysAuthenticator.java | 161 +++++++++ .../apache/sshd/SinglePublicKeyAuthTest.java | 2 +- .../sshd/common/util/GenericUtilsTest.java | 59 ++++ .../apache/sshd/deprecated/UserAuthAgent.java | 2 +- .../sshd/deprecated/UserAuthPublicKey.java | 2 +- .../config/keys/AuthorizedKeyEntryTest.java | 108 ++++++ .../keys/AuthorizedKeysAuthenticatorTest.java | 124 +++++++ .../DefaultAuthorizedKeysAuthenticatorTest.java | 67 ++++ .../sshd/server/config/keys/authorized_keys | 16 + 36 files changed, 2536 insertions(+), 129 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/SshBuilder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/SshBuilder.java b/sshd-core/src/main/java/org/apache/sshd/SshBuilder.java index 367e379..cd4d7d1 100644 --- a/sshd-core/src/main/java/org/apache/sshd/SshBuilder.java +++ b/sshd-core/src/main/java/org/apache/sshd/SshBuilder.java @@ -56,7 +56,9 @@ import org.apache.sshd.common.session.ConnectionService; import org.apache.sshd.common.signature.BuiltinSignatures; import org.apache.sshd.common.util.ObjectBuilder; import org.apache.sshd.common.util.SecurityUtils; +import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; import org.apache.sshd.server.global.CancelTcpipForwardHandler; import org.apache.sshd.server.global.KeepAliveHandler; import org.apache.sshd.server.global.NoMoreSessionsHandler; @@ -122,6 +124,7 @@ public class SshBuilder { if (compressionFactories == null) { compressionFactories = Arrays.<NamedFactory<Compression>>asList(BuiltinCompressions.none); } + if (macFactories == null) { macFactories = setUpDefaultMacs(false); } @@ -129,6 +132,7 @@ public class SshBuilder { if (fileSystemFactory == null) { fileSystemFactory = new NativeFileSystemFactory(); } + if (tcpipForwarderFactory == null) { tcpipForwarderFactory = new DefaultTcpipForwarderFactory(); } @@ -425,6 +429,17 @@ public class SshBuilder { } }; + protected PublickeyAuthenticator pubkeyAuthenticator; + + public ServerBuilder() { + super(); + } + + public ServerBuilder publickeyAuthenticator(PublickeyAuthenticator auth) { + pubkeyAuthenticator = auth; + return this; + } + @Override protected ServerBuilder fillWithDefaultValues() { super.fillWithDefaultValues(); @@ -446,9 +461,21 @@ public class SshBuilder { if (factory == null) { factory = SshServer.DEFAULT_SSH_SERVER_FACTORY; } + + if (pubkeyAuthenticator == null) { + pubkeyAuthenticator = DefaultAuthorizedKeysAuthenticator.INSTANCE; + } + return me(); } + @Override + public SshServer build(boolean isFillWithDefaultValues) { + SshServer server = super.build(isFillWithDefaultValues); + server.setPublickeyAuthenticator(pubkeyAuthenticator); + return server; + } + /** * @param ignoreUnsupported If {@code true} then all the default * key exchanges are included, regardless of whether they are currently http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentClient.java b/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentClient.java index 9e48c7d..20510ec 100644 --- a/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentClient.java @@ -33,8 +33,8 @@ import java.security.PublicKey; import java.util.List; import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.AbstractLoggingBean; -import org.apache.sshd.common.util.KeyUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthPublicKey.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthPublicKey.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthPublicKey.java index d3a0279..205d6c2 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthPublicKey.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthPublicKey.java @@ -18,7 +18,7 @@ */ package org.apache.sshd.client.auth; -import static org.apache.sshd.common.util.KeyUtils.getKeyType; +import static org.apache.sshd.common.config.keys.KeyUtils.getKeyType; import java.io.IOException; import java.security.KeyPair; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java index 5e98ac2..6efe31a 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java @@ -23,10 +23,10 @@ import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.Signature; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.kex.AbstractDH; import org.apache.sshd.common.kex.DHFactory; import org.apache.sshd.common.session.AbstractSession; -import org.apache.sshd.common.util.KeyUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java index b1a05f9..afe5269 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java @@ -26,10 +26,10 @@ import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.Signature; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.kex.AbstractDH; import org.apache.sshd.common.kex.DHFactory; import org.apache.sshd.common.session.AbstractSession; -import org.apache.sshd.common.util.KeyUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/StaticServerKeyVerifier.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/StaticServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/StaticServerKeyVerifier.java index f0f8aa4..9ebf0d4 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/StaticServerKeyVerifier.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/StaticServerKeyVerifier.java @@ -24,8 +24,8 @@ import java.security.PublicKey; import org.apache.sshd.ClientSession; import org.apache.sshd.client.ServerKeyVerifier; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.AbstractLoggingBean; -import org.apache.sshd.common.util.KeyUtils; /** * Returns the same constant answer {@code true/false} regardless http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java b/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java index 2cc250c..ad42865 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java @@ -23,6 +23,7 @@ import java.security.spec.ECFieldFp; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.EllipticCurve; +import java.util.Collections; import java.util.Map; import java.util.TreeMap; @@ -187,4 +188,26 @@ public class ECCurves { new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", 16), 1); } + + private static final class LazySpecsMapHolder { + private static final Map<String,ECParameterSpec> specsMap = + Collections.unmodifiableMap(new TreeMap<String, ECParameterSpec>(String.CASE_INSENSITIVE_ORDER) { + private static final long serialVersionUID = 1L; // we're not serializing it + + { + put(NISTP256, EllipticCurves.nistp256); + put(NISTP384, EllipticCurves.nistp384); + put(NISTP521, EllipticCurves.nistp521); + } + }); + } + + @SuppressWarnings("synthetic-access") + public static ECParameterSpec getECParameterSpec(String curveName) { + if (GenericUtils.isEmpty(curveName)) { + return null; + } else { + return LazySpecsMapHolder.specsMap.get(curveName); + } + } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java new file mode 100644 index 0000000..fc97f13 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java @@ -0,0 +1,150 @@ +/* + * 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.StreamCorruptedException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Collection; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.IoUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractPublicKeyEntryDecoder<K extends PublicKey> implements PublicKeyEntryDecoder<K> { + private final Class<K> keyType; + private final Collection<String> names; + + protected AbstractPublicKeyEntryDecoder(Class<K> keyType, Collection<String> names) { + this.keyType = ValidateUtils.checkNotNull(keyType, "No key type specified", GenericUtils.EMPTY_OBJECT_ARRAY); + this.names = ValidateUtils.checkNotNullAndNotEmpty(names, "No type names provided", GenericUtils.EMPTY_OBJECT_ARRAY); + } + + @Override + public final Class<K> getKeyType() { + return keyType; + } + + @Override + public Collection<String> getSupportedTypeNames() { + return names; + } + + @Override + public K decodePublicKey(byte... keyData) throws IOException, GeneralSecurityException { + return decodePublicKey(keyData, 0, GenericUtils.length(keyData)); + } + + @Override + public K 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); + } + } + + @Override + public K decodePublicKey(InputStream keyData) throws IOException, GeneralSecurityException { + // the actual data is preceded by a string that repeats the key type + String type = 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); + } + + public K generatePublicKey(KeySpec keySpec) throws GeneralSecurityException { + KeyFactory factory = getKeyFactoryInstance(); + Class<K> keyType = getKeyType(); + return keyType.cast(factory.generatePublic(keySpec)); + } + + public abstract KeyFactory getKeyFactoryInstance() throws GeneralSecurityException; + + /** + * @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 + */ + public abstract K decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException; + + @Override + public String toString() { + return getKeyType().getSimpleName() + ": " + getSupportedTypeNames(); + } + + public static final String decodeString(InputStream s) throws IOException { + return decodeString(s, StandardCharsets.UTF_8); + } + + public static final String decodeString(InputStream s, String charset) throws IOException { + return decodeString(s, Charset.forName(charset)); + } + + public static final String decodeString(InputStream s, Charset cs) throws IOException { + byte[] bytes=readRLEBytes(s); + return new String(bytes, cs); + } + + public static final BigInteger decodeBigInt(InputStream s) throws IOException { + return new BigInteger(readRLEBytes(s)); + } + + public static final byte[] readRLEBytes(InputStream s) throws IOException { + int len=decodeInt(s); + byte[] bytes=new byte[len]; + IoUtils.readFully(s, bytes); + return bytes; + } + + public static final int decodeInt(InputStream s) throws IOException { + byte[] bytes={ 0, 0, 0, 0 }; + IoUtils.readFully(s, bytes); + return ((bytes[0] & 0xFF) << 24) + | ((bytes[1] & 0xFF) << 16) + | ((bytes[2] & 0xFF) << 8) + | (bytes[3] & 0xFF); + } + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java new file mode 100644 index 0000000..62acb4c --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.config.keys; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; + +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DSSPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<DSAPublicKey> { + public static final DSSPublicKeyEntryDecoder INSTANCE = new DSSPublicKeyEntryDecoder(); + + public DSSPublicKeyEntryDecoder() { + super(DSAPublicKey.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("Unepected key type: " + keyType); + } + + BigInteger p=decodeBigInt(keyData); + BigInteger q=decodeBigInt(keyData); + BigInteger g=decodeBigInt(keyData); + BigInteger y=decodeBigInt(keyData); + + return generatePublicKey(new DSAPublicKeySpec(y, p, q, g)); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory("DSA"); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java new file mode 100644 index 0000000..29be249 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.config.keys; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.NoSuchProviderException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.SecurityUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<ECPublicKey> { + public static final ECDSAPublicKeyEntryDecoder INSTANCE = new ECDSAPublicKeyEntryDecoder(); + + public ECDSAPublicKeyEntryDecoder() { + super(ECPublicKey.class, + Collections.unmodifiableList( + Arrays.asList(KeyPairProvider.ECDSA_SHA2_NISTP256, KeyPairProvider.ECDSA_SHA2_NISTP384, KeyPairProvider.ECDSA_SHA2_NISTP521))); + } + + @Override + public ECPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException { + if (GenericUtils.isEmpty(keyType) || (!keyType.startsWith(ECCurves.ECDSA_SHA2_PREFIX))) { + throw new InvalidKeySpecException("Not an EC curve name: " + keyType); + } + + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + String keyCurveName = keyType.substring(ECCurves.ECDSA_SHA2_PREFIX.length()); + ECParameterSpec paramSpec = ECCurves.getECParameterSpec(keyCurveName); + if (paramSpec == null) { + throw new InvalidKeySpecException("Unknown EC key curve name: " + keyCurveName); + } + + // see rfc5656 section 3.1 + String encCurveName = decodeString(keyData); + if (!keyCurveName.equals(encCurveName)) { + throw new InvalidKeySpecException("Mismatched key curve name (" + keyCurveName + ") vs. encoded one (" + encCurveName + ")"); + } + + byte[] octets = readRLEBytes(keyData); + final ECPoint w; + try { + if ((w = octetStringToEcPoint(octets)) == null) { + throw new InvalidKeySpecException("No ECPoint generated for curve=" + keyCurveName + " from octets=" + BufferUtils.printHex(':', octets)); + } + } catch(RuntimeException e) { + throw new InvalidKeySpecException("Failed (" + e.getClass().getSimpleName() + ")" + + " to generate ECPoint for curve=" + keyCurveName + + " from octets=" + BufferUtils.printHex(':', octets) + + ": " + e.getMessage()); + } + + return generatePublicKey(new ECPublicKeySpec(w, paramSpec)); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + if (SecurityUtils.hasEcc()) { + return SecurityUtils.getKeyFactory("EC"); + } else { + throw new NoSuchProviderException("ECC not supported"); + } + } + + // see rfc5480 section 2.2 + public static final byte ECPOINT_UNCOMPRESSED_FORM_INDICATOR=0x04; + public static final byte ECPOINT_COMPRESSED_VARIANT_2=0x02; + public static final byte ECPOINT_COMPRESSED_VARIANT_3=0x02; + + public static ECPoint octetStringToEcPoint(byte ... octets) { + if (GenericUtils.isEmpty(octets)) { + return null; + } + + int startIndex=findFirstNonZeroIndex(octets); + if (startIndex < 0) { + throw new IllegalArgumentException("All zeroes ECPoint N/A"); + } + + byte indicator=octets[startIndex]; + ECPointCompression compression=ECPointCompression.fromIndicatorValue(indicator); + if (compression == null) { + throw new UnsupportedOperationException("Unknown compression indicator value: 0x" + Integer.toHexString(indicator & 0xFF)); + } + + // The coordinates actually start after the compression indicator + return compression.octetStringToEcPoint(octets, startIndex + 1, octets.length - startIndex - 1); + } + + private static int findFirstNonZeroIndex(byte ... octets) { + if (GenericUtils.isEmpty(octets)) { + return (-1); + } + + for (int index=0; index < octets.length; index++) { + if (octets[index] != 0) { + return index; + } + } + + return (-1); // all zeroes + } + /** + * The various {@link ECPoint} representation compression indicators + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="https://www.ietf.org/rfc/rfc5480.txt">RFC-5480 - section 2.2</A> + */ + public enum ECPointCompression { + // see http://tools.ietf.org/html/draft-jivsov-ecc-compact-00 + // see http://crypto.stackexchange.com/questions/8914/ecdsa-compressed-public-key-point-back-to-uncompressed-public-key-point + VARIANT2((byte) 0x02) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + byte[] xp=new byte[len]; + System.arraycopy(octets, startIndex, xp, 0, len); + BigInteger x=octetStringToInteger(xp); + + // TODO derive even Y... + throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); + } + }, + VARIANT3((byte) 0x03) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + byte[] xp=new byte[len]; + System.arraycopy(octets, startIndex, xp, 0, len); + BigInteger x=octetStringToInteger(xp); + + // TODO derive odd Y... + throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); + } + }, + UNCOMPRESSED((byte) 0x04) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + int numElements=len / 2; /* x, y */ + if (len != (numElements * 2 )) { // make sure length is not odd + throw new IllegalArgumentException("octetStringToEcPoint(" + name() + ") " + + " invalid remainder octets representation: " + + " expected=" + (2 * numElements) + ", actual=" + len); + } + + byte[] xp=new byte[numElements], yp=new byte[numElements]; + System.arraycopy(octets, startIndex, xp, 0, numElements); + System.arraycopy(octets, startIndex + numElements, yp, 0, numElements); + + BigInteger x=octetStringToInteger(xp); + BigInteger y=octetStringToInteger(yp); + return new ECPoint(x, y); + } + }; + + private final byte indicatorValue; + public final byte getIndicatorValue() { + return indicatorValue; + } + + ECPointCompression(byte indicator) { + indicatorValue = indicator; + } + + public abstract ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len); + + public static final Set<ECPointCompression> VALUES= + Collections.unmodifiableSet(EnumSet.allOf(ECPointCompression.class)); + public static final ECPointCompression fromIndicatorValue(int value) { + if ((value < 0) || (value > 0xFF)) { + return null; // must be a byte value + } + + for (ECPointCompression c : VALUES) { + if (value == c.getIndicatorValue()) { + return c; + } + } + + return null; + } + + /** + * Converts the given octet string (defined by ASN.1 specifications) to a {@link BigInteger} + * As octet strings always represent positive integers, a zero-byte is prepended to + * the given array if necessary (if is MSB equal to 1), then this is converted to BigInteger + * The conversion is defined in the Section 2.3.8 + * @param octets - octet string bytes to be converted + * @return The {@link BigInteger} representation of the octet string + */ + public static BigInteger octetStringToInteger(byte ... octets) { + if (octets == null) { + return null; + } else if (octets.length == 0) { + return BigInteger.ZERO; + } else { + return new BigInteger(1, octets); + } + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java new file mode 100644 index 0000000..4d9b2af --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.common.config.keys; + +import java.security.Key; +import java.security.KeyPair; +import java.security.PublicKey; +import java.security.interfaces.DSAKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECParameterSpec; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import org.apache.sshd.common.Digest; +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + +/** + * Utility class for keys + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class KeyUtils { + private static final Map<String,PublicKeyEntryDecoder<? extends PublicKey>> byKeyTypeDecodersMap = + new TreeMap<String, PublicKeyEntryDecoder<? extends PublicKey>>(String.CASE_INSENSITIVE_ORDER); + private static final Map<Class<?>,PublicKeyEntryDecoder<? extends PublicKey>> byKeyClassDecodersMap = + new HashMap<Class<?>, PublicKeyEntryDecoder<? extends PublicKey>>(); + + static { + registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE); + registerPublicKeyEntryDecoder(DSSPublicKeyEntryDecoder.INSTANCE); + registerPublicKeyEntryDecoder(ECDSAPublicKeyEntryDecoder.INSTANCE); + } + + private KeyUtils() { + throw new UnsupportedOperationException("No instance"); + } + + public static void registerPublicKeyEntryDecoder(PublicKeyEntryDecoder<? extends PublicKey> decoder) { + ValidateUtils.checkNotNull(decoder, "No decoder specified", GenericUtils.EMPTY_OBJECT_ARRAY); + + Class<?> keyType = ValidateUtils.checkNotNull(decoder.getKeyType(), "No key type declared", GenericUtils.EMPTY_OBJECT_ARRAY); + synchronized(byKeyClassDecodersMap) { + byKeyClassDecodersMap.put(keyType, decoder); + } + + Collection<String> names = ValidateUtils.checkNotNullAndNotEmpty(decoder.getSupportedTypeNames(), "No supported key type", GenericUtils.EMPTY_OBJECT_ARRAY); + synchronized(byKeyTypeDecodersMap) { + for (String n : names) { + PublicKeyEntryDecoder<? extends PublicKey> prev = byKeyTypeDecodersMap.put(n, decoder); + if (prev != null) { + continue; // debug breakpoint + } + } + } + } + + /** + * @param keyType The {@code OpenSSH} key type string - ignored if {@code null}/empty + * @return The registered {@link PublicKeyEntryDecoder} or {code null} if not found + */ + public static PublicKeyEntryDecoder<? extends PublicKey> getPublicKeyEntryDecoder(String keyType) { + if (GenericUtils.isEmpty(keyType)) { + return null; + } + + synchronized(byKeyTypeDecodersMap) { + return byKeyTypeDecodersMap.get(keyType); + } + } + + /** + * @param key The {@link PublicKey} - 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<? extends PublicKey> getPublicKeyEntryDecoder(PublicKey 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 PublicKey} + * compatible type + * @return The registered {@link PublicKeyEntryDecoder} or {code null} if no match found + */ + public static PublicKeyEntryDecoder<? extends PublicKey> getPublicKeyEntryDecoder(Class<?> keyType) { + if ((keyType == null) || (!PublicKey.class.isAssignableFrom(keyType))) { + return null; + } + + synchronized(byKeyTypeDecodersMap) { + { + PublicKeyEntryDecoder<? extends PublicKey> decoder=byKeyTypeDecodersMap.get(keyType); + if (decoder != null) { + return decoder; + } + } + + // in case it is a derived class + for (PublicKeyEntryDecoder<? extends PublicKey> decoder : byKeyTypeDecodersMap.values()) { + Class<?> t = decoder.getKeyType(); + if (t.isAssignableFrom(keyType)) { + return decoder; + } + } + } + + return null; + } + + /** + * Retrieve the public key fingerprint + * + * @param key the public key - ignored if {@code null} + * @return the fingerprint or {@code null} if no key + */ + public static String getFingerPrint(PublicKey key) { + if (key == null) { + return null; + } + + try { + Buffer buffer = new ByteArrayBuffer(); + buffer.putRawPublicKey(key); + Digest md5 = BuiltinDigests.md5.create(); + md5.init(); + md5.update(buffer.array(), 0, buffer.wpos()); + byte[] data = md5.digest(); + return BufferUtils.printHex(data, 0, data.length, ':'); + } catch(Exception e) { + return "Unable to compute fingerprint"; + } + } + + /** + * Retrieve the key type + * + * @param kp a key pair + * @return the key type + */ + public static String getKeyType(KeyPair kp) { + return getKeyType(kp.getPrivate() != null ? kp.getPrivate() : kp.getPublic()); + } + + /** + * Retrieve the key type + * + * @param key a public or private key + * @return the key type + */ + public static String getKeyType(Key key) { + 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(); + return ECCurves.ECDSA_SHA2_PREFIX + ECCurves.getCurveName(ecSpec); + } + return null; + } + + /** + * @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 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 { + 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 if (Objects.equals(k1.getPublicExponent(), k2.getPublicExponent()) + && Objects.equals(k1.getModulus(), k2.getModulus())) { + return true; + } else { + return false; + } + } + + 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 if (Objects.equals(k1.getY(), k2.getY()) + && compareDSAParams(k1.getParams(), k2.getParams())) { + return true; + } else { + return false; + } + } + + 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 if (Objects.equals(p1.getG(), p2.getG()) + && Objects.equals(p1.getP(), p2.getP()) + && Objects.equals(p1.getQ(), p2.getQ())) { + return true; + } else { + return false; + } + } + + 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 if (Objects.equals(k1.getW(), k2.getW()) + && compareECParams(k1.getParams(), k2.getParams())) { + return true; + } else { + return false; + } + } + + 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 if (Objects.equals(s1.getOrder(), s2.getOrder()) + && (s1.getCofactor() == s2.getCofactor()) + && Objects.equals(s1.getGenerator(), s2.getGenerator()) + && Objects.equals(s1.getCurve(), s2.getCurve())) { + return true; + } else { + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java new file mode 100644 index 0000000..a3dbf8b --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java @@ -0,0 +1,218 @@ +/* + * 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.File; +import java.io.IOException; +import java.io.Serializable; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.Objects; + +import org.apache.sshd.common.util.Base64; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * <P>Represents a {@link PublicKey} whose data is formatted according to + * the <A HREF="http://en.wikibooks.org/wiki/OpenSSH">OpenSSH</A> format:</P></BR> + * <CODE><PRE> + * <key-type> <base64-encoded-public-key-data> + * </CODE></PRE> + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class PublicKeyEntry implements Serializable { + 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; + } + + /** + * @return The resolved {@link PublicKey} - never {@code null}. + * <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() throws IOException, GeneralSecurityException { + String kt = getKeyType(); + PublicKeyEntryDecoder<? extends PublicKey> decoder = KeyUtils.getPublicKeyEntryDecoder(kt); + if (decoder == null) { + throw new InvalidKeySpecException("No decoder registered for key type=" + kt); + } + + byte[] data = getKeyData(); + PublicKey key = decoder.decodePublicKey(data); + if (key == null) { + throw new InvalidKeyException("No key of type=" + kt + " decoded for data=" + BufferUtils.printHex(':', data)); + } + + 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; + } + + if (Objects.equals(getKeyType(), e.getKeyType()) + && Arrays.equals(getKeyData(), e.getKeyData())) { + return true; + } else { + return false; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (this == obj) + return true; + if (getClass() != obj.getClass()) + return false; + + if (isEquivalent((PublicKeyEntry) obj)) { + return true; + } else { + return false; + } + } + + @Override + public String toString() { + byte[] data = getKeyData(); + return getKeyType() + " " + (GenericUtils.isEmpty(data) ? "<no-key>" : Base64.encodeToString(data)); + } + + /** + * @param data 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 final PublicKeyEntry parsePublicKeyEntry(String data) throws IllegalArgumentException { + if (GenericUtils.isEmpty(data)) { + return null; + } else { + return parsePublicKeyEntry(new PublicKeyEntry(), data); + } + } + + /** + * @param entry The {@link PublicKeyEntry} whose contents are to be + * updated - ignored if {@code null} + * @param data 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 final <E extends PublicKeyEntry> E parsePublicKeyEntry(E entry, String data) throws IllegalArgumentException { + 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(); + byte[] keyData = Base64.decodeString(b64Data); + if (GenericUtils.isEmpty(keyData)) { + throw new IllegalArgumentException("Bad format (no BASE64 key data): " + data); + } + + entry.setKeyType(keyType); + entry.setKeyData(keyData); + return entry; + } + + /** + * 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 class LazyDefaultKeysFolderHolder { + private static final File folder= + new File(System.getProperty("user.home") + File.separator + STD_KEYFILE_FOLDER_NAME); + } + + /** + * @return The default OpenSSH folder used to hold key files - e.g., + * {@code known_hosts}, {@code authorized_keys}, etc. + */ + @SuppressWarnings("synthetic-access") + public static final File getDefaultKeysFolder() { + return LazyDefaultKeysFolderHolder.folder; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java new file mode 100644 index 0000000..1ac3ab3 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.config.keys; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.Collection; + +/** + * Represents a decoder of an {@code OpenSSH} encoded key data + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface PublicKeyEntryDecoder<K extends PublicKey> { + /** + * @return The {@link Class} of the {@link PublicKey} that is the result + * of decoding + */ + Class<K> getKeyType(); + + /** + * @return The {@link Collection} of {@code OpenSSH} key type names that + * are supported by this decoder - e.g., ECDSA keys have several curve names. + * <B>Caveat:</B> this collection may be un-modifiable... + */ + Collection<String> getSupportedTypeNames(); + + /** + * @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 + */ + K decodePublicKey(byte ... keyData) throws IOException, GeneralSecurityException; + K decodePublicKey(byte[] keyData, int offset, int length) throws IOException, GeneralSecurityException; + K decodePublicKey(InputStream keyData) throws IOException, GeneralSecurityException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java new file mode 100644 index 0000000..08d5bc7 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.config.keys; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Collections; + +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class RSAPublicKeyDecoder extends AbstractPublicKeyEntryDecoder<RSAPublicKey> { + public static final RSAPublicKeyDecoder INSTANCE = new RSAPublicKeyDecoder(); + + public RSAPublicKeyDecoder() { + super(RSAPublicKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_RSA))); + } + + @Override + public RSAPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_RSA.equals(keyType)) { // just in case we were invoked directly + throw new InvalidKeySpecException("Unepected key type: " + keyType); + } + + BigInteger e=decodeBigInt(keyData); + BigInteger n=decodeBigInt(keyData); + + return generatePublicKey(new RSAPublicKeySpec(n, e)); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory("RSA"); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java index 9503486..0d4b5ef 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java @@ -110,7 +110,7 @@ public enum BuiltinDHFactories implements DHFactory { if (!GenericUtils.isEmpty(params)) { throw new IllegalArgumentException("No accepted parameters for " + getName()); } - return new ECDH(ECCurves.EllipticCurves.nistp256); + return new ECDH(ECCurves.NISTP256); } @Override @@ -124,7 +124,7 @@ public enum BuiltinDHFactories implements DHFactory { if (!GenericUtils.isEmpty(params)) { throw new IllegalArgumentException("No accepted parameters for " + getName()); } - return new ECDH(ECCurves.EllipticCurves.nistp384); + return new ECDH(ECCurves.NISTP384); } @Override @@ -138,7 +138,7 @@ public enum BuiltinDHFactories implements DHFactory { if (!GenericUtils.isEmpty(params)) { throw new IllegalArgumentException("No accepted parameters for " + getName()); } - return new ECDH(ECCurves.EllipticCurves.nistp521); + return new ECDH(ECCurves.NISTP521); } @Override http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java index 08dc695..741f99f 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java @@ -51,7 +51,11 @@ public class ECDH extends AbstractDH { private KeyAgreement myKeyAgree; public ECDH() throws Exception { - this(null); + this((ECParameterSpec) null); + } + + public ECDH(String curveName) throws Exception { + this(ValidateUtils.checkNotNull(ECCurves.getECParameterSpec(curveName), "Unknown curve name: %s", curveName)); } public ECDH(ECParameterSpec paramSpec) throws Exception { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java b/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java index 4a5dd6e..03ba5f2 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java @@ -22,9 +22,9 @@ import java.security.KeyPair; import java.util.ArrayList; import java.util.List; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.AbstractLoggingBean; import org.apache.sshd.common.util.GenericUtils; -import org.apache.sshd.common.util.KeyUtils; import org.apache.sshd.common.util.ValidateUtils; /** http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java b/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java index 445fc11..37af11c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java @@ -21,6 +21,8 @@ package org.apache.sshd.common.keyprovider; import java.security.KeyPair; import java.util.Collections; +import org.apache.sshd.common.cipher.ECCurves; + /** * Provider for key pairs. This provider is used on the server side to provide * the host key, or on the client side to provide the user key. @@ -42,17 +44,17 @@ public interface KeyPairProvider { /** * SSH identifier for EC keys in NIST curve P-256 */ - String ECDSA_SHA2_NISTP256 = "ecdsa-sha2-nistp256"; + String ECDSA_SHA2_NISTP256 = ECCurves.ECDSA_SHA2_PREFIX + ECCurves.NISTP256; /** * SSH identifier for EC keys in NIST curve P-384 */ - String ECDSA_SHA2_NISTP384 = "ecdsa-sha2-nistp384"; + String ECDSA_SHA2_NISTP384 = ECCurves.ECDSA_SHA2_PREFIX + ECCurves.NISTP384; /** * SSH identifier for EC keys in NIST curve P-521 */ - String ECDSA_SHA2_NISTP521 = "ecdsa-sha2-nistp521"; + String ECDSA_SHA2_NISTP521 = ECCurves.ECDSA_SHA2_PREFIX + ECCurves.NISTP521; /** * Load available keys. http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java b/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java index ffc476c..a7cc8ca 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java @@ -19,6 +19,7 @@ */ package org.apache.sshd.common.util; +import java.nio.charset.StandardCharsets; import java.security.InvalidParameterException; /** @@ -33,6 +34,7 @@ import java.security.InvalidParameterException; * This class was * @author Apache Software Foundation commons codec (http://commons.apache.org/codec/) * @author <a href="http://mina.apache.org">Apache MINA Project</a> + * TODO replace this class with {@code java.util.Base64} when upgrading to JDK 1.8 */ public class Base64 { @@ -168,6 +170,10 @@ public class Base64 { return true; } + public static String encodeToString(byte ... bytes) { + return new String(encodeBase64(bytes), StandardCharsets.UTF_8); + } + /** * Encodes binary data using the base64 algorithm but * does not chunk the output. @@ -334,6 +340,14 @@ public class Base64 { return encodedData; } + public static byte[] decodeString(String s) { + if (GenericUtils.isEmpty(s)) { + return GenericUtils.EMPTY_BYTE_ARRAY; + } else { + return decodeBase64(s.getBytes(StandardCharsets.UTF_8)); + } + } + /** * Decodes Base64 data into octects * @@ -345,8 +359,8 @@ public class Base64 { base64Data = discardNonBase64(base64Data); // handle the edge case, so we don't have to worry about it later - if (base64Data.length == 0) { - return new byte[0]; + if (GenericUtils.isEmpty(base64Data)) { + return GenericUtils.EMPTY_BYTE_ARRAY; } int numberQuadruple = base64Data.length / FOURBYTE; @@ -363,7 +377,7 @@ public class Base64 { // ignore the '=' padding while (base64Data[lastData - 1] == PAD) { if (--lastData == 0) { - return new byte[0]; + return GenericUtils.EMPTY_BYTE_ARRAY; } } decodedData = new byte[lastData - numberQuadruple]; @@ -437,22 +451,52 @@ public class Base64 { * encoded data." * * @param data The base-64 encoded data to groom - * @return The data, less non-base64 characters (see RFC 2045). + * @return The data, less non-base64 characters (see RFC 2045) - + * may be same as input if all data was base-64 */ - static byte[] discardNonBase64(byte[] data) { - byte groomedData[] = new byte[data.length]; + public static byte[] discardNonBase64(byte[] data) { + if (GenericUtils.isEmpty(data)) { + return data; + } + + byte groomedData[] = null; int bytesCopied = 0; for (int i = 0; i < data.length; i++) { - if (isBase64(data[i])) { - groomedData[bytesCopied++] = data[i]; + byte b = data[i]; + + if (isBase64(b)) { + if (groomedData != null) { + // we had to filter out some non-BASE64 bytes + groomedData[bytesCopied++] = b; + } + } else { + // this means ALL the characters up to this index were BASE64 + if (groomedData == null) { + groomedData = new byte[data.length - 1 /* the current character, which is NOT BASE64 */]; + + if ((bytesCopied=i) > 0) { + System.arraycopy(data, 0, groomedData, 0, bytesCopied); + } + } } } - byte packedData[] = new byte[bytesCopied]; + if (groomedData == null) { + return data; // all characters where BASE64 + } - System.arraycopy(groomedData, 0, packedData, 0, bytesCopied); + if (bytesCopied <= 0) { + return GenericUtils.EMPTY_BYTE_ARRAY; + } + + // if we were lucky and only ONE character was groomed + if (bytesCopied == groomedData.length) { + return groomedData; + } + byte packedData[] = new byte[bytesCopied]; + System.arraycopy(groomedData, 0, packedData, 0, bytesCopied); return packedData; } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java index c75235b..657a6ad 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -289,4 +289,48 @@ public class GenericUtils { return set; } + + public static final String QUOTES="\"'"; + + /** + * @param s The {@link CharSequence} to be checked + * @return If the sequence contains any of the {@link #QUOTES} + * on <U>both</U> ends, then they are stripped, otherwise + * nothing is done + * @see #stripDelimiters(CharSeqeuence, char) + */ + public static final CharSequence stripQuotes(CharSequence s) { + if (isEmpty(s)) { + return s; + } + + for (int index=0; index < QUOTES.length(); index++) { + char delim = QUOTES.charAt(index); + CharSequence v = stripDelimiters(s, delim); + if (v != s) { // if stripped one don't continue + return v; + } + } + + return s; + } + + /** + * @param s The {@link CharSequence} to be checked + * @param delim The expected delimiter + * @return If the sequence contains the delimiter on <U>both</U> ends, + * then it is are stripped, otherwise nothing is done + */ + public static final CharSequence stripDelimiters(CharSequence s, char delim) { + if (isEmpty(s) || (s.length() < 2)) { + return s; + } + + int lastPos = s.length() - 1; + if ((s.charAt(0) != delim) || (s.charAt(lastPos) != delim)) { + return s; + } else { + return s.subSequence(1, lastPos); + } + } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/util/IoUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/IoUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/IoUtils.java index 7483542..da17c22 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/IoUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/IoUtils.java @@ -19,6 +19,7 @@ package org.apache.sshd.common.util; import java.io.Closeable; +import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -226,4 +227,65 @@ public class IoUtils { return null; } } + + /** + * Read the requested number of bytes or fail if there are not enough left. + * @param input where to read input from + * @param buffer destination + * @throws IOException if there is a problem reading the file + * @throws EOFException if the number of bytes read was incorrect + */ + public static void readFully(InputStream input, byte[] buffer) throws IOException { + readFully(input, buffer, 0, buffer.length); + } + + /** + * Read the requested number of bytes or fail if there are not enough left. + * @param input where to read input from + * @param buffer destination + * @param offset initial offset into buffer + * @param length length to read, must be >= 0 + * @throws IOException if there is a problem reading the file + * @throws EOFException if the number of bytes read was incorrect + */ + public static void readFully(InputStream input, byte[] buffer, int offset, int length) throws IOException { + int actual = read(input, buffer, offset, length); + if (actual != length) { + throw new EOFException("Premature EOF - expected=" + length + ", actual=" + actual); + } + } + + /** + * Read as many bytes as possible until EOF or achieved required length + * @param input where to read input from + * @param buffer destination + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + */ + public static int read(InputStream input, byte[] buffer) throws IOException { + return read(input, buffer, 0, buffer.length); + } + + /** + * Read as many bytes as possible until EOF or achieved required length + * @param input where to read input from + * @param buffer destination + * @param offset initial offset into buffer + * @param length length to read - ignored if non-positive + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + */ + public static int read(InputStream input, byte[] buffer, int offset, int length) throws IOException { + for (int remaining = length, curOffset = offset; remaining > 0; ) { + int count = input.read(buffer, curOffset, remaining); + if (count == (-1)) { // EOF before achieved required length + return curOffset - offset; + } + + remaining -= count; + curOffset += count; + } + + return length; + } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/util/KeyUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/KeyUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/KeyUtils.java deleted file mode 100644 index 6bea4d3..0000000 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/KeyUtils.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.util; - -import java.security.Key; -import java.security.KeyPair; -import java.security.PublicKey; -import java.security.interfaces.DSAKey; -import java.security.interfaces.ECKey; -import java.security.interfaces.RSAKey; -import java.security.spec.ECParameterSpec; - -import org.apache.sshd.common.Digest; -import org.apache.sshd.common.cipher.ECCurves; -import org.apache.sshd.common.digest.BuiltinDigests; -import org.apache.sshd.common.keyprovider.KeyPairProvider; -import org.apache.sshd.common.util.buffer.Buffer; -import org.apache.sshd.common.util.buffer.BufferUtils; -import org.apache.sshd.common.util.buffer.ByteArrayBuffer; - -/** - * Utility class for keys - * - * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> - */ -public class KeyUtils { - - /** - * Retrieve the public key fingerprint - * - * @param key the public key - ignored if {@code null} - * @return the fingerprint or {@code null} if no key - */ - public static String getFingerPrint(PublicKey key) { - if (key == null) { - return null; - } - - try { - Buffer buffer = new ByteArrayBuffer(); - buffer.putRawPublicKey(key); - Digest md5 = BuiltinDigests.md5.create(); - md5.init(); - md5.update(buffer.array(), 0, buffer.wpos()); - byte[] data = md5.digest(); - return BufferUtils.printHex(data, 0, data.length, ':'); - } catch(Exception e) { - return "Unable to compute fingerprint"; - } - } - - /** - * Retrieve the key type - * - * @param kp a key pair - * @return the key type - */ - public static String getKeyType(KeyPair kp) { - return getKeyType(kp.getPrivate() != null ? kp.getPrivate() : kp.getPublic()); - } - - /** - * Retrieve the key type - * - * @param key a public or private key - * @return the key type - */ - public static String getKeyType(Key key) { - 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(); - return ECCurves.ECDSA_SHA2_PREFIX + ECCurves.getCurveName(ecSpec); - } - return null; - } - - private KeyUtils() { - } - -} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java new file mode 100644 index 0000000..83c1656 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java @@ -0,0 +1,169 @@ +/* + * 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.util.io; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.sshd.common.util.AbstractLoggingBean; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.IoUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * Watches over changes for a file and re-loads them if file has changed - including + * if file is deleted or (re-)created + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class ModifiableFileWatcher extends AbstractLoggingBean { + private final Path file; + private final AtomicBoolean lastExisted = new AtomicBoolean(false); + private final AtomicLong lastSize = new AtomicLong(Long.MIN_VALUE); + private final AtomicLong lastModified = new AtomicLong(-1L); + protected final LinkOption[] options; + + public ModifiableFileWatcher(File file) { + this(ValidateUtils.checkNotNull(file, "No file to watch", GenericUtils.EMPTY_OBJECT_ARRAY).toPath()); + } + + public ModifiableFileWatcher(Path file) { + this(file, IoUtils.getLinkOptions(false)); + } + + public ModifiableFileWatcher(Path file, LinkOption ... options) { + this.file = ValidateUtils.checkNotNull(file, "No path to watch", GenericUtils.EMPTY_OBJECT_ARRAY); + // use a clone to avoid being sensitive to changes in the passed array + this.options = (options == null) ? IoUtils.EMPTY_OPTIONS : options.clone(); + } + + /** + * @return The watched {@link Path} + */ + public final Path getPath() { + return file; + } + + public final boolean exists() throws IOException { + return Files.exists(getPath(), options); + } + + public final long size() throws IOException { + if (exists()) { + return Files.size(getPath()); + } else { + return (-1L); + } + } + + public final FileTime lastModified() throws IOException { + if (exists()) { + BasicFileAttributes attrs = Files.readAttributes(getPath(), BasicFileAttributes.class, options); + return attrs.lastModifiedTime(); + } else { + return null; + } + } + + /** + * @return {@code true} if the watched file has probably been changed + * @throws IOException If failed to query file data + */ + public boolean checkReloadRequired() throws IOException { + boolean exists = exists(); + // if existence state changed from last time + if (exists != lastExisted.getAndSet(exists)) { + return true; + } + + if (!exists) { + // file did not exist and still does not exist + resetReloadAttributes(); + return false; + } + + long size = size(); + if (size < 0L) { + // means file no longer exists + resetReloadAttributes(); + return true; + } + + // if size changed then obviously need reload + if (size != lastSize.getAndSet(size)) { + return true; + } + + FileTime modifiedTime = lastModified(); + if (modifiedTime == null) { + // means file no longer exists + resetReloadAttributes(); + return true; + } + + long timestamp = modifiedTime.toMillis(); + if (timestamp != lastModified.getAndSet(timestamp)) { + return true; + } + + return false; + } + + /** + * Resets the state attributes used to detect changes to the initial + * construction values - i.e., file assumed not to exist and no known + * size of modify time + */ + public void resetReloadAttributes() { + lastExisted.set(false); + lastSize.set(Long.MIN_VALUE); + lastModified.set(-1L); + } + + /** + * May be called to refresh the state attributes used to detect changes + * e.g., file existence, size and last-modified time once re-loading is + * successfully completed. If the file does not exist then the attributes + * are reset to an "unknown" state. + * @throws IOException If failed to access the file (if exists) + * @see #resetReloadAttributes() + */ + public void updateReloadAttributes() throws IOException { + if (exists()) { + long size = size(); + FileTime modifiedTime = lastModified(); + + if ((size >= 0L) && (modifiedTime != null)) { + lastExisted.set(true); + lastSize.set(size); + lastModified.set(modifiedTime.toMillis()); + return; + } + } + + resetReloadAttributes(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/server/PublickeyAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/PublickeyAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/PublickeyAuthenticator.java index 78c8adf..d6224fb 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/PublickeyAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/PublickeyAuthenticator.java @@ -19,9 +19,12 @@ package org.apache.sshd.server; import java.security.PublicKey; +import java.util.Collection; +import java.util.Collections; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.AbstractLoggingBean; -import org.apache.sshd.common.util.KeyUtils; +import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.server.session.ServerSession; /** @@ -42,6 +45,42 @@ public interface PublickeyAuthenticator { boolean authenticate(String username, PublicKey key, ServerSession session); /** + * Checks against a {@link Collection} of {@link PublicKey}s + */ + public static class KeySetPublickeyAuthenticator extends AbstractLoggingBean implements PublickeyAuthenticator { + private final Collection<? extends PublicKey> keySet; + + public KeySetPublickeyAuthenticator(Collection<? extends PublicKey> keySet) { + this.keySet = (keySet == null) ? Collections.<PublicKey>emptyList() : keySet; + } + + public final Collection<? extends PublicKey> getKeySet() { + return keySet; + } + + @Override + public boolean authenticate(String username, PublicKey key, ServerSession session) { + return authenticate(username, key, session, getKeySet()); + } + + public boolean authenticate(String username, PublicKey key, ServerSession session, Collection<? extends PublicKey> keys) { + if (GenericUtils.isEmpty(keys)) { + if (log.isDebugEnabled()) { + log.debug("authenticate(" + username + ")[" + session + "] no keys"); + } + + return false; + } + + PublicKey matchKey = KeyUtils.findMatchingKey(key, keys); + boolean matchFound = (matchKey != null); + if (log.isDebugEnabled()) { + log.debug("authenticate(" + username + ")[" + session + "] match found=" + matchFound); + } + return matchFound; + } + } + /** * Returns the same constant result {@code true/false} regardless */ public static abstract class StaticPublickeyAuthenticator extends AbstractLoggingBean implements PublickeyAuthenticator { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/server/auth/CachingPublicKeyAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/CachingPublicKeyAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/CachingPublicKeyAuthenticator.java index d836f4e..0e0880b 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/CachingPublicKeyAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/CachingPublicKeyAuthenticator.java @@ -28,8 +28,7 @@ import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; /** - * TODO Add javadoc - * + * Caches the result per session * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, SessionListener {
