M src/main/java/eu/siacs/conversations/entities/Account.java => src/main/java/eu/siacs/conversations/entities/Account.java +3 -0
@@ 635,6 635,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
REGISTRATION_INVALID_TOKEN(true,false),
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
TLS_ERROR,
+ TLS_ERROR_DOMAIN,
INCOMPATIBLE_SERVER,
TOR_NOT_AVAILABLE,
DOWNGRADE_ATTACK,
@@ 701,6 702,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return R.string.account_status_regis_invalid_token;
case TLS_ERROR:
return R.string.account_status_tls_error;
+ case TLS_ERROR_DOMAIN:
+ return R.string.account_status_tls_error_domain;
case INCOMPATIBLE_SERVER:
return R.string.account_status_incompatible_server;
case TOR_NOT_AVAILABLE:
M src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java => src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +1 -2
@@ 124,7 124,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
final X509TrustManager trustManager;
- final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
if (interactive) {
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
} else {
@@ 133,7 132,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
try {
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
builder.sslSocketFactory(sf, trustManager);
- builder.hostnameVerifier(hostnameVerifier);
+ builder.hostnameVerifier(new StrictHostnameVerifier());
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
}
}
M src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java => src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +4 -211
@@ 48,10 48,8 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ 64,26 62,20 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
-import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
-import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.DomainHostnameVerifier;
import eu.siacs.conversations.entities.MTMDecision;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.FileBackend;
@@ 101,12 93,10 @@ import eu.siacs.conversations.ui.MemorizingActivity;
*/
public class MemorizingTrustManager {
-
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
- final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
@@ 114,7 104,6 @@ public class MemorizingTrustManager {
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
- private final static int NOTIFICATION_ID = 100509;
static String KEYSTORE_DIR = "KeyStore";
static String KEYSTORE_FILE = "KeyStore.bks";
private static int decisionId = 0;
@@ 168,20 157,6 @@ public class MemorizingTrustManager {
this.defaultTrustManager = getTrustManager(null);
}
- /**
- * Changes the path for the KeyStore file.
- * <p>
- * The actual filename relative to the app's directory will be
- * <code>app_<i>dirname</i>/<i>filename</i></code>.
- *
- * @param dirname directory to store the KeyStore.
- * @param filename file name for the KeyStore.
- */
- public static void setKeyStoreFile(String dirname, String filename) {
- KEYSTORE_DIR = dirname;
- KEYSTORE_FILE = filename;
- }
-
private static boolean isIp(final String server) {
return server != null && (
PATTERN_IPV4.matcher(server).matches()
@@ 217,9 192,7 @@ public class MemorizingTrustManager {
MessageDigest md = MessageDigest.getInstance(digest);
md.update(cert.getEncoded());
return hexString(md.digest());
- } catch (java.security.cert.CertificateEncodingException e) {
- return e.getMessage();
- } catch (java.security.NoSuchAlgorithmException e) {
+ } catch (CertificateEncodingException | NoSuchAlgorithmException e) {
return e.getMessage();
}
}
@@ 240,7 213,7 @@ public class MemorizingTrustManager {
}
}
- void init(Context m) {
+ void init(final Context m) {
master = m;
masterHandler = new Handler(m.getMainLooper());
notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
@@ 264,36 237,6 @@ public class MemorizingTrustManager {
}
/**
- * Binds an Activity to the MTM for displaying the query dialog.
- * <p>
- * This is useful if your connection is run from a service that is
- * triggered by user interaction -- in such cases the activity is
- * visible and the user tends to ignore the service notification.
- * <p>
- * You should never have a hidden activity bound to MTM! Use this
- * function in onResume() and @see unbindDisplayActivity in onPause().
- *
- * @param act Activity to be bound
- */
- public void bindDisplayActivity(AppCompatActivity act) {
- foregroundAct = act;
- }
-
- /**
- * Removes an Activity from the MTM display stack.
- * <p>
- * Always call this function when the Activity added with
- * {@link #bindDisplayActivity(AppCompatActivity)} is hidden.
- *
- * @param act Activity to be unbound
- */
- public void unbindDisplayActivity(AppCompatActivity act) {
- // do not remove if it was overridden by a different activity
- if (foregroundAct == act)
- foregroundAct = null;
- }
-
- /**
* Get a list of all certificate aliases stored in MTM.
*
* @return an {@link Enumeration} of all certificates
@@ 308,21 251,6 @@ public class MemorizingTrustManager {
}
/**
- * Get a certificate for a given alias.
- *
- * @param alias the certificate's alias as returned by {@link #getCertificates()}.
- * @return the certificate associated with the alias or <tt>null</tt> if none found.
- */
- public Certificate getCertificate(String alias) {
- try {
- return appKeyStore.getCertificate(alias);
- } catch (KeyStoreException e) {
- // this should never happen, however...
- throw new RuntimeException(e);
- }
- }
-
- /**
* Removes the given certificate from MTMs key store.
*
* <p>
@@ 340,32 268,6 @@ public class MemorizingTrustManager {
keyStoreUpdated();
}
- /**
- * Creates a new hostname verifier supporting user interaction.
- *
- * <p>This method creates a new {@link HostnameVerifier} that is bound to
- * the given instance of {@link MemorizingTrustManager}, and leverages an
- * existing {@link HostnameVerifier}. The returned verifier performs the
- * following steps, returning as soon as one of them succeeds:
- * /p>
- * <ol>
- * <li>Success, if the wrapped defaultVerifier accepts the certificate.</li>
- * <li>Success, if the server certificate is stored in the keystore under the given hostname.</li>
- * <li>Ask the user and return accordingly.</li>
- * <li>Failure on exception.</li>
- * </ol>
- *
- * @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check
- * @return a new hostname verifier using the MTM's key store
- * @throws IllegalArgumentException if the defaultVerifier parameter is null
- */
- public DomainHostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier, final boolean interactive) {
- if (defaultVerifier == null)
- throw new IllegalArgumentException("The default verifier may not be null");
-
- return new MemorizingHostnameVerifier(defaultVerifier, interactive);
- }
-
X509TrustManager getTrustManager(KeyStore ks) {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
@@ 452,16 354,8 @@ public class MemorizingTrustManager {
}
}
- private boolean isExpiredException(Throwable e) {
- do {
- if (e instanceof CertificateExpiredException)
- return true;
- e = e.getCause();
- } while (e != null);
- return false;
- }
- public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
+ private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
throws CertificateException {
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
try {
@@ 470,13 364,8 @@ public class MemorizingTrustManager {
appTrustManager.checkServerTrusted(chain, authType);
else
appTrustManager.checkClientTrusted(chain, authType);
- } catch (CertificateException ae) {
+ } catch (final CertificateException ae) {
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
- // if the cert is stored in our appTrustManager, we ignore expiredness
- if (isExpiredException(ae)) {
- LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore");
- return;
- }
if (isCertKnown(chain[0])) {
LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
return;
@@ 673,40 562,6 @@ public class MemorizingTrustManager {
return si.toString();
}
- private String hostNameMessage(X509Certificate cert, String hostname) {
- StringBuffer si = new StringBuffer();
-
- si.append(master.getString(R.string.mtm_hostname_mismatch, hostname));
- si.append("\n\n");
- try {
- Collection<List<?>> sans = cert.getSubjectAlternativeNames();
- if (sans == null) {
- si.append(cert.getSubjectDN());
- si.append("\n");
- } else for (List<?> altName : sans) {
- Object name = altName.get(1);
- if (name instanceof String) {
- si.append("[");
- si.append(altName.get(0));
- si.append("] ");
- si.append(name);
- si.append("\n");
- }
- }
- } catch (CertificateParsingException e) {
- e.printStackTrace();
- si.append("<Parsing error: ");
- si.append(e.getLocalizedMessage());
- si.append(">\n");
- }
- si.append("\n");
- si.append(master.getString(R.string.mtm_connect_anyway));
- si.append("\n\n");
- si.append(master.getString(R.string.mtm_cert_details));
- certDetails(si, cert);
- return si.toString();
- }
-
/**
* Returns the top-most entry of the activity stack.
*
@@ 764,17 619,6 @@ public class MemorizingTrustManager {
}
}
- boolean interactHostname(X509Certificate cert, String hostname) {
- switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) {
- case MTMDecision.DECISION_ALWAYS:
- storeCert(hostname, cert);
- case MTMDecision.DECISION_ONCE:
- return true;
- default:
- return false;
- }
- }
-
public X509TrustManager getNonInteractive(String domain) {
return new NonInteractiveMemorizingTrustManager(domain);
}
@@ 791,57 635,6 @@ public class MemorizingTrustManager {
return new InteractiveMemorizingTrustManager(null);
}
- class MemorizingHostnameVerifier implements DomainHostnameVerifier {
- private final HostnameVerifier defaultVerifier;
- private final boolean interactive;
-
- public MemorizingHostnameVerifier(HostnameVerifier wrapped, boolean interactive) {
- this.defaultVerifier = wrapped;
- this.interactive = interactive;
- }
-
- @Override
- public boolean verify(String domain, String hostname, SSLSession session) {
- LOGGER.log(Level.FINE, "hostname verifier for " + domain + ", trying default verifier first");
- // if the default verifier accepts the hostname, we are done
- if (defaultVerifier instanceof DomainHostnameVerifier) {
- if (((DomainHostnameVerifier) defaultVerifier).verify(domain, hostname, session)) {
- return true;
- }
- } else {
- if (defaultVerifier.verify(domain, session)) {
- return true;
- }
- }
-
-
- // otherwise, we check if the hostname is an alias for this cert in our keystore
- try {
- X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0];
- //Log.d(TAG, "cert: " + cert);
- if (cert.equals(appKeyStore.getCertificate(domain.toLowerCase(Locale.US)))) {
- LOGGER.log(Level.FINE, "certificate for " + domain + " is in our keystore. accepting.");
- return true;
- } else {
- LOGGER.log(Level.FINE, "server " + domain + " provided wrong certificate, asking user.");
- if (interactive) {
- return interactHostname(cert, domain);
- } else {
- return false;
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- return false;
- }
- }
-
- @Override
- public boolean verify(String domain, SSLSession sslSession) {
- return verify(domain, null, sslSession);
- }
- }
-
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
M src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java => src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +9 -23
@@ 428,7 428,7 @@ public class XmppConnection implements Runnable {
return tag != null && tag.isStart("stream");
}
- private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
+ private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
final SSLContext sc = SSLSocketHelper.getSSLContext();
final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
final KeyManager[] keyManager;
@@ 439,9 439,7 @@ public class XmppConnection implements Runnable {
}
final String domain = account.getServer();
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
- final SSLSocketFactory factory = sc.getSocketFactory();
- final DomainHostnameVerifier verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier(), mInteractive);
- return new TlsFactoryVerifier(factory, verifier);
+ return sc.getSocketFactory();
}
@Override
@@ 816,21 814,22 @@ public class XmppConnection implements Runnable {
}
private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
- final TlsFactoryVerifier tlsFactoryVerifier;
+ final SSLSocketFactory sslSocketFactory;
try {
- tlsFactoryVerifier = getTlsFactoryVerifier();
+ sslSocketFactory = getSSLSocketFactory();
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
throw new StateChangingException(Account.State.TLS_ERROR);
}
final InetAddress address = socket.getInetAddress();
- final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
+ final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
SSLSocketHelper.setSecurity(sslSocket);
SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer()));
SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
- if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
+ final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
+ if (!xmppDomainVerifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate domain verification failed");
FileBackend.close(sslSocket);
- throw new StateChangingException(Account.State.TLS_ERROR);
+ throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
}
return sslSocket;
}
@@ 1738,19 1737,6 @@ public class XmppConnection implements Runnable {
UNKNOWN
}
- private static class TlsFactoryVerifier {
- private final SSLSocketFactory factory;
- private final DomainHostnameVerifier verifier;
-
- TlsFactoryVerifier(final SSLSocketFactory factory, final DomainHostnameVerifier verifier) throws IOException {
- this.factory = factory;
- this.verifier = verifier;
- if (factory == null || verifier == null) {
- throw new IOException("could not setup ssl");
- }
- }
- }
-
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
M src/main/res/values/strings.xml => src/main/res/values/strings.xml +1 -0
@@ 163,6 163,7 @@
<string name="account_status_regis_not_sup">Registration not supported by server</string>
<string name="account_status_regis_invalid_token">Invalid registration token</string>
<string name="account_status_tls_error">TLS negotiation failed</string>
+ <string name="account_status_tls_error_domain">Domain not verifiable</string>
<string name="account_status_policy_violation">Policy violation</string>
<string name="account_status_incompatible_server">Incompatible server</string>
<string name="account_status_stream_error">Stream error</string>