Repository: apache/mina-sshd
Issue: SSHD Client failing on private key file with key password

### Version

2.9.2

### Bug description

When attempting to run the SSH client with that private key file that is protected with a password the Mina SSH client doesn't make the connection.

The client was run using the command

`java -jar MinaSshClient-2.9.2.jar -i K:\\id_rsa myuser@192.168.247.129`



### Actual behavior

The client seems to fail to load the key resource with the following warning

`WARNING: org.apache.sshd.cli.client.SshCl: Failed (FailedLoginException) to load key resource=K:\\id_rsa: No password provider for encrypted key in K:\\id_rsa`

the connection subsequently fails with a "No more authentication methods available"


### Expected behavior

The client should prompt for a password for the key file or have another command line parameter to specify it.

### Relevant log output

_No response_

### Other information

I did a bit of debugging on this and for whatever reason the prompt for the password for the private key file isn't being triggered by the code in the _SshClientCliSupport_,  _setupSessionIdentities_ method.  

Whether or not it's the correct solution what  I did in my own code is I used the setPasswordFinder method on the FileKeyPairProvider with the same code segment as for the client `setFilePasswordProvider` and it seemed to work.


Changed files:
sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MultiKeyIdentityProvider.java
sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
sshd-core/src/main/java/org/apache/sshd/client/SshClient.java

diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MultiKeyIdentityProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MultiKeyIdentityProvider.java
index 08e4429c8..697e8edcc 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MultiKeyIdentityProvider.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MultiKeyIdentityProvider.java
@@ -32,12 +32,17 @@ import org.apache.sshd.common.session.SessionContext;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class MultiKeyIdentityProvider implements KeyIdentityProvider {
+
     protected final Iterable<? extends KeyIdentityProvider> providers;
 
     public MultiKeyIdentityProvider(Iterable<? extends KeyIdentityProvider> providers) {
         this.providers = providers;
     }
 
+    public Iterable<? extends KeyIdentityProvider> getProviders() {
+        return providers == null ? Collections::emptyIterator : providers;
+    }
+
     @Override
     public Iterable<KeyPair> loadKeys(SessionContext session) {
         return new Iterable<KeyPair>() {
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 3c9cbc1ab..cc493b214 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -78,6 +78,7 @@ import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.ServiceFactory;
 import org.apache.sshd.common.channel.ChannelFactory;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProviderManager;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.future.SshFutureListener;
@@ -85,8 +86,10 @@ import org.apache.sshd.common.helpers.AbstractFactoryManager;
 import org.apache.sshd.common.io.IoConnectFuture;
 import org.apache.sshd.common.io.IoConnector;
 import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.keyprovider.MultiKeyIdentityProvider;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.ExceptionUtils;
 import org.apache.sshd.common.util.GenericUtils;
@@ -742,16 +745,49 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
 
         if (useDefaultIdentities) {
             setupDefaultSessionIdentities(session, identities);
+        } else if (identities == null) {
+            session.setKeyIdentityProvider(KeyIdentityProvider.EMPTY_KEYS_PROVIDER);
         } else {
-            session.setKeyIdentityProvider(
-                    (identities == null)
-                            ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
-                            : identities);
+            session.setKeyIdentityProvider(ensureFilePasswordProvider(identities));
         }
 
         connectFuture.setSession(session);
     }
 
+    /**
+     * Sets this client's {@link FilePasswordProvider} on the {@link KeyIdentityProvider} if it is an
+     * {@link AbstractResourceKeyPairProvider} or implements {@link FilePasswordProviderManager} and doesn't have one
+     * yet. If the given {@code identities} is a {@link MultiKeyIdentityProvider}, the wrapped identity providers are
+     * set up.
+     *
+     * @param  identities {@link KeyIdentityProvider} to set up
+     * @return            a {@link KeyIdentityProvider} configured with a {@link FilePasswordProvider}
+     * @see               #getFilePasswordProvider()
+     */
+    protected KeyIdentityProvider ensureFilePasswordProvider(KeyIdentityProvider identities) {
+        if (identities instanceof AbstractResourceKeyPairProvider<?>) {
+            AbstractResourceKeyPairProvider<?> keyProvider = (AbstractResourceKeyPairProvider<?>) identities;
+            if (keyProvider.getPasswordFinder() == null) {
+                FilePasswordProvider passwordProvider = getFilePasswordProvider();
+                if (passwordProvider != null) {
+                    keyProvider.setPasswordFinder(passwordProvider);
+                }
+            }
+        } else if (identities instanceof FilePasswordProviderManager) {
+            FilePasswordProviderManager keyProvider = (FilePasswordProviderManager) identities;
+            if (keyProvider.getFilePasswordProvider() == null) {
+                FilePasswordProvider passwordProvider = getFilePasswordProvider();
+                if (passwordProvider != null) {
+                    keyProvider.setFilePasswordProvider(passwordProvider);
+                }
+            }
+        } else if (identities instanceof MultiKeyIdentityProvider) {
+            MultiKeyIdentityProvider multiProvider = (MultiKeyIdentityProvider) identities;
+            multiProvider.getProviders().forEach(this::ensureFilePasswordProvider);
+        }
+        return identities;
+    }
+
     protected void setupDefaultSessionIdentities(
             ClientSession session, KeyIdentityProvider extraIdentities)
             throws IOException, GeneralSecurityException {
@@ -765,8 +801,9 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
             }
         }
 
-        // Prefer the extra identities to come first since they were probably indicate by the host-config entry
+        // Prefer the extra identities to come first since they were probably indicated by the host-config entry
         KeyIdentityProvider kpEffective = KeyIdentityProvider.resolveKeyIdentityProvider(extraIdentities, kpSession);
+        kpEffective = ensureFilePasswordProvider(kpEffective);
         if (!UnaryEquator.isSameReference(kpSession, kpEffective)) {
             if (debugEnabled) {
                 log.debug("setupDefaultSessionIdentities({}) key identity provider enhanced", session);
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
index a164956cd..dc63fd486 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
@@ -31,6 +31,9 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.security.KeyPair;
 import java.util.Arrays;
 import java.util.Collection;
@@ -76,6 +79,8 @@ import org.apache.sshd.common.channel.ChannelListener;
 import org.apache.sshd.common.channel.StreamingChannel;
 import org.apache.sshd.common.channel.exception.SshChannelClosedException;
 import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyEncryptionContext;
+import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter;
 import org.apache.sshd.common.future.CloseFuture;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.IoInputStream;
@@ -83,6 +88,7 @@ import org.apache.sshd.common.io.IoOutputStream;
 import org.apache.sshd.common.io.IoReadFuture;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.Session;
@@ -93,6 +99,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.core.CoreModuleProperties;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
@@ -1048,6 +1055,45 @@ public class ClientTest extends BaseTestSupport {
         assertNull("Session closure not signalled", clientSessionHolder.get());
     }
 
+    @Test
+    public void testPublicKeyAuthWithEncryptedKey() throws Exception {
+        // Create an encrypted private key file
+        KeyPair pair = SecurityUtils.getKeyPairGenerator("RSA").generateKeyPair();
+        Path keyFile = getTestResourcesFolder().resolve("userKey");
+        Files.deleteIfExists(keyFile);
+        OpenSSHKeyEncryptionContext options = new OpenSSHKeyEncryptionContext();
+        options.setPassword("test-passphrase");
+        options.setCipherName("AES");
+        options.setCipherMode("CTR");
+        options.setCipherType("256");
+        try (OutputStream out = Files.newOutputStream(keyFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
+            OpenSSHKeyPairResourceWriter.INSTANCE.writePrivateKey(pair, "test key", options, out);
+        }
+        // The server accepts only this key
+        sshd.setPublickeyAuthenticator((username, key, session) -> KeyUtils.compareKeys(key, pair.getPublic()));
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+        // Configure the client to use the encrypted key file
+        client.setKeyIdentityProvider(new FileKeyPairProvider(keyFile));
+        AtomicBoolean passwordProvided = new AtomicBoolean();
+        client.setFilePasswordProvider((session, file, index) -> {
+            passwordProvided.set(true);
+            return "test-passphrase";
+        });
+        client.setUserAuthFactories(Collections.singletonList(UserAuthPublicKeyFactory.INSTANCE));
+        client.start();
+
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT)
+                .getSession()) {
+            assertNotNull("Client session creation not signalled", clientSessionHolder.get());
+            session.auth().verify(AUTH_TIMEOUT);
+            assertTrue("Password provider should have been called", passwordProvided.get());
+        } finally {
+            client.stop();
+        }
+        assertNull("Session closure not signalled", clientSessionHolder.get());
+    }
+
     @Test
     public void testPublicKeyAuthNewWithFailureOnFirstIdentity() throws Exception {
         SimpleGeneratorHostKeyProvider provider = new SimpleGeneratorHostKeyProvider();