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/Host.java b/core/src/main/java/ch/cyberduck/core/Host.java index 09d3d431181..3cf9dc4b97e 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(); + /** + * Jump host configuration for SSH bastion host connections + */ + private Host jumphost; /** * The protocol identifier. */ @@ -357,6 +361,20 @@ 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; + } + /** * @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 0042461e914..e9b0891d7b8 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; @@ -35,18 +36,18 @@ 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 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 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()) { log.warn("Prompt to select identity file not readable {}", credentials.getIdentity()); @@ -54,12 +55,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 +68,63 @@ 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)) { + 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")); + } + } + } + } } - 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); } @@ -205,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 9f6f973467c..b710595995d 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; @@ -88,12 +89,17 @@ 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, prompt, new LoginOptions(bookmark.getProtocol())); + login.validate(bookmark, session.getFeature(X509KeyManager.class), prompt, new LoginOptions(bookmark.getProtocol())); } this.connect(session, callback); return true; @@ -102,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(); } @@ -130,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 { @@ -162,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/core/src/main/java/ch/cyberduck/core/LoginService.java b/core/src/main/java/ch/cyberduck/core/LoginService.java index bc33171bb88..e74b772b751 100644 --- a/core/src/main/java/ch/cyberduck/core/LoginService.java +++ b/core/src/main/java/ch/cyberduck/core/LoginService.java @@ -21,6 +21,7 @@ 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 { @@ -28,10 +29,10 @@ 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 prompt Login prompt + * @param options Login mechanism features */ - void validate(Host bookmark, 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/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..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 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(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()); 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