diff --git a/src/main/java/im/conversations/android/Conversations.java b/src/main/java/im/conversations/android/Conversations.java index 7356f27f5..2e3148c35 100644 --- a/src/main/java/im/conversations/android/Conversations.java +++ b/src/main/java/im/conversations/android/Conversations.java @@ -1,9 +1,16 @@ package im.conversations.android; import android.app.Application; +import android.util.Log; + import com.google.android.material.color.DynamicColors; + +import eu.siacs.conversations.Config; import im.conversations.android.xmpp.ConnectionPool; import java.security.SecureRandom; +import java.security.Security; + +import org.conscrypt.Conscrypt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,6 +23,11 @@ public class Conversations extends Application { @Override public void onCreate() { super.onCreate(); + try { + Security.insertProviderAt(Conscrypt.newProvider(), 1); + } catch (final Throwable throwable) { + LOGGER.warn("Could not initialize security provider", throwable); + } ConnectionPool.getInstance(this).reconfigure(); DynamicColors.applyToActivitiesIfAvailable(this); } diff --git a/src/main/java/im/conversations/android/tls/SSLSockets.java b/src/main/java/im/conversations/android/tls/SSLSockets.java new file mode 100644 index 000000000..bc6ecb6fe --- /dev/null +++ b/src/main/java/im/conversations/android/tls/SSLSockets.java @@ -0,0 +1,193 @@ +package im.conversations.android.tls; + +import android.os.Build; +import androidx.annotation.RequiresApi; +import com.google.common.base.Strings; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import org.conscrypt.Conscrypt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SSLSockets { + + private static final String[] ENABLED_CIPHERS = { + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA", + + // Fallback. + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA384", + "TLS_RSA_WITH_AES_256_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA384", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + }; + + private static final String[] WEAK_CIPHER_PATTERNS = { + "_NULL_", "_EXPORT_", "_anon_", "_RC4_", "_DES_", "_MD5", + }; + + private static final Logger LOGGER = LoggerFactory.getLogger(SSLSockets.class); + + public static void setSecurity(final SSLSocket sslSocket) { + final String[] supportProtocols; + final Collection supportedProtocols = + new LinkedList<>(Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = supportedProtocols.toArray(new String[0]); + + sslSocket.setEnabledProtocols(supportProtocols); + + final String[] cipherSuites = getOrderedCipherSuites(sslSocket.getSupportedCipherSuites()); + if (cipherSuites.length > 0) { + sslSocket.setEnabledCipherSuites(cipherSuites); + } + } + + public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { + final Collection cipherSuites = new LinkedHashSet<>(Arrays.asList(ENABLED_CIPHERS)); + final List platformCiphers = Arrays.asList(platformSupportedCipherSuites); + cipherSuites.retainAll(platformCiphers); + cipherSuites.addAll(platformCiphers); + filterWeakCipherSuites(cipherSuites); + cipherSuites.remove("TLS_FALLBACK_SCSV"); + return cipherSuites.toArray(new String[0]); + } + + private static void filterWeakCipherSuites(final Collection cipherSuites) { + final Iterator it = cipherSuites.iterator(); + while (it.hasNext()) { + String cipherName = it.next(); + // remove all ciphers with no or very weak encryption or no authentication + for (final String weakCipherPattern : WEAK_CIPHER_PATTERNS) { + if (cipherName.contains(weakCipherPattern)) { + it.remove(); + break; + } + } + } + } + + public static void setHostname(final SSLSocket socket, final String hostname) { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setHostname(socket, hostname); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + setHostnameNougat(socket, hostname); + } else { + setHostnameReflection(socket, hostname); + } + } + + private static void setHostnameReflection(final SSLSocket socket, final String hostname) { + try { + socket.getClass().getMethod("setHostname", String.class).invoke(socket, hostname); + } catch (final IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + LOGGER.warn("Could not set SNI hostname on socket", e); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private static void setHostnameNougat(final SSLSocket socket, final String hostname) { + final SSLParameters parameters = new SSLParameters(); + parameters.setServerNames(Collections.singletonList(new SNIHostName(hostname))); + socket.setSSLParameters(parameters); + } + + private static void setApplicationProtocolReflection( + final SSLSocket socket, final String protocol) { + try { + final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class); + // the concatenation of 8-bit, length prefixed protocol names, just one in our case... + // http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 + final byte[] protocolUTF8Bytes = protocol.getBytes(StandardCharsets.UTF_8); + final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1]; + lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow + System.arraycopy( + protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length); + method.invoke(socket, new Object[] {lengthPrefixedProtocols}); + } catch (final IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + LOGGER.warn("Could not set application protocol on socket", e); + } + } + + public static void setApplicationProtocol(final SSLSocket socket, final String protocol) { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setApplicationProtocols(socket, new String[] {protocol}); + } else { + setApplicationProtocolReflection(socket, protocol); + } + } + + public static SSLContext getSSLContext() throws NoSuchAlgorithmException { + try { + return SSLContext.getInstance("TLSv1.3"); + } catch (NoSuchAlgorithmException e) { + return SSLContext.getInstance("TLSv1.2"); + } + } + + public static Version version(final Socket socket) { + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + return Version.of(sslSocket.getSession().getProtocol()); + } else { + return Version.NONE; + } + } + + public enum Version { + TLS_1_0, + TLS_1_1, + TLS_1_2, + TLS_1_3, + UNKNOWN, + NONE; + + private static Version of(final String protocol) { + switch (Strings.nullToEmpty(protocol)) { + case "TLSv1": + return TLS_1_0; + case "TLSv1.1": + return TLS_1_1; + case "TLSv1.2": + return TLS_1_2; + case "TLSv1.3": + return TLS_1_3; + default: + return UNKNOWN; + } + } + } +} diff --git a/src/main/java/im/conversations/android/ui/model/SetupViewModel.java b/src/main/java/im/conversations/android/ui/model/SetupViewModel.java index 4ba6ac9d3..2ae78b295 100644 --- a/src/main/java/im/conversations/android/ui/model/SetupViewModel.java +++ b/src/main/java/im/conversations/android/ui/model/SetupViewModel.java @@ -6,11 +6,9 @@ import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; - import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; - import im.conversations.android.database.model.Account; import im.conversations.android.repository.AccountRepository; import im.conversations.android.ui.Event; @@ -21,8 +19,6 @@ import org.jxmpp.stringprep.XmppStringprepException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.Future; - public class SetupViewModel extends AndroidViewModel { private static final Logger LOGGER = LoggerFactory.getLogger(SetupViewModel.class); @@ -70,24 +66,28 @@ public class SetupViewModel extends AndroidViewModel { public boolean submitPassword() { final BareJid address; try { - address =JidCreate.bareFrom(this.xmppAddress.getValue()); + address = JidCreate.bareFrom(this.xmppAddress.getValue()); } catch (final XmppStringprepException e) { xmppAddressError.postValue("Not a valid jid"); return true; } final String password = this.password.getValue(); - final var creationFuture = this.accountRepository.createAccountAsync(address,password, true); - Futures.addCallback(creationFuture, new FutureCallback() { - @Override - public void onSuccess(final Account account) { - LOGGER.info("Successfully created account {}",account.address); - } + final var creationFuture = + this.accountRepository.createAccountAsync(address, password, true); + Futures.addCallback( + creationFuture, + new FutureCallback() { + @Override + public void onSuccess(final Account account) { + LOGGER.info("Successfully created account {}", account.address); + } - @Override - public void onFailure(@NonNull final Throwable t) { - LOGGER.warn("Could not create account", t); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable t) { + LOGGER.warn("Could not create account", t); + } + }, + MoreExecutors.directExecutor()); return true; } diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/src/main/java/im/conversations/android/xmpp/XmppConnection.java index eabb1c58b..597c29c2e 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -28,7 +28,6 @@ import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.xmpp.bind.Bind2; import im.conversations.android.Conversations; @@ -38,6 +37,7 @@ import im.conversations.android.database.CredentialStore; import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Connection; import im.conversations.android.database.model.Credential; +import im.conversations.android.tls.SSLSockets; import im.conversations.android.xml.Element; import im.conversations.android.xml.Namespace; import im.conversations.android.xml.Tag; diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java b/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java index 020b1e186..b03b02a47 100644 --- a/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java +++ b/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java @@ -9,7 +9,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableBiMap; import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.SSLSockets; +import im.conversations.android.tls.SSLSockets; import im.conversations.android.xml.Element; import im.conversations.android.xml.Namespace; import java.util.Arrays; diff --git a/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java b/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java index d794a1eb3..df34a40ba 100644 --- a/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java +++ b/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java @@ -1,13 +1,18 @@ package im.conversations.android.xmpp.sasl; import android.util.Base64; + import com.google.common.base.Strings; -import eu.siacs.conversations.utils.CryptoHelper; +import com.google.common.io.BaseEncoding; + +import im.conversations.android.IDs; import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Credential; + import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; + import javax.net.ssl.SSLSocket; public class DigestMd5 extends SaslMechanism { @@ -58,21 +63,21 @@ public class DigestMd5 extends SaslMechanism { + Strings.nullToEmpty(credential.password); final MessageDigest md = MessageDigest.getInstance("MD5"); final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = CryptoHelper.random(100); + final String cNonce = IDs.huge(); final byte[] a1 = concatenate( y, (":" + nonce + ":" + cNonce) .getBytes(Charset.defaultCharset())); final String a2 = "AUTHENTICATE:" + digestUri; - final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); + final String ha1 = bytesToHex(md.digest(a1)); final String ha2 = - CryptoHelper.bytesToHex( + bytesToHex( md.digest(a2.getBytes(Charset.defaultCharset()))); final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2; final String response = - CryptoHelper.bytesToHex( + bytesToHex( md.digest(kd.getBytes(Charset.defaultCharset()))); final String saslString = "username=\"" @@ -110,4 +115,8 @@ public class DigestMd5 extends SaslMechanism { } return null; } + + private static String bytesToHex(final byte[] bytes) { + return BaseEncoding.base16().lowerCase().encode(bytes); + } } diff --git a/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java b/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java index 7915874a1..9dde11840 100644 --- a/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java +++ b/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java @@ -9,9 +9,9 @@ import com.google.common.collect.Multimap; import com.google.common.hash.HashFunction; import com.google.common.primitives.Bytes; import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.SSLSockets; import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Credential; +import im.conversations.android.tls.SSLSockets; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; diff --git a/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java b/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java index 567b06891..40be22327 100644 --- a/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java +++ b/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java @@ -5,9 +5,9 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.SSLSockets; import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Credential; +import im.conversations.android.tls.SSLSockets; import im.conversations.android.xml.Element; import im.conversations.android.xml.Namespace; import java.util.Collection;