http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java new file mode 100644 index 0000000..793965c --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java @@ -0,0 +1,97 @@ +/* + * 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.security.eddsa; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.impl.AbstractPublicKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.security.SecurityUtils; + +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class Ed25519PublicKeyDecoder extends AbstractPublicKeyEntryDecoder<EdDSAPublicKey, EdDSAPrivateKey> { + public static final Ed25519PublicKeyDecoder INSTANCE = new Ed25519PublicKeyDecoder(); + + private Ed25519PublicKeyDecoder() { + super(EdDSAPublicKey.class, EdDSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_ED25519))); + } + + @Override + public EdDSAPublicKey clonePublicKey(EdDSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePublicKey(new EdDSAPublicKeySpec(key.getA(), key.getParams())); + } + } + + @Override + public EdDSAPrivateKey clonePrivateKey(EdDSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePrivateKey(new EdDSAPrivateKeySpec(key.getSeed(), key.getParams())); + } + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(SecurityUtils.EDDSA); + } + + @Override + public String encodePublicKey(OutputStream s, EdDSAPublicKey key) throws IOException { + Objects.requireNonNull(key, "No public key provided"); + KeyEntryResolver.encodeString(s, KeyPairProvider.SSH_ED25519); + byte[] seed = getSeedValue(key); + KeyEntryResolver.writeRLEBytes(s, seed); + return KeyPairProvider.SSH_ED25519; + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(SecurityUtils.EDDSA); + } + + @Override + public EdDSAPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException { + byte[] seed = KeyEntryResolver.readRLEBytes(keyData); + return EdDSAPublicKey.class.cast(SecurityUtils.generateEDDSAPublicKey(keyType, seed)); + } + + public static byte[] getSeedValue(EdDSAPublicKey key) { + // a bit of reverse-engineering on the EdDSAPublicKeySpec + return (key == null) ? null : key.getAbyte(); + } +} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java new file mode 100644 index 0000000..61f16e9 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java @@ -0,0 +1,104 @@ +/* + * 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.security.eddsa; + +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.Provider; +import java.security.Signature; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ReflectionUtils; +import org.apache.sshd.common.util.security.AbstractSecurityProviderRegistrar; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.common.util.threads.ThreadUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class EdDSASecurityProviderRegistrar extends AbstractSecurityProviderRegistrar { + public static final String PROVIDER_CLASS = "net.i2p.crypto.eddsa.EdDSASecurityProvider"; + // Do not define a static registrar instance to minimize class loading issues + private final AtomicReference<Boolean> supportHolder = new AtomicReference<>(null); + + public EdDSASecurityProviderRegistrar() { + super(SecurityUtils.EDDSA); + } + + @Override + public boolean isEnabled() { + if (!super.isEnabled()) { + return false; + } + + // For backward compatibility + return this.getBooleanProperty(SecurityUtils.EDDSA_SUPPORTED_PROP, true); + } + + @Override + public Provider getSecurityProvider() { + try { + return getOrCreateProvider(PROVIDER_CLASS); + } catch (ReflectiveOperationException t) { + Throwable e = GenericUtils.peelException(t); + log.error("getSecurityProvider({}) failed ({}) to instantiate {}: {}", + getName(), e.getClass().getSimpleName(), PROVIDER_CLASS, e.getMessage()); + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + + throw new RuntimeException(e); + } + } + + @Override + public boolean isSecurityEntitySupported(Class<?> entityType, String name) { + if (!isSupported()) { + return false; + } + + if (KeyPairGenerator.class.isAssignableFrom(entityType) + || KeyFactory.class.isAssignableFrom(entityType)) { + return Objects.compare(name, getName(), String.CASE_INSENSITIVE_ORDER) == 0; + } else if (Signature.class.isAssignableFrom(entityType)) { + return Objects.compare(SecurityUtils.CURVE_ED25519_SHA512, name, String.CASE_INSENSITIVE_ORDER) == 0; + } else { + return false; + } + } + + @Override + public boolean isSupported() { + Boolean supported; + synchronized (supportHolder) { + supported = supportHolder.get(); + if (supported != null) { + return supported.booleanValue(); + } + + ClassLoader cl = ThreadUtils.resolveDefaultClassLoader(getClass()); + supported = ReflectionUtils.isClassAvailable(cl, "net.i2p.crypto.eddsa.EdDSAKey"); + supportHolder.set(supported); + } + + return supported.booleanValue(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java new file mode 100644 index 0000000..242f550 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java @@ -0,0 +1,201 @@ +/* + * 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.security.eddsa; + +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; +import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.security.SecurityUtils; + +import net.i2p.crypto.eddsa.EdDSAEngine; +import net.i2p.crypto.eddsa.EdDSAKey; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class EdDSASecurityProviderUtils { + // See EdDSANamedCurveTable + public static final String CURVE_ED25519_SHA512 = "Ed25519"; + + private EdDSASecurityProviderUtils() { + throw new UnsupportedOperationException("No instance"); + } + + public static Class<? extends PublicKey> getEDDSAPublicKeyType() { + return EdDSAPublicKey.class; + } + + public static Class<? extends PrivateKey> getEDDSAPrivateKeyType() { + return EdDSAPrivateKey.class; + } + + public static int getEDDSAKeySize(Key key) { + return (SecurityUtils.isEDDSACurveSupported() && (key instanceof EdDSAKey)) ? 256 : -1; + } + + public static boolean compareEDDSAPPublicKeys(PublicKey k1, PublicKey k2) { + if (!SecurityUtils.isEDDSACurveSupported()) { + return false; + } + + if ((k1 instanceof EdDSAPublicKey) && (k2 instanceof EdDSAPublicKey)) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } + + EdDSAPublicKey ed1 = (EdDSAPublicKey) k1; + EdDSAPublicKey ed2 = (EdDSAPublicKey) k2; + return Arrays.equals(ed1.getAbyte(), ed2.getAbyte()) + && compareEDDSAKeyParams(ed1.getParams(), ed2.getParams()); + } + + return false; + } + + public static boolean isEDDSASignatureAlgorithm(String algorithm) { + return EdDSAEngine.SIGNATURE_ALGORITHM.equalsIgnoreCase(algorithm); + } + + public static EdDSAPublicKey recoverEDDSAPublicKey(PrivateKey key) throws GeneralSecurityException { + ValidateUtils.checkTrue(SecurityUtils.isEDDSACurveSupported(), SecurityUtils.EDDSA + " not supported"); + if (!(key instanceof EdDSAPrivateKey)) { + throw new InvalidKeyException("Private key is not " + SecurityUtils.EDDSA); + } + + EdDSAPrivateKey prvKey = (EdDSAPrivateKey) key; + EdDSAPublicKeySpec keySpec = new EdDSAPublicKeySpec(prvKey.getAbyte(), prvKey.getParams()); + KeyFactory factory = SecurityUtils.getKeyFactory(SecurityUtils.EDDSA); + return EdDSAPublicKey.class.cast(factory.generatePublic(keySpec)); + } + + public static org.apache.sshd.common.signature.Signature getEDDSASignature() { + ValidateUtils.checkTrue(SecurityUtils.isEDDSACurveSupported(), SecurityUtils.EDDSA + " not supported"); + return new SignatureEd25519(); + } + + public static boolean isEDDSAKeyFactoryAlgorithm(String algorithm) { + return SecurityUtils.EDDSA.equalsIgnoreCase(algorithm); + } + + public static boolean isEDDSAKeyPairGeneratorAlgorithm(String algorithm) { + return SecurityUtils.EDDSA.equalsIgnoreCase(algorithm); + } + + public static PublicKeyEntryDecoder<? extends PublicKey, ? extends PrivateKey> getEDDSAPublicKeyEntryDecoder() { + ValidateUtils.checkTrue(SecurityUtils.isEDDSACurveSupported(), SecurityUtils.EDDSA + " not supported"); + return Ed25519PublicKeyDecoder.INSTANCE; + } + + public static PrivateKeyEntryDecoder<? extends PublicKey, ? extends PrivateKey> getOpenSSHEDDSAPrivateKeyEntryDecoder() { + ValidateUtils.checkTrue(SecurityUtils.isEDDSACurveSupported(), SecurityUtils.EDDSA + " not supported"); + return OpenSSHEd25519PrivateKeyEntryDecoder.INSTANCE; + } + + public static boolean compareEDDSAPrivateKeys(PrivateKey k1, PrivateKey k2) { + if (!SecurityUtils.isEDDSACurveSupported()) { + return false; + } + + if ((k1 instanceof EdDSAPrivateKey) && (k2 instanceof EdDSAPrivateKey)) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } + + EdDSAPrivateKey ed1 = (EdDSAPrivateKey) k1; + EdDSAPrivateKey ed2 = (EdDSAPrivateKey) k2; + return Arrays.equals(ed1.getSeed(), ed2.getSeed()) + && compareEDDSAKeyParams(ed1.getParams(), ed2.getParams()); + } + + return false; + } + + public static boolean compareEDDSAKeyParams(EdDSAParameterSpec s1, EdDSAParameterSpec s2) { + if (Objects.equals(s1, s2)) { + return true; + } else if (s1 == null || s2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(s1.getHashAlgorithm(), s2.getHashAlgorithm()) + && Objects.equals(s1.getCurve(), s2.getCurve()) + && Objects.equals(s1.getB(), s2.getB()); + } + } + + public static PublicKey generateEDDSAPublicKey(byte[] seed) throws GeneralSecurityException { + if (!SecurityUtils.isEDDSACurveSupported()) { + throw new NoSuchAlgorithmException(SecurityUtils.EDDSA + " not supported"); + } + + EdDSAParameterSpec params = EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512); + EdDSAPublicKeySpec keySpec = new EdDSAPublicKeySpec(seed, params); + KeyFactory factory = SecurityUtils.getKeyFactory(SecurityUtils.EDDSA); + return factory.generatePublic(keySpec); + } + + public static PrivateKey generateEDDSAPrivateKey(byte[] seed) throws GeneralSecurityException { + if (!SecurityUtils.isEDDSACurveSupported()) { + throw new NoSuchAlgorithmException(SecurityUtils.EDDSA + " not supported"); + } + + EdDSAParameterSpec params = EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512); + EdDSAPrivateKeySpec keySpec = new EdDSAPrivateKeySpec(seed, params); + KeyFactory factory = SecurityUtils.getKeyFactory(SecurityUtils.EDDSA); + return factory.generatePrivate(keySpec); + } + + public static <B extends Buffer> B putRawEDDSAPublicKey(B buffer, PublicKey key) { + ValidateUtils.checkTrue(SecurityUtils.isEDDSACurveSupported(), SecurityUtils.EDDSA + " not supported"); + EdDSAPublicKey edKey = ValidateUtils.checkInstanceOf(key, EdDSAPublicKey.class, "Not an EDDSA public key: %s", key); + byte[] seed = Ed25519PublicKeyDecoder.getSeedValue(edKey); + ValidateUtils.checkNotNull(seed, "No seed extracted from key: %s", edKey.getA()); + buffer.putString(KeyPairProvider.SSH_ED25519); + buffer.putBytes(seed); + return buffer; + } + + public static <B extends Buffer> B putEDDSAKeyPair(B buffer, PublicKey pubKey, PrivateKey prvKey) { + ValidateUtils.checkTrue(SecurityUtils.isEDDSACurveSupported(), SecurityUtils.EDDSA + " not supported"); + ValidateUtils.checkInstanceOf(pubKey, EdDSAPublicKey.class, "Not an EDDSA public key: %s", pubKey); + ValidateUtils.checkInstanceOf(prvKey, EdDSAPrivateKey.class, "Not an EDDSA private key: %s", prvKey); + throw new UnsupportedOperationException("Full SSHD-440 implementation N/A"); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java new file mode 100644 index 0000000..4888818 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java @@ -0,0 +1,172 @@ +/* + * 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.security.eddsa; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +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.impl.AbstractPrivateKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.security.SecurityUtils; + +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHEd25519PrivateKeyEntryDecoder extends AbstractPrivateKeyEntryDecoder<EdDSAPublicKey, EdDSAPrivateKey> { + public static final OpenSSHEd25519PrivateKeyEntryDecoder INSTANCE = new OpenSSHEd25519PrivateKeyEntryDecoder(); + private static final int PK_SIZE = 32; + private static final int SK_SIZE = 32; + private static final int KEYPAIR_SIZE = PK_SIZE + SK_SIZE; + + public OpenSSHEd25519PrivateKeyEntryDecoder() { + super(EdDSAPublicKey.class, EdDSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_ED25519))); + } + + @Override + public EdDSAPrivateKey decodePrivateKey(String keyType, FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_ED25519.equals(keyType)) { + throw new InvalidKeyException("Unsupported key type: " + keyType); + } + + if (!SecurityUtils.isEDDSACurveSupported()) { + throw new NoSuchAlgorithmException(SecurityUtils.EDDSA + " provider not supported"); + } + + // ed25519 bernstein naming: pk .. public key, sk .. secret key + // we expect to find two byte arrays with the following structure (type:size): + // [pk:32], [sk:32,pk:32] + + byte[] pk = KeyEntryResolver.readRLEBytes(keyData); + byte[] keypair = KeyEntryResolver.readRLEBytes(keyData); + + if (pk.length != PK_SIZE) { + throw new InvalidKeyException(String.format(Locale.ENGLISH, "Unexpected pk size: %s (expected %s)", pk.length, PK_SIZE)); + } + + if (keypair.length != KEYPAIR_SIZE) { + throw new InvalidKeyException(String.format(Locale.ENGLISH, "Unexpected keypair size: %s (expected %s)", keypair.length, KEYPAIR_SIZE)); + } + + byte[] sk = Arrays.copyOf(keypair, SK_SIZE); + + // verify that the keypair contains the expected pk + // yes, it's stored redundant, this seems to mimic the output structure of the keypair generation interface + if (!Arrays.equals(pk, Arrays.copyOfRange(keypair, SK_SIZE, KEYPAIR_SIZE))) { + throw new InvalidKeyException("Keypair did not contain the public key."); + } + + // create the private key + EdDSAParameterSpec params = EdDSANamedCurveTable.getByName(EdDSASecurityProviderUtils.CURVE_ED25519_SHA512); + EdDSAPrivateKey privateKey = generatePrivateKey(new EdDSAPrivateKeySpec(sk, params)); + + // the private key class contains the calculated public key (Abyte) + // pointers to the corresponding code: + // EdDSAPrivateKeySpec.EdDSAPrivateKeySpec(byte[], EdDSAParameterSpec): A = spec.getB().scalarMultiply(a); + // EdDSAPrivateKey.EdDSAPrivateKey(EdDSAPrivateKeySpec): this.Abyte = this.A.toByteArray(); + + // we can now verify the generated pk matches the one we read + if (!Arrays.equals(privateKey.getAbyte(), pk)) { + throw new InvalidKeyException("The provided pk does NOT match the computed pk for the given sk."); + } + + return privateKey; + } + + @Override + public String encodePrivateKey(OutputStream s, EdDSAPrivateKey key) throws IOException { + Objects.requireNonNull(key, "No private key provided"); + + // ed25519 bernstein naming: pk .. public key, sk .. secret key + // we are expected to write the following arrays (type:size): + // [pk:32], [sk:32,pk:32] + + byte[] sk = key.getSeed(); + byte[] pk = key.getAbyte(); + + Objects.requireNonNull(sk, "No seed"); + + byte[] keypair = new byte[KEYPAIR_SIZE]; + System.arraycopy(sk, 0, keypair, 0, SK_SIZE); + System.arraycopy(pk, 0, keypair, SK_SIZE, PK_SIZE); + + KeyEntryResolver.writeRLEBytes(s, pk); + KeyEntryResolver.writeRLEBytes(s, keypair); + + return KeyPairProvider.SSH_ED25519; + } + + @Override + public boolean isPublicKeyRecoverySupported() { + return true; + } + + @Override + public EdDSAPublicKey recoverPublicKey(EdDSAPrivateKey prvKey) throws GeneralSecurityException { + return EdDSASecurityProviderUtils.recoverEDDSAPublicKey(prvKey); + } + + @Override + public EdDSAPublicKey clonePublicKey(EdDSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePublicKey(new EdDSAPublicKeySpec(key.getA(), key.getParams())); + } + } + + @Override + public EdDSAPrivateKey clonePrivateKey(EdDSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePrivateKey(new EdDSAPrivateKeySpec(key.getSeed(), key.getParams())); + } + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(SecurityUtils.EDDSA); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(SecurityUtils.EDDSA); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java new file mode 100644 index 0000000..012be95 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java @@ -0,0 +1,49 @@ +/* + * 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.security.eddsa; + +import java.util.Map; + +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.signature.AbstractSignature; +import org.apache.sshd.common.util.ValidateUtils; + +import net.i2p.crypto.eddsa.EdDSAEngine; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class SignatureEd25519 extends AbstractSignature { + public SignatureEd25519() { + super(EdDSAEngine.SIGNATURE_ALGORITHM); + } + + @Override + public boolean verify(byte[] sig) throws Exception { + byte[] data = sig; + Map.Entry<String, byte[]> encoding = extractEncodedSignature(data); + if (encoding != null) { + String keyType = encoding.getKey(); + ValidateUtils.checkTrue(KeyPairProvider.SSH_ED25519.equals(keyType), "Mismatched key type: %s", keyType); + data = encoding.getValue(); + } + + return doVerify(data); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/threads/CloseableExecutorService.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/threads/CloseableExecutorService.java b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/CloseableExecutorService.java new file mode 100644 index 0000000..3b9beeb --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/CloseableExecutorService.java @@ -0,0 +1,28 @@ +/* + * 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.threads; + +import java.util.concurrent.ExecutorService; + +import org.apache.sshd.common.Closeable; + +public interface CloseableExecutorService extends ExecutorService, Closeable { + // Nothing extra +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ExecutorServiceCarrier.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ExecutorServiceCarrier.java b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ExecutorServiceCarrier.java new file mode 100644 index 0000000..b44bd46 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ExecutorServiceCarrier.java @@ -0,0 +1,31 @@ +/* + * 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.threads; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface ExecutorServiceCarrier { + /** + * @return The {@link CloseableExecutorService} to use + */ + CloseableExecutorService getExecutorService(); + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/threads/NoCloseExecutor.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/threads/NoCloseExecutor.java b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/NoCloseExecutor.java new file mode 100644 index 0000000..cb42805 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/NoCloseExecutor.java @@ -0,0 +1,160 @@ +/* + * 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.threads; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.DefaultCloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * Wraps an {@link ExecutorService} as a {@link CloseableExecutorService} + * and avoids calling its {@code shutdown} methods when the wrapper is shut down + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class NoCloseExecutor implements CloseableExecutorService { + protected final ExecutorService executor; + protected final CloseFuture closeFuture; + + public NoCloseExecutor(ExecutorService executor) { + this.executor = executor; + closeFuture = new DefaultCloseFuture(null, null); + } + + @Override + public <T> Future<T> submit(Callable<T> task) { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.submit(task); + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.submit(task, result); + } + + @Override + public Future<?> submit(Runnable task) { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.submit(task); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) + throws InterruptedException { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.invokeAll(tasks); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.invokeAll(tasks, timeout, unit); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks) + throws InterruptedException, ExecutionException { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.invokeAny(tasks); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + return executor.invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + ValidateUtils.checkState(!isShutdown(), "Executor has been shut down"); + executor.execute(command); + } + + @Override + public void shutdown() { + close(true); + } + + @Override + public List<Runnable> shutdownNow() { + close(true); + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return isClosed(); + } + + @Override + public boolean isTerminated() { + return isClosed(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + try { + return closeFuture.await(timeout, unit); + } catch (IOException e) { + throw (InterruptedException) new InterruptedException().initCause(e); + } + } + + @Override + public CloseFuture close(boolean immediately) { + closeFuture.setClosed(); + return closeFuture; + } + + @Override + public void addCloseFutureListener(SshFutureListener<CloseFuture> listener) { + closeFuture.addListener(listener); + } + + @Override + public void removeCloseFutureListener(SshFutureListener<CloseFuture> listener) { + closeFuture.removeListener(listener); + } + + @Override + public boolean isClosed() { + return closeFuture.isClosed(); + } + + @Override + public boolean isClosing() { + return isClosed(); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshThreadPoolExecutor.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshThreadPoolExecutor.java b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshThreadPoolExecutor.java new file mode 100644 index 0000000..ccaa655 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshThreadPoolExecutor.java @@ -0,0 +1,138 @@ +/* + * 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.threads; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.util.closeable.AbstractCloseable; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class SshThreadPoolExecutor extends ThreadPoolExecutor implements CloseableExecutorService { + protected final DelegateCloseable closeable = new DelegateCloseable(); + + protected class DelegateCloseable extends AbstractCloseable { + protected DelegateCloseable() { + super(); + } + + @Override + protected CloseFuture doCloseGracefully() { + shutdown(); + return closeFuture; + } + + @Override + protected void doCloseImmediately() { + shutdownNow(); + super.doCloseImmediately(); + } + + protected void setClosed() { + closeFuture.setClosed(); + } + } + + public SshThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + + public SshThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + } + + public SshThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); + } + + public SshThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + } + + @Override + protected void terminated() { + closeable.doCloseImmediately(); + } + + @Override + public void shutdown() { + super.shutdown(); + } + + @Override + public List<Runnable> shutdownNow() { + return super.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return super.isShutdown(); + } + + @Override + public boolean isTerminating() { + return super.isTerminating(); + } + + @Override + public boolean isTerminated() { + return super.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return super.awaitTermination(timeout, unit); + } + + @Override + public CloseFuture close(boolean immediately) { + return closeable.close(immediately); + } + + @Override + public void addCloseFutureListener(SshFutureListener<CloseFuture> listener) { + closeable.addCloseFutureListener(listener); + } + + @Override + public void removeCloseFutureListener(SshFutureListener<CloseFuture> listener) { + closeable.removeCloseFutureListener(listener); + } + + @Override + public boolean isClosed() { + return closeable.isClosed(); + } + + @Override + public boolean isClosing() { + return closeable.isClosing(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshdThreadFactory.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshdThreadFactory.java b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshdThreadFactory.java new file mode 100644 index 0000000..5dc0c7b --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshdThreadFactory.java @@ -0,0 +1,78 @@ +/* + * 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.threads; + +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * Default {@link ThreadFactory} used by {@link ThreadUtils} to create + * thread pools if user did provide one + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class SshdThreadFactory extends AbstractLoggingBean implements ThreadFactory { + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public SshdThreadFactory(String name) { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + String effectiveName = name.replace(' ', '-'); + namePrefix = "sshd-" + effectiveName + "-thread-"; + } + + @Override + public Thread newThread(Runnable r) { + Thread t; + try { + // see SSHD-668 + if (System.getSecurityManager() != null) { + t = AccessController.doPrivileged((PrivilegedExceptionAction<Thread>) () -> + new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0)); + } else { + t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + } + } catch (PrivilegedActionException e) { + Exception err = e.getException(); + if (err instanceof RuntimeException) { + throw (RuntimeException) err; + } else { + throw new RuntimeException(err); + } + } + + if (!t.isDaemon()) { + t.setDaemon(true); + } + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + if (log.isTraceEnabled()) { + log.trace("newThread({})[{}] runnable={}", group, t.getName(), r); + } + return t; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ThreadUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ThreadUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ThreadUtils.java new file mode 100644 index 0000000..c803389 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/threads/ThreadUtils.java @@ -0,0 +1,185 @@ +/* + * 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.threads; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Utility class for thread pools. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class ThreadUtils { + private ThreadUtils() { + throw new UnsupportedOperationException("No instance"); + } + + /** + * Wraps an {@link CloseableExecutorService} in such a way as to "protect" + * it for calls to the {@link CloseableExecutorService#shutdown()} or + * {@link CloseableExecutorService#shutdownNow()}. All other calls are delegated as-is + * to the original service. <B>Note:</B> the exposed wrapped proxy will + * answer correctly the {@link CloseableExecutorService#isShutdown()} query if indeed + * one of the {@code shutdown} methods was invoked. + * + * @param executorService The original service - ignored if {@code null} + * @param shutdownOnExit If {@code true} then it is OK to shutdown the executor + * so no wrapping takes place. + * @return Either the original service or a wrapped one - depending on the + * value of the <tt>shutdownOnExit</tt> parameter + */ + public static CloseableExecutorService protectExecutorServiceShutdown(CloseableExecutorService executorService, boolean shutdownOnExit) { + if (executorService == null || shutdownOnExit || executorService instanceof NoCloseExecutor) { + return executorService; + } else { + return new NoCloseExecutor(executorService); + } + } + + public static CloseableExecutorService noClose(CloseableExecutorService executorService) { + return protectExecutorServiceShutdown(executorService, false); + } + + public static ClassLoader resolveDefaultClassLoader(Object anchor) { + return resolveDefaultClassLoader(anchor == null ? null : anchor.getClass()); + } + + public static Iterable<ClassLoader> resolveDefaultClassLoaders(Object anchor) { + return resolveDefaultClassLoaders(anchor == null ? null : anchor.getClass()); + } + + public static <T> T createDefaultInstance(Class<?> anchor, Class<T> targetType, String className) + throws ReflectiveOperationException { + return createDefaultInstance(resolveDefaultClassLoaders(anchor), targetType, className); + } + + public static <T> T createDefaultInstance(ClassLoader cl, Class<T> targetType, String className) + throws ReflectiveOperationException { + Class<?> instanceType = cl.loadClass(className); + Object instance = instanceType.newInstance(); + return targetType.cast(instance); + } + + public static <T> T createDefaultInstance(Iterable<ClassLoader> cls, Class<T> targetType, String className) + throws ReflectiveOperationException { + for (ClassLoader cl : cls) { + try { + return createDefaultInstance(cl, targetType, className); + } catch (ClassNotFoundException e) { + // Ignore + } + } + throw new ClassNotFoundException(className); + } + + /** + * <P>Attempts to find the most suitable {@link ClassLoader} as follows:</P> + * <UL> + * <LI><P> + * Check the {@link Thread#getContextClassLoader()} value + * </P></LI> + * + * <LI><P> + * If no thread context class loader then check the anchor + * class (if given) for its class loader + * </P></LI> + * + * <LI><P> + * If still no loader available, then use {@link ClassLoader#getSystemClassLoader()} + * </P></LI> + * </UL> + * + * @param anchor The anchor {@link Class} to use if no current thread + * - ignored if {@code null} + * context class loader + * @return The resolver {@link ClassLoader} + */ + public static ClassLoader resolveDefaultClassLoader(Class<?> anchor) { + Thread thread = Thread.currentThread(); + ClassLoader cl = thread.getContextClassLoader(); + if (cl != null) { + return cl; + } + + if (anchor != null) { + cl = anchor.getClassLoader(); + } + + if (cl == null) { // can happen for core Java classes + cl = ClassLoader.getSystemClassLoader(); + } + + return cl; + } + + public static Iterable<ClassLoader> resolveDefaultClassLoaders(Class<?> anchor) { + Set<ClassLoader> cls = new LinkedHashSet<>(); + Thread thread = Thread.currentThread(); + ClassLoader cl = thread.getContextClassLoader(); + if (cl != null) { + cls.add(cl); + } + if (anchor != null) { + cls.add(anchor.getClassLoader()); + } + cls.add(ClassLoader.getSystemClassLoader()); + return cls; + } + + public static CloseableExecutorService newFixedThreadPoolIf(CloseableExecutorService executorService, String poolName, int nThreads) { + return executorService == null ? newFixedThreadPool(poolName, nThreads) : executorService; + } + + public static CloseableExecutorService newFixedThreadPool(String poolName, int nThreads) { + return new SshThreadPoolExecutor( + nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, // TODO make this configurable + new LinkedBlockingQueue<>(), + new SshdThreadFactory(poolName), + new ThreadPoolExecutor.CallerRunsPolicy()); + } + + public static CloseableExecutorService newCachedThreadPoolIf(CloseableExecutorService executorService, String poolName) { + return executorService == null ? newCachedThreadPool(poolName) : executorService; + } + + public static CloseableExecutorService newCachedThreadPool(String poolName) { + return new SshThreadPoolExecutor( + 0, Integer.MAX_VALUE, // TODO make this configurable + 60L, TimeUnit.SECONDS, // TODO make this configurable + new SynchronousQueue<>(), + new SshdThreadFactory(poolName), + new ThreadPoolExecutor.CallerRunsPolicy()); + } + + public static ScheduledExecutorService newSingleThreadScheduledExecutor(String poolName) { + return new ScheduledThreadPoolExecutor(1, new SshdThreadFactory(poolName)); + } + + public static CloseableExecutorService newSingleThreadExecutor(String poolName) { + return newFixedThreadPool(poolName, 1); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/AbstractGeneratorHostKeyProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/AbstractGeneratorHostKeyProvider.java b/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/AbstractGeneratorHostKeyProvider.java new file mode 100644 index 0000000..9131f99 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/AbstractGeneratorHostKeyProvider.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.server.keyprovider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.BuiltinIdentities; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * Holds a <U>single</U> {@link KeyPair} which is generated the 1st time + * {@link #loadKeys()} is called. If there is a file backing it up and the + * file exists, the key is loaded from it. Otherwise a new key pair is + * generated and saved (provided a path is configured and {@link #isOverwriteAllowed()} + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractGeneratorHostKeyProvider extends AbstractKeyPairProvider { + public static final String DEFAULT_ALGORITHM = KeyUtils.RSA_ALGORITHM; + public static final boolean DEFAULT_ALLOWED_TO_OVERWRITE = true; + + private final AtomicReference<KeyPair> keyPairHolder = new AtomicReference<>(); + + private Path path; + private String algorithm = DEFAULT_ALGORITHM; + private int keySize; + private AlgorithmParameterSpec keySpec; + private boolean overwriteAllowed = DEFAULT_ALLOWED_TO_OVERWRITE; + + protected AbstractGeneratorHostKeyProvider() { + super(); + } + + public Path getPath() { + return path; + } + + public void setFile(File file) { + setPath((file == null) ? null : file.toPath()); + } + + public void setPath(Path path) { + this.path = (path == null) ? null : path.toAbsolutePath(); + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public int getKeySize() { + return keySize; + } + + public void setKeySize(int keySize) { + this.keySize = keySize; + } + + public AlgorithmParameterSpec getKeySpec() { + return keySpec; + } + + public void setKeySpec(AlgorithmParameterSpec keySpec) { + this.keySpec = keySpec; + } + + public boolean isOverwriteAllowed() { + return overwriteAllowed; + } + + public void setOverwriteAllowed(boolean overwriteAllowed) { + this.overwriteAllowed = overwriteAllowed; + } + + public void clearLoadedKeys() { + KeyPair kp; + synchronized (keyPairHolder) { + kp = keyPairHolder.getAndSet(null); + } + + if ((kp != null) & log.isDebugEnabled()) { + PublicKey key = kp.getPublic(); + log.debug("clearLoadedKeys({}) removed key={}-{}", + getPath(), KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + } + } + + @Override // co-variant return + public synchronized List<KeyPair> loadKeys() { + Path keyPath = getPath(); + KeyPair kp; + synchronized (keyPairHolder) { + kp = keyPairHolder.get(); + if (kp == null) { + try { + kp = resolveKeyPair(keyPath); + if (kp != null) { + keyPairHolder.set(kp); + } + } catch (Throwable t) { + log.warn("loadKeys({}) Failed ({}) to resolve: {}", + keyPath, t.getClass().getSimpleName(), t.getMessage()); + if (log.isDebugEnabled()) { + log.debug("loadKeys(" + keyPath + ") resolution failure details", t); + } + } + } + } + + if (kp == null) { + return Collections.emptyList(); + } else { + return Collections.singletonList(kp); + } + } + + protected KeyPair resolveKeyPair(Path keyPath) throws IOException, GeneralSecurityException { + String alg = getAlgorithm(); + KeyPair kp; + if (keyPath != null) { + try { + kp = loadFromFile(alg, keyPath); + if (kp != null) { + return kp; + } + } catch (Throwable e) { + log.warn("resolveKeyPair({}) Failed ({}) to load: {}", + keyPath, e.getClass().getSimpleName(), e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("resolveKeyPair(" + keyPath + ") load failure details", e); + } + } + } + + // either no file specified or no key in file + try { + kp = generateKeyPair(alg); + if (kp == null) { + return null; + } + + if (log.isDebugEnabled()) { + PublicKey key = kp.getPublic(); + log.debug("resolveKeyPair({}) generated {} key={}-{}", + keyPath, alg, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + } + } catch (Throwable e) { + log.warn("resolveKeyPair({})[{}] Failed ({}) to generate {} key-pair: {}", + keyPath, alg, e.getClass().getSimpleName(), alg, e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("resolveKeyPair(" + keyPath + ")[" + alg + "] key-pair generation failure details", e); + } + + return null; + } + + if (keyPath != null) { + try { + writeKeyPair(kp, keyPath); + } catch (Throwable e) { + log.warn("resolveKeyPair({})[{}] Failed ({}) to write {} key: {}", + alg, keyPath, e.getClass().getSimpleName(), alg, e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("resolveKeyPair(" + keyPath + ")[" + alg + "] write failure details", e); + } + } + } + + return kp; + } + + protected KeyPair loadFromFile(String alg, Path keyPath) throws IOException, GeneralSecurityException { + LinkOption[] options = IoUtils.getLinkOptions(true); + if ((!Files.exists(keyPath, options)) || (!Files.isRegularFile(keyPath, options))) { + return null; + } + + KeyPair kp = readKeyPair(keyPath, IoUtils.EMPTY_OPEN_OPTIONS); + if (kp == null) { + return null; + } + + PublicKey key = kp.getPublic(); + String keyAlgorithm = key.getAlgorithm(); + if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyAlgorithm)) { + keyAlgorithm = KeyUtils.EC_ALGORITHM; + } else if (BuiltinIdentities.Constants.ED25519.equalsIgnoreCase(keyAlgorithm)) { + keyAlgorithm = SecurityUtils.EDDSA; + } + + if (Objects.equals(alg, keyAlgorithm)) { + if (log.isDebugEnabled()) { + log.debug("resolveKeyPair({}) loaded key={}-{}", + keyPath, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + } + return kp; + } + + // Not same algorithm - start again + if (log.isDebugEnabled()) { + log.debug("resolveKeyPair({}) mismatched loaded key algorithm: expected={}, loaded={}", + keyPath, alg, keyAlgorithm); + } + Files.deleteIfExists(keyPath); + return null; + } + + protected KeyPair readKeyPair(Path keyPath, OpenOption... options) throws IOException, GeneralSecurityException { + try (InputStream inputStream = Files.newInputStream(keyPath, options)) { + return doReadKeyPair(keyPath.toString(), inputStream); + } + } + + protected KeyPair doReadKeyPair(String resourceKey, InputStream inputStream) throws IOException, GeneralSecurityException { + return SecurityUtils.loadKeyPairIdentity(resourceKey, inputStream, null); + } + + protected void writeKeyPair(KeyPair kp, Path keyPath, OpenOption... options) throws IOException, GeneralSecurityException { + if ((!Files.exists(keyPath)) || isOverwriteAllowed()) { + try (OutputStream os = Files.newOutputStream(keyPath, options)) { + doWriteKeyPair(keyPath.toString(), kp, os); + } catch (Throwable e) { + log.warn("writeKeyPair({}) failed ({}) to write key {}: {}", + keyPath, e.getClass().getSimpleName(), e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("writeKeyPair(" + keyPath + ") write failure details", e); + } + } + } else { + log.error("Overwriting key ({}) is disabled: using throwaway {}: {}", + keyPath, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint((kp == null) ? null : kp.getPublic())); + } + } + + protected abstract void doWriteKeyPair(String resourceKey, KeyPair kp, OutputStream outputStream) throws IOException, GeneralSecurityException; + + protected KeyPair generateKeyPair(String algorithm) throws GeneralSecurityException { + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm); + if (keySpec != null) { + generator.initialize(keySpec); + log.info("generateKeyPair(" + algorithm + ") generating host key - spec=" + keySpec.getClass().getSimpleName()); + } else if (keySize != 0) { + generator.initialize(keySize); + log.info("generateKeyPair(" + algorithm + ") generating host key - size=" + keySize); + } else if (KeyUtils.EC_ALGORITHM.equals(algorithm)) { + // If left to our own devices choose the biggest key size possible + int numCurves = ECCurves.SORTED_KEY_SIZE.size(); + ECCurves curve = ECCurves.SORTED_KEY_SIZE.get(numCurves - 1); + generator.initialize(curve.getParameters()); + } + + return generator.generateKeyPair(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/SimpleGeneratorHostKeyProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/SimpleGeneratorHostKeyProvider.java b/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/SimpleGeneratorHostKeyProvider.java new file mode 100644 index 0000000..3bccde8 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/server/keyprovider/SimpleGeneratorHostKeyProvider.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.server.keyprovider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.spec.InvalidKeySpecException; + +/** + * TODO Add javadoc + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class SimpleGeneratorHostKeyProvider extends AbstractGeneratorHostKeyProvider { + public SimpleGeneratorHostKeyProvider() { + super(); + } + + public SimpleGeneratorHostKeyProvider(File file) { + this((file == null) ? null : file.toPath()); + } + + public SimpleGeneratorHostKeyProvider(Path path) { + setPath(path); + } + + @Override + protected KeyPair doReadKeyPair(String resourceKey, InputStream inputStream) throws IOException, GeneralSecurityException { + try (ObjectInputStream r = new ObjectInputStream(inputStream)) { + try { + return (KeyPair) r.readObject(); + } catch (ClassNotFoundException e) { + throw new InvalidKeySpecException("Missing classes: " + e.getMessage(), e); + } + } + } + + @Override + protected void doWriteKeyPair(String resourceKey, KeyPair kp, OutputStream outputStream) throws IOException, GeneralSecurityException { + try (ObjectOutputStream w = new ObjectOutputStream(outputStream)) { + w.writeObject(kp); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/test/java/org/apache/sshd/client/auth/password/PasswordIdentityProviderTest.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/test/java/org/apache/sshd/client/auth/password/PasswordIdentityProviderTest.java b/sshd-common/src/test/java/org/apache/sshd/client/auth/password/PasswordIdentityProviderTest.java new file mode 100644 index 0000000..165805d --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/client/auth/password/PasswordIdentityProviderTest.java @@ -0,0 +1,72 @@ +/* + * 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.client.auth.password; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class PasswordIdentityProviderTest extends JUnitTestSupport { + public PasswordIdentityProviderTest() { + super(); + } + + @Test + public void testMultiProvider() { + String[][] values = { + {getClass().getSimpleName(), getCurrentTestName()}, + {new Date(System.currentTimeMillis()).toString()}, + {getClass().getPackage().getName()} + }; + List<String> expected = new ArrayList<>(); + Collection<PasswordIdentityProvider> providers = new LinkedList<>(); + for (String[] va : values) { + Collection<String> passwords = Arrays.asList(va); + expected.addAll(passwords); + + PasswordIdentityProvider p = PasswordIdentityProvider.wrapPasswords(passwords); + assertProviderContents("Wrapped", p, passwords); + providers.add(p); + } + + PasswordIdentityProvider p = PasswordIdentityProvider.multiProvider(providers); + assertProviderContents("Multi", p, expected); + } + + private static void assertProviderContents(String message, PasswordIdentityProvider p, Iterable<String> expected) { + assertNotNull(message + ": no provider", p); + assertEquals(message, expected, p.loadPasswords()); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java new file mode 100644 index 0000000..741ba7b --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java @@ -0,0 +1,139 @@ +/* + * 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.client.config.hosts; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class ConfigFileHostEntryResolverTest extends JUnitTestSupport { + public ConfigFileHostEntryResolverTest() { + super(); + } + + @Test + public void testConfigFileReload() throws IOException { + Path dir = getTempTargetRelativeFile(getClass().getSimpleName()); + AtomicInteger reloadCount = new AtomicInteger(); + ConfigFileHostEntryResolver resolver = new ConfigFileHostEntryResolver(assertHierarchyTargetFolderExists(dir).resolve(getCurrentTestName() + ".config.txt")) { + @Override + protected List<HostConfigEntry> reloadHostConfigEntries(Path path, String host, int port, String username) + throws IOException { + reloadCount.incrementAndGet(); + return super.reloadHostConfigEntries(path, host, port, username); + } + }; + Path path = resolver.getPath(); + + HostConfigEntry expected = new HostConfigEntry(getCurrentTestName(), getCurrentTestName(), 7365, getCurrentTestName()); + testConfigFileReload("Non-existing", path, reloadCount, null, resolver, expected, null); + testConfigFileReload("Empty", path, reloadCount, Collections.emptyList(), resolver, expected, null); + testConfigFileReload("Global", path, reloadCount, + Collections.singletonList(new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, expected.getHost(), expected.getPort(), expected.getUsername())), + resolver, expected, expected); + testConfigFileReload("Wildcard", path, reloadCount, + Arrays.asList( + new HostConfigEntry( + HostPatternsHolder.ALL_HOSTS_PATTERN, + getClass().getSimpleName(), + 1234, + getClass().getSimpleName()), + new HostConfigEntry( + expected.getHost() + Character.toString(HostPatternsHolder.WILDCARD_PATTERN), + expected.getHost(), + expected.getPort(), + expected.getUsername())), + resolver, expected, expected); + testConfigFileReload("Specific", path, reloadCount, + Arrays.asList( + new HostConfigEntry( + HostPatternsHolder.ALL_HOSTS_PATTERN, + getClass().getSimpleName(), + 1234, + getClass().getSimpleName()), + new HostConfigEntry( + getClass().getSimpleName() + Character.toString(HostPatternsHolder.WILDCARD_PATTERN), + getClass().getSimpleName(), + 1234, + getClass().getSimpleName()), + expected), + resolver, expected, expected); + } + + private static void testConfigFileReload( + String phase, Path path, AtomicInteger reloadCount, + Collection<? extends HostConfigEntry> entries, + HostConfigEntryResolver resolver, + HostConfigEntry query, + HostConfigEntry expected) + throws IOException { + if (entries == null) { + if (Files.exists(path)) { + Files.delete(path); + } + } else { + HostConfigEntry.writeHostConfigEntries(path, entries, IoUtils.EMPTY_OPEN_OPTIONS); + } + + reloadCount.set(0); + + for (int index = 1; index < Byte.SIZE; index++) { + HostConfigEntry actual = + resolver.resolveEffectiveHost(query.getHostName(), query.getPort(), query.getUsername()); + + if (entries == null) { + assertEquals(phase + "[" + index + "]: mismatched reload count", 0, reloadCount.get()); + } else { + assertEquals(phase + "[" + index + "]: mismatched reload count", 1, reloadCount.get()); + } + + if (expected == null) { + assertNull(phase + "[" + index + "]: Unexpected success for " + query, actual); + } else { + assertNotNull(phase + "[" + index + "]: No result for " + query, actual); + assertNotSame(phase + "[" + index + "]: No cloned result for " + query, expected, actual); + assertEquals(phase + "[" + index + "]: Mismatched host for " + query, + expected.getHostName(), actual.getHostName()); + assertEquals(phase + "[" + index + "]: Mismatched port for " + query, + expected.getPort(), actual.getPort()); + assertEquals(phase + "[" + index + "]: Mismatched user for " + query, + expected.getUsername(), actual.getUsername()); + } + } + } +}
