From 66967d9e6db8ae75ae0f84bf9e89e7ce4718face Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Thu, 23 Apr 2020 13:51:42 +0200
Subject: [PATCH] Implement TLSv1.3 support

---
 .../ui/info/core/CoreInfoFragment.kt          | 20 ++++++--
 app/src/main/res/values-de/strings_info.xml   |  1 +
 app/src/main/res/values-it/strings_info.xml   |  1 +
 app/src/main/res/values-lt/strings_info.xml   |  1 +
 app/src/main/res/values-pt/strings_info.xml   |  1 +
 app/src/main/res/values/strings_info.xml      |  1 +
 .../libquassel/util/nio/WrappedChannel.kt     | 22 +++++++-
 .../libquassel/util/nio/WrappedChannelTest.kt | 50 +++++++++++++++++++
 8 files changed, 92 insertions(+), 5 deletions(-)
 create mode 100644 lib/src/test/java/de/kuschku/libquassel/util/nio/WrappedChannelTest.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt
index ce5dc9f95..699928862 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt
@@ -101,6 +101,7 @@ class CoreInfoFragment : ServiceBoundFragment() {
   private val movementMethod = BetterLinkMovementMethod.newInstance()
 
   private val cipherSuiteRegex = Regex("TLS_(.*)_WITH_(.*)")
+  private val cipherSuiteRegex13 = Regex("TLS_()(.*)")
 
   init {
     movementMethod.setOnLinkLongClickListener(LinkLongClickMenuHelper())
@@ -188,19 +189,30 @@ class CoreInfoFragment : ServiceBoundFragment() {
         secureDetails.visibility = View.GONE
       }
 
+      val protocol = it.orNull()?.protocol
+      val cipherSuiteRegex =
+        if (it.orNull()?.protocol == "TLSv1.3") cipherSuiteRegex13
+        else cipherSuiteRegex
+
       val (keyExchangeMechanism, cipherSuite) = it.orNull()?.cipherSuite?.let { cipherSuite ->
         cipherSuiteRegex.matchEntire(cipherSuite)?.destructured
       }?.let { (keyExchangeMechanism, cipherSuite) ->
         Pair(keyExchangeMechanism, cipherSuite)
       } ?: Pair(null, null)
 
-      val protocol = it.orNull()?.protocol
+
       if (cipherSuite != null && keyExchangeMechanism != null && protocol != null) {
+        // TLSv1.3 has no key exchange mechanism in the ciphersuite
+        if (keyExchangeMechanism.isEmpty()) {
+          secureConnectionCiphersuite.text = context?.getString(R.string.label_core_connection_ciphersuite_13,
+                                                                cipherSuite)
+        } else {
+          secureConnectionCiphersuite.text = context?.getString(R.string.label_core_connection_ciphersuite,
+                                                                cipherSuite,
+                                                                keyExchangeMechanism)
+        }
         secureConnectionProtocol.text = context?.getString(R.string.label_core_connection_protocol,
                                                            protocol)
-        secureConnectionCiphersuite.text = context?.getString(R.string.label_core_connection_ciphersuite,
-                                                              cipherSuite,
-                                                              keyExchangeMechanism)
         secureConnectionProtocol.visibility = View.VISIBLE
         secureConnectionCiphersuite.visibility = View.VISIBLE
       } else {
diff --git a/app/src/main/res/values-de/strings_info.xml b/app/src/main/res/values-de/strings_info.xml
index 6483be3a0..090b79442 100644
--- a/app/src/main/res/values-de/strings_info.xml
+++ b/app/src/main/res/values-de/strings_info.xml
@@ -36,6 +36,7 @@
   <string name="label_core_connection_verified_by">Verbindung verifiziert von %1$s</string>
   <string name="label_core_connection_protocol">Die Verbindung verwendet %1$s</string>
   <string name="label_core_connection_ciphersuite">Die Verbindung ist mit %1$s verschlüsselt und authentifiziert und verwendet %2$s als Mechanismus für den Schlüsselaustausch</string>
+  <string name="label_core_connection_ciphersuite_13">Die Verbindung ist mit %1$s verschlüsselt und authentifiziert</string>
   <string name="label_core_connection_verified_by_unknown">Unbekannt</string>
   <string name="label_core_connection_insecure">Unsichere Verbindung</string>
 
diff --git a/app/src/main/res/values-it/strings_info.xml b/app/src/main/res/values-it/strings_info.xml
index fb262774b..bf26b3df8 100644
--- a/app/src/main/res/values-it/strings_info.xml
+++ b/app/src/main/res/values-it/strings_info.xml
@@ -36,6 +36,7 @@
   <string name="label_core_connection_verified_by">Connessione verificata da %1$s</string>
   <string name="label_core_connection_protocol">La connessione utilizza %1$s</string>
   <string name="label_core_connection_ciphersuite">La connessione è crittografata ed autenticata con %1$s ed utilizza %2$s come meccanismo di scambio delle chiavi</string>
+  <string name="label_core_connection_ciphersuite_13">La connessione è crittografata ed autenticata con %1$s</string>
   <string name="label_core_connection_verified_by_unknown">Sconosciuto</string>
   <string name="label_core_connection_insecure">Connessione insicura</string>
 
diff --git a/app/src/main/res/values-lt/strings_info.xml b/app/src/main/res/values-lt/strings_info.xml
index acc6d5301..54ae9fa0d 100644
--- a/app/src/main/res/values-lt/strings_info.xml
+++ b/app/src/main/res/values-lt/strings_info.xml
@@ -36,6 +36,7 @@
   <string name="label_core_connection_verified_by">Ryšys patvirtintas %1$s</string>
   <string name="label_core_connection_protocol">Šis ryšys naudoja %1$s</string>
   <string name="label_core_connection_ciphersuite">Šis ryšys yra užkoduotas ir autentifikuotas naudojant %1$s, ir naudoja %2$s raktų apsikeitimui</string>
+  <string name="label_core_connection_ciphersuite_13">Šis ryšys yra užkoduotas ir autentifikuotas naudojant %1$s</string>
   <string name="label_core_connection_verified_by_unknown">Nežinoma</string>
   <string name="label_core_connection_insecure">Nesaugus ryšys</string>
 
diff --git a/app/src/main/res/values-pt/strings_info.xml b/app/src/main/res/values-pt/strings_info.xml
index 5308e8887..1e377ba15 100644
--- a/app/src/main/res/values-pt/strings_info.xml
+++ b/app/src/main/res/values-pt/strings_info.xml
@@ -36,6 +36,7 @@
   <string name="label_core_connection_verified_by">Ligação verificada por %1$s</string>
   <string name="label_core_connection_protocol">A ligação utiliza %1$s</string>
   <string name="label_core_connection_ciphersuite">A ligação é encriptada e autenticada utilizando %1$s e usa  %2$s como mecanismo de troca de chaves</string>
+  <string name="label_core_connection_ciphersuite_13">A ligação é encriptada e autenticada utilizando %1$s</string>
   <string name="label_core_connection_verified_by_unknown">Desconhecido</string>
   <string name="label_core_connection_insecure">Ligação insegura</string>
 
diff --git a/app/src/main/res/values/strings_info.xml b/app/src/main/res/values/strings_info.xml
index 089e3f32d..827ec1560 100644
--- a/app/src/main/res/values/strings_info.xml
+++ b/app/src/main/res/values/strings_info.xml
@@ -36,6 +36,7 @@
   <string name="label_core_connection_verified_by">Connection verified by %1$s</string>
   <string name="label_core_connection_protocol">The connection uses %1$s</string>
   <string name="label_core_connection_ciphersuite">The connection is encrypted and authenticated using %1$s and uses %2$s as the key exchange mechanism</string>
+  <string name="label_core_connection_ciphersuite_13">The connection is encrypted and authenticated using %1$s</string>
   <string name="label_core_connection_verified_by_unknown">Unknown</string>
   <string name="label_core_connection_insecure">Insecure Connection</string>
 
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt b/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt
index ce101ab93..878ea7fc8 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt
@@ -22,6 +22,8 @@ package de.kuschku.libquassel.util.nio
 import de.kuschku.libquassel.connection.HostnameVerifier
 import de.kuschku.libquassel.connection.SocketAddress
 import de.kuschku.libquassel.util.compatibility.CompatibilityUtils
+import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
+import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel
 import de.kuschku.libquassel.util.compatibility.StreamChannelFactory
 import java.io.*
 import java.net.Socket
@@ -32,6 +34,7 @@ import java.nio.channels.InterruptibleChannel
 import java.nio.channels.ReadableByteChannel
 import java.nio.channels.WritableByteChannel
 import java.security.GeneralSecurityException
+import java.security.NoSuchAlgorithmException
 import java.security.cert.X509Certificate
 import java.util.zip.InflaterInputStream
 import javax.net.ssl.SSLContext
@@ -60,6 +63,8 @@ class WrappedChannel private constructor(
   }
 
   companion object {
+    const val DEFAULT_TLS_VERSION = "TLSv1.2"
+
     fun ofSocket(s: Socket, closeListeners: List<Closeable> = emptyList()): WrappedChannel {
       return WrappedChannel(
         s,
@@ -68,6 +73,12 @@ class WrappedChannel private constructor(
         closeListeners = closeListeners + s.getInputStream() + s.getOutputStream()
       )
     }
+
+    fun selectBestTlsVersion(availableVersions: Array<String>): String? {
+      return availableVersions.filter {
+        it.startsWith("TLSv") && it >= "TLSv1.2"
+      }.sorted().lastOrNull()
+    }
   }
 
   fun withCompression(): WrappedChannel {
@@ -82,7 +93,16 @@ class WrappedChannel private constructor(
   @Throws(GeneralSecurityException::class, IOException::class)
   fun withSSL(certificateManager: X509TrustManager, hostnameVerifier: HostnameVerifier,
               address: SocketAddress): WrappedChannel {
-    val context = SSLContext.getInstance("TLSv1.2")
+    val tlsVersion = try {
+      selectBestTlsVersion(SSLContext.getDefault().defaultSSLParameters.protocols)
+    } catch (e: NoSuchAlgorithmException) {
+      null
+    }
+    log(LogLevel.DEBUG,
+        "WrappedChannel",
+        "TLS Version chosen is: $tlsVersion, with fallback $DEFAULT_TLS_VERSION")
+
+    val context = SSLContext.getInstance(tlsVersion ?: DEFAULT_TLS_VERSION)
     val managers = arrayOf(certificateManager)
     context.init(null, managers, null)
     val factory = context.socketFactory
diff --git a/lib/src/test/java/de/kuschku/libquassel/util/nio/WrappedChannelTest.kt b/lib/src/test/java/de/kuschku/libquassel/util/nio/WrappedChannelTest.kt
new file mode 100644
index 000000000..ff5bbc085
--- /dev/null
+++ b/lib/src/test/java/de/kuschku/libquassel/util/nio/WrappedChannelTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2020 Janne Mareike Koschinski
+ * Copyright (c) 2020 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.util.nio
+
+import de.kuschku.libquassel.util.nio.WrappedChannel.Companion.selectBestTlsVersion
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class WrappedChannelTest {
+  @Test
+  fun doesNotSelectOutdatedTlsVersions() {
+    assertEquals(null, selectBestTlsVersion(arrayOf(
+      "SSLv3", "TLSv1", "TLSv1.0", "TLSv1.1"
+    )))
+  }
+
+  @Test
+  fun rejectsNonTlsProtocols() {
+    assertEquals(null, selectBestTlsVersion(arrayOf(
+      "SSLv3", "UberSecurityProtocol5"
+    )))
+  }
+
+  @Test
+  fun selectsLatestTlsVersion() {
+    assertEquals("TLSv1.2", selectBestTlsVersion(arrayOf(
+      "SSLv3", "TLSv1", "TLSv1.0", "TLSv1.1", "TLSv1.2", "UberSecurityProtocol5"
+    )))
+    assertEquals("TLSv1.3", selectBestTlsVersion(arrayOf(
+      "SSLv3", "TLSv1", "TLSv1.0", "TLSv1.1", "TLSv1.2", "TLSv1.3", "UberSecurityProtocol5"
+    )))
+  }
+}
-- 
GitLab