From af4bb26fa8fa488e695a72e1c41c2f8157bc7449 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 18 Nov 2025 21:13:15 +0100 Subject: [PATCH 1/4] Fail when reading private key for selected certificate fails. --- .../cyberduck/core/KeychainLoginService.java | 54 ++++++++++++------- .../core/LoginConnectionService.java | 2 +- .../java/ch/cyberduck/core/LoginService.java | 8 +-- .../ch/cyberduck/core/ssl/X509KeyManager.java | 10 ++++ .../core/KeychainLoginServiceTest.java | 4 +- 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java b/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java index 0042461e914..4079d006686 100644 --- a/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java +++ b/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java @@ -22,6 +22,7 @@ import ch.cyberduck.core.exception.LocalAccessDeniedException; import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.exception.LoginFailureException; +import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.threading.CancelCallback; import org.apache.commons.lang3.StringUtils; @@ -44,9 +45,10 @@ public KeychainLoginService(final HostPasswordStore keychain) { } @Override - public void validate(final Host bookmark, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { - log.debug("Validate login credentials for {}", bookmark); - final Credentials credentials = bookmark.getCredentials(); + public void validate(final Session session, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { + log.debug("Validate login credentials for {}", session); + final Host host = session.getHost(); + final Credentials credentials = host.getCredentials(); if(credentials.isPublicKeyAuthentication()) { if(!credentials.getIdentity().attributes().getPermission().isReadable()) { log.warn("Prompt to select identity file not readable {}", credentials.getIdentity()); @@ -54,12 +56,12 @@ public void validate(final Host bookmark, final LoginCallback prompt, final Logi } } if(options.keychain) { - log.debug("Lookup credentials in keychain for {}", bookmark); + log.debug("Lookup credentials in keychain for {}", host); if(options.password) { if(StringUtils.isBlank(credentials.getPassword())) { - final String password = keychain.findLoginPassword(bookmark); + final String password = keychain.findLoginPassword(host); if(StringUtils.isNotBlank(password)) { - log.info("Fetched password from keychain for {}", bookmark); + log.info("Fetched password from keychain for {}", host); // No need to reinsert found password to the keychain. credentials.setPassword(password).setSaved(false); } @@ -67,52 +69,64 @@ public void validate(final Host bookmark, final LoginCallback prompt, final Logi } if(options.token) { if(StringUtils.isBlank(credentials.getToken())) { - final String token = keychain.findLoginToken(bookmark); + final String token = keychain.findLoginToken(host); if(StringUtils.isNotBlank(token)) { - log.info("Fetched token from keychain for {}", bookmark); + log.info("Fetched token from keychain for {}", host); // No need to reinsert found token to the keychain. credentials.setToken(token).setSaved(false); } } } if(options.publickey) { - final String passphrase = keychain.findPrivateKeyPassphrase(bookmark); + final String passphrase = keychain.findPrivateKeyPassphrase(host); if(StringUtils.isNotBlank(passphrase)) { - log.info("Fetched private key passphrase from keychain for {}", bookmark); + log.info("Fetched private key passphrase from keychain for {}", host); // No need to reinsert found token to the keychain. credentials.setIdentityPassphrase(passphrase).setSaved(false); } } if(options.oauth) { - final OAuthTokens tokens = keychain.findOAuthTokens(bookmark); + final OAuthTokens tokens = keychain.findOAuthTokens(host); if(tokens.validate()) { - log.info("Fetched OAuth tokens {} from keychain for {}", tokens, bookmark); + log.info("Fetched OAuth tokens {} from keychain for {}", tokens, host); // No need to reinsert found token to the keychain. credentials.setOauth(tokens).setSaved(tokens.isExpired()); } } + if(options.certificate) { + final String alias = host.getCredentials().getCertificate(); + if(StringUtils.isNotBlank(alias)) { + final X509KeyManager manager = session.getFeature(X509KeyManager.class); + if(manager != null) { + if(null == manager.getPrivateKey(alias)) { + log.warn("No private key found for alias {} in keychain", alias); + throw new LoginFailureException(LocaleFactory.localizedString("Provide additional login credentials", "Credentials")); + } + } + } + } } - if(!credentials.validate(bookmark.getProtocol(), options)) { + if(!credentials.validate(host.getProtocol(), options)) { log.warn("Failed validation of credentials {} with options {}", credentials, options); - final CredentialsConfigurator configurator = CredentialsConfiguratorFactory.get(bookmark.getProtocol()); + final CredentialsConfigurator configurator = CredentialsConfiguratorFactory.get(host.getProtocol()); log.debug("Auto configure credentials with {}", configurator); - final Credentials configuration = configurator.configure(bookmark); - if(configuration.validate(bookmark.getProtocol(), options)) { - bookmark.setCredentials(configuration); - log.info("Auto configured credentials {} for {}", configuration, bookmark); + final Credentials configuration = configurator.configure(host); + if(configuration.validate(host.getProtocol(), options)) { + host.setCredentials(configuration); + log.info("Auto configured credentials {} for {}", configuration, host); return; } final StringAppender message = new StringAppender(); if(options.password) { message.append(MessageFormat.format(LocaleFactory.localizedString( - "Login {0} with username and password", "Credentials"), BookmarkNameProvider.toString(bookmark))); + "Login {0} with username and password", "Credentials"), BookmarkNameProvider.toString(host))); } if(options.publickey) { message.append(LocaleFactory.localizedString( "Select the private key in PEM or PuTTY format", "Credentials")); } message.append(LocaleFactory.localizedString("No login credentials could be found in the Keychain", "Credentials")); - this.prompt(bookmark, message.toString(), prompt, options); + this.prompt(host, message.toString(), prompt, options); } log.debug("Validated credentials {} with options {}", credentials, options); } diff --git a/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java b/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java index 9f6f973467c..2bf3b698d4f 100644 --- a/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java +++ b/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java @@ -93,7 +93,7 @@ public boolean check(final Session session, final CancelCallback callback) th } // Obtain password from keychain or prompt synchronized(login) { - login.validate(bookmark, prompt, new LoginOptions(bookmark.getProtocol())); + login.validate(session, prompt, new LoginOptions(bookmark.getProtocol())); } this.connect(session, callback); return true; diff --git a/core/src/main/java/ch/cyberduck/core/LoginService.java b/core/src/main/java/ch/cyberduck/core/LoginService.java index bc33171bb88..cb7fbcb0713 100644 --- a/core/src/main/java/ch/cyberduck/core/LoginService.java +++ b/core/src/main/java/ch/cyberduck/core/LoginService.java @@ -27,11 +27,11 @@ public interface LoginService { /** * Obtain password from password store or prompt user for input * - * @param bookmark Credentials - * @param pompt Login prompt - * @param options Login mechanism features + * @param session Credentials + * @param pompt Login prompt + * @param options Login mechanism features */ - void validate(Host bookmark, LoginCallback pompt, LoginOptions options) throws ConnectionCanceledException, LoginFailureException; + void validate(Session session, LoginCallback pompt, LoginOptions options) throws ConnectionCanceledException, LoginFailureException; /** * Login and prompt on failure diff --git a/core/src/main/java/ch/cyberduck/core/ssl/X509KeyManager.java b/core/src/main/java/ch/cyberduck/core/ssl/X509KeyManager.java index 5c3e52b9889..eb8fca88120 100644 --- a/core/src/main/java/ch/cyberduck/core/ssl/X509KeyManager.java +++ b/core/src/main/java/ch/cyberduck/core/ssl/X509KeyManager.java @@ -19,6 +19,7 @@ */ import java.security.Principal; +import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.List; @@ -40,4 +41,13 @@ public interface X509KeyManager extends javax.net.ssl.X509KeyManager { * @param issuers Acceptable CA issuer subject names or null if it does not matter which issuers are used */ X509Certificate getCertificate(String alias, String[] keyTypes, Principal[] issuers); + + /** + * Find private key for certificate to use for authentication with mutual TLS + * + * @param alias Certificate alias + * @return Null when not found + */ + @Override + PrivateKey getPrivateKey(String alias); } diff --git a/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java b/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java index e9352ae0f7c..5f241229278 100644 --- a/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java +++ b/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java @@ -51,7 +51,7 @@ else if(1 == i) { @Test(expected = LoginCanceledException.class) public void testCancel() throws Exception { LoginService l = new KeychainLoginService(new DisabledPasswordStore()); - l.validate(new Host(new TestProtocol(), "h"), new DisabledLoginCallback(), new LoginOptions()); + l.validate(new NullSession(new Host(new TestProtocol(), "h")), new DisabledLoginCallback(), new LoginOptions()); } @Test @@ -68,7 +68,7 @@ public String findLoginPassword(final Host bookmark) { final Credentials credentials = new Credentials(); credentials.setUsername("u"); final Host host = new Host(new TestProtocol(), "test.cyberduck.ch", credentials); - l.validate(host, new DisabledLoginCallback(), new LoginOptions(host.getProtocol())); + l.validate(new NullSession(host), new DisabledLoginCallback(), new LoginOptions(host.getProtocol())); assertTrue(keychain.get()); assertFalse(host.getCredentials().isSaved()); assertEquals("P", host.getCredentials().getPassword()); From 07ee29929ca9013629b84bb8ea8c5229f90c50bb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Dec 2025 17:16:40 +0100 Subject: [PATCH 2/4] Add parameter to retrieve private key for mTLS. --- .../ch/cyberduck/cli/TerminalLoginService.java | 6 +++--- .../ch/cyberduck/core/KeychainLoginService.java | 14 ++++---------- .../ch/cyberduck/core/LoginConnectionService.java | 3 ++- .../main/java/ch/cyberduck/core/LoginService.java | 7 ++++--- .../cyberduck/core/KeychainLoginServiceTest.java | 6 ++++-- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/cli/src/main/java/ch/cyberduck/cli/TerminalLoginService.java b/cli/src/main/java/ch/cyberduck/cli/TerminalLoginService.java index 1b1e0a4f0a4..2c50d1030f4 100644 --- a/cli/src/main/java/ch/cyberduck/cli/TerminalLoginService.java +++ b/cli/src/main/java/ch/cyberduck/cli/TerminalLoginService.java @@ -27,9 +27,9 @@ import ch.cyberduck.core.LoginOptions; import ch.cyberduck.core.PasswordStoreFactory; import ch.cyberduck.core.exception.ConnectionCanceledException; -import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.exception.LoginFailureException; import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.ssl.X509KeyManager; import org.apache.commons.cli.CommandLine; import org.apache.commons.lang3.StringUtils; @@ -44,7 +44,7 @@ public TerminalLoginService(final CommandLine input) { } @Override - public void validate(final Host bookmark, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { + public void validate(final Host bookmark, final X509KeyManager keys, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { final Credentials credentials = bookmark.getCredentials(); if(input.hasOption(TerminalOptionsBuilder.Params.anonymous.name())) { credentials.setUsername(PreferencesFactory.get().getProperty("connection.login.anon.name")); @@ -61,6 +61,6 @@ public void validate(final Host bookmark, final LoginCallback prompt, final Logi if(StringUtils.isNotBlank(credentials.getUsername()) && StringUtils.isNotBlank(credentials.getPassword())) { return; } - super.validate(bookmark, prompt, options); + super.validate(bookmark, keys, prompt, options); } } diff --git a/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java b/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java index 4079d006686..7988a4c026c 100644 --- a/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java +++ b/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java @@ -36,18 +36,13 @@ public class KeychainLoginService implements LoginService { private final HostPasswordStore keychain; - public KeychainLoginService() { - this(PasswordStoreFactory.get()); - } - public KeychainLoginService(final HostPasswordStore keychain) { this.keychain = keychain; } @Override - public void validate(final Session session, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { - log.debug("Validate login credentials for {}", session); - final Host host = session.getHost(); + public void validate(final Host host, final X509KeyManager keys, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { + log.debug("Validate login credentials for {}", host); final Credentials credentials = host.getCredentials(); if(credentials.isPublicKeyAuthentication()) { if(!credentials.getIdentity().attributes().getPermission().isReadable()) { @@ -96,9 +91,8 @@ public void validate(final Session session, final LoginCallback prompt, final if(options.certificate) { final String alias = host.getCredentials().getCertificate(); if(StringUtils.isNotBlank(alias)) { - final X509KeyManager manager = session.getFeature(X509KeyManager.class); - if(manager != null) { - if(null == manager.getPrivateKey(alias)) { + if(keys != null) { + if(null == keys.getPrivateKey(alias)) { log.warn("No private key found for alias {} in keychain", alias); throw new LoginFailureException(LocaleFactory.localizedString("Provide additional login credentials", "Credentials")); } diff --git a/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java b/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java index 2bf3b698d4f..fb050275129 100644 --- a/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java +++ b/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java @@ -24,6 +24,7 @@ import ch.cyberduck.core.proxy.ProxyFactory; import ch.cyberduck.core.proxy.ProxyFinder; import ch.cyberduck.core.proxy.ProxyHostUrlProvider; +import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.threading.CancelCallback; import org.apache.commons.lang3.StringUtils; @@ -93,7 +94,7 @@ public boolean check(final Session session, final CancelCallback callback) th } // Obtain password from keychain or prompt synchronized(login) { - login.validate(session, prompt, new LoginOptions(bookmark.getProtocol())); + login.validate(bookmark, session.getFeature(X509KeyManager.class), prompt, new LoginOptions(bookmark.getProtocol())); } this.connect(session, callback); return true; diff --git a/core/src/main/java/ch/cyberduck/core/LoginService.java b/core/src/main/java/ch/cyberduck/core/LoginService.java index cb7fbcb0713..e74b772b751 100644 --- a/core/src/main/java/ch/cyberduck/core/LoginService.java +++ b/core/src/main/java/ch/cyberduck/core/LoginService.java @@ -21,17 +21,18 @@ import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.exception.LoginFailureException; +import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.threading.CancelCallback; public interface LoginService { /** * Obtain password from password store or prompt user for input * - * @param session Credentials - * @param pompt Login prompt + * @param bookmark Credentials + * @param prompt Login prompt * @param options Login mechanism features */ - void validate(Session session, LoginCallback pompt, LoginOptions options) throws ConnectionCanceledException, LoginFailureException; + void validate(Host bookmark, X509KeyManager keys, LoginCallback prompt, LoginOptions options) throws ConnectionCanceledException, LoginFailureException; /** * Login and prompt on failure diff --git a/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java b/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java index 5f241229278..1393628f8d4 100644 --- a/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java +++ b/core/src/test/java/ch/cyberduck/core/KeychainLoginServiceTest.java @@ -4,6 +4,7 @@ import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.proxy.DisabledProxyFinder; +import ch.cyberduck.core.ssl.DefaultX509KeyManager; import ch.cyberduck.core.threading.CancelCallback; import org.junit.Test; @@ -51,7 +52,8 @@ else if(1 == i) { @Test(expected = LoginCanceledException.class) public void testCancel() throws Exception { LoginService l = new KeychainLoginService(new DisabledPasswordStore()); - l.validate(new NullSession(new Host(new TestProtocol(), "h")), new DisabledLoginCallback(), new LoginOptions()); + l.validate(new Host(new TestProtocol(), "h"), + new DefaultX509KeyManager(), new DisabledLoginCallback(), new LoginOptions()); } @Test @@ -68,7 +70,7 @@ public String findLoginPassword(final Host bookmark) { final Credentials credentials = new Credentials(); credentials.setUsername("u"); final Host host = new Host(new TestProtocol(), "test.cyberduck.ch", credentials); - l.validate(new NullSession(host), new DisabledLoginCallback(), new LoginOptions(host.getProtocol())); + l.validate(host, new DefaultX509KeyManager(), new DisabledLoginCallback(), new LoginOptions(host.getProtocol())); assertTrue(keychain.get()); assertFalse(host.getCredentials().isSaved()); assertEquals("P", host.getCredentials().getPassword()); From 6a5de82d2533f222ca2f541d91ae71b1361ff111 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Dec 2025 17:29:47 +0100 Subject: [PATCH 3/4] Validate jumphost configuration on connect. --- .../src/main/java/ch/cyberduck/core/Host.java | 12 +++++++ .../cyberduck/core/KeychainLoginService.java | 9 +++++- .../core/LoginConnectionService.java | 31 +++++++++++-------- .../ch/cyberduck/core/sftp/SFTPSession.java | 9 +----- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/Host.java b/core/src/main/java/ch/cyberduck/core/Host.java index 09d3d431181..24d10920d41 100644 --- a/core/src/main/java/ch/cyberduck/core/Host.java +++ b/core/src/main/java/ch/cyberduck/core/Host.java @@ -38,6 +38,10 @@ public class Host implements Serializable, Comparable, PreferencesReader { * The credentials to authenticate with for the CDN */ private final Credentials cloudfront = new Credentials(); + /** + * Proxy configuration + */ + private Host jumphost; /** * The protocol identifier. */ @@ -357,6 +361,14 @@ public Host withCredentials(final Credentials credentials) { return this; } + public Host getJumphost() { + return jumphost; + } + + public void setJumphost(final Host jumphost) { + this.jumphost = jumphost; + } + /** * @return Credentials to modify CDN configuration */ diff --git a/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java b/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java index 7988a4c026c..e9b0891d7b8 100644 --- a/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java +++ b/core/src/main/java/ch/cyberduck/core/KeychainLoginService.java @@ -43,6 +43,10 @@ public KeychainLoginService(final HostPasswordStore keychain) { @Override public void validate(final Host host, final X509KeyManager keys, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException { log.debug("Validate login credentials for {}", host); + final Host jumphost = host.getJumphost(); + if(null != jumphost) { + this.validate(jumphost, keys, prompt, new LoginOptions(jumphost.getProtocol())); + } final Credentials credentials = host.getCredentials(); if(credentials.isPublicKeyAuthentication()) { if(!credentials.getIdentity().attributes().getPermission().isReadable()) { @@ -213,9 +217,12 @@ public boolean authenticate(final Session session, final ProgressListener lis public void save(final Host bookmark) { final Credentials credentials = bookmark.getCredentials(); if(credentials.isSaved()) { - // Write credentials to keychain + // Write credentials to the password store try { keychain.save(bookmark); + if(bookmark.getJumphost() != null) { + keychain.save(bookmark.getJumphost()); + } } catch(LocalAccessDeniedException e) { log.error("Failure saving credentials for {} in keychain. {}", bookmark, e); diff --git a/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java b/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java index fb050275129..b710595995d 100644 --- a/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java +++ b/core/src/main/java/ch/cyberduck/core/LoginConnectionService.java @@ -89,10 +89,15 @@ public boolean check(final Session session, final CancelCallback callback) th } if(session.isConnected()) { log.debug("Skip opening connection for session {}", session); - // Connection already open + // Connection is already open return false; } - // Obtain password from keychain or prompt + final Host jumphost = JumpHostConfiguratorFactory.get(bookmark.getProtocol()).getJumphost(bookmark.getHostname()); + if(null != jumphost) { + log.debug("Configure with jump host {}", jumphost); + bookmark.setJumphost(jumphost); + } + // Get password from the password store or prompt synchronized(login) { login.validate(bookmark, session.getFeature(X509KeyManager.class), prompt, new LoginOptions(bookmark.getProtocol())); } @@ -103,7 +108,7 @@ public boolean check(final Session session, final CancelCallback callback) th @Override public void close(final Session session) throws BackgroundException { listener.message(MessageFormat.format(LocaleFactory.localizedString("Disconnecting {0}", "Status"), - session.getHost().getHostname())); + session.getHost().getHostname())); // Close the underlying socket first session.interrupt(); } @@ -131,24 +136,24 @@ public void connect(final Session session, final CancelCallback cancel) throw } } listener.message(MessageFormat.format(LocaleFactory.localizedString("Opening {0} connection to {1}", "Status"), - bookmark.getProtocol().getName(), hostname)); + bookmark.getProtocol().getName(), hostname)); // The IP address could successfully be determined session.open(proxy, key, prompt, cancel); listener.message(MessageFormat.format(LocaleFactory.localizedString("{0} connection opened", "Status"), - bookmark.getProtocol().getName())); + bookmark.getProtocol().getName())); // Update last accessed timestamp bookmark.setTimestamp(new Date()); // Warning about insecure connection prior authenticating if(session.alert(prompt)) { // Warning if credentials are sent plaintext. prompt.warn(bookmark, MessageFormat.format(LocaleFactory.localizedString("Unsecured {0} connection", "Credentials"), - bookmark.getProtocol().getName()), - MessageFormat.format("{0} {1}.", MessageFormat.format(LocaleFactory.localizedString("{0} will be sent in plaintext.", "Credentials"), - bookmark.getProtocol().getPasswordPlaceholder()), - LocaleFactory.localizedString("Please contact your web hosting service provider for assistance", "Support")), - LocaleFactory.localizedString("Continue", "Credentials"), - LocaleFactory.localizedString("Disconnect", "Credentials"), - String.format("connection.unsecure.%s", bookmark.getHostname())); + bookmark.getProtocol().getName()), + MessageFormat.format("{0} {1}.", MessageFormat.format(LocaleFactory.localizedString("{0} will be sent in plaintext.", "Credentials"), + bookmark.getProtocol().getPasswordPlaceholder()), + LocaleFactory.localizedString("Please contact your web hosting service provider for assistance", "Support")), + LocaleFactory.localizedString("Continue", "Credentials"), + LocaleFactory.localizedString("Disconnect", "Credentials"), + String.format("connection.unsecure.%s", bookmark.getHostname())); } // Login try { @@ -163,7 +168,7 @@ public void connect(final Session session, final CancelCallback cancel) throw private void authenticate(final Session session, final CancelCallback callback) throws BackgroundException { if(!login.authenticate(session, listener, prompt, callback)) { if(session.isConnected()) { - // Next attempt with updated credentials but cancel when prompt is dismissed + // Next attempt with updated credentials but cancel when the prompt is dismissed this.authenticate(session, callback); } else { diff --git a/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPSession.java b/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPSession.java index 4258ee988ac..b38f431c4bb 100644 --- a/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPSession.java +++ b/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPSession.java @@ -39,10 +39,8 @@ import ch.cyberduck.core.sftp.compression.JcraftDelayedZlibCompression; import ch.cyberduck.core.sftp.compression.JcraftZlibCompression; import ch.cyberduck.core.sftp.openssh.OpenSSHAgentAuthenticator; -import ch.cyberduck.core.sftp.openssh.OpenSSHCredentialsConfigurator; import ch.cyberduck.core.sftp.openssh.OpenSSHHostnameConfigurator; import ch.cyberduck.core.sftp.openssh.OpenSSHIdentityAgentConfigurator; -import ch.cyberduck.core.sftp.openssh.OpenSSHJumpHostConfigurator; import ch.cyberduck.core.sftp.openssh.OpenSSHPreferredAuthenticationsConfigurator; import ch.cyberduck.core.sftp.openssh.WindowsOpenSSHAgentAuthenticator; import ch.cyberduck.core.sftp.putty.PageantAuthenticator; @@ -144,19 +142,14 @@ protected SSHClient connect(final HostKeyCallback key, final LoginCallback promp final SSHClient connection = this.toClient(key, configuration); try { // Look for jump host configuration - final Host proxy = new OpenSSHJumpHostConfigurator().getJumphost(host.getHostname()); + final Host proxy = host.getJumphost(); if(null != proxy) { log.info("Connect using jump host configuration {}", proxy); final SSHClient hop = this.toClient(key, configuration); hop.connect(proxy.getHostname(), proxy.getPort()); - proxy.setCredentials(new OpenSSHCredentialsConfigurator().configure(proxy)); - final KeychainLoginService service = new KeychainLoginService(); - service.validate(proxy, prompt, new LoginOptions(proxy.getProtocol())); // Authenticate with jump host this.authenticate(hop, proxy, prompt, new DisabledCancelCallback()); log.debug("Authenticated with jump host {}", proxy); - // Write credentials to keychain - service.save(proxy); final DirectConnection tunnel = hop.newDirectConnection( new OpenSSHHostnameConfigurator().getHostname(host.getHostname()), host.getPort()); // Connect to internal host From 7933f9973ae532c6c8d95c9ea09305ba3cfbe8fb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Dec 2025 17:44:11 +0100 Subject: [PATCH 4/4] Javadoc. --- core/src/main/java/ch/cyberduck/core/Host.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/Host.java b/core/src/main/java/ch/cyberduck/core/Host.java index 24d10920d41..3cf9dc4b97e 100644 --- a/core/src/main/java/ch/cyberduck/core/Host.java +++ b/core/src/main/java/ch/cyberduck/core/Host.java @@ -39,7 +39,7 @@ public class Host implements Serializable, Comparable, PreferencesReader { */ private final Credentials cloudfront = new Credentials(); /** - * Proxy configuration + * Jump host configuration for SSH bastion host connections */ private Host jumphost; /** @@ -361,10 +361,16 @@ public Host withCredentials(final Credentials credentials) { return this; } + /** + * @return Jump host configuration for SSH bastion host connections + */ public Host getJumphost() { return jumphost; } + /** + * @param jumphost Jump host configuration for SSH bastion host connections + */ public void setJumphost(final Host jumphost) { this.jumphost = jumphost; }