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());