diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/AsyncBackend.kt b/app/src/main/java/de/kuschku/quasseldroid/service/AsyncBackend.kt index c14d452887cf91785a089a1af13919c26c18d46d..cfc6ccc0dd6ae8e6b0c0f252e83fa4dbc07e07fb 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/service/AsyncBackend.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/service/AsyncBackend.kt @@ -1,7 +1,7 @@ package de.kuschku.quasseldroid.service +import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.session.Backend -import de.kuschku.libquassel.session.SocketAddress import de.kuschku.libquassel.util.compatibility.HandlerService class AsyncBackend( diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt index 89e74d0d4b4b1adc4b939cac12db79444dcc84f8..9908f53660d59eab57cecdd238b18d2fec0c887e 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt @@ -1,15 +1,19 @@ package de.kuschku.quasseldroid.service -import android.annotation.SuppressLint import android.arch.lifecycle.Observer import android.content.* import android.net.ConnectivityManager +import de.kuschku.libquassel.connection.ConnectionState +import de.kuschku.libquassel.connection.HostnameVerifier +import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.protocol.ClientData import de.kuschku.libquassel.protocol.Protocol import de.kuschku.libquassel.protocol.Protocol_Feature import de.kuschku.libquassel.protocol.Protocol_Features import de.kuschku.libquassel.quassel.QuasselFeatures -import de.kuschku.libquassel.session.* +import de.kuschku.libquassel.session.Backend +import de.kuschku.libquassel.session.ISession +import de.kuschku.libquassel.session.SessionManager import de.kuschku.malheur.CrashHandler import de.kuschku.quasseldroid.BuildConfig import de.kuschku.quasseldroid.Keys @@ -19,6 +23,10 @@ import de.kuschku.quasseldroid.persistence.QuasselBacklogStorage import de.kuschku.quasseldroid.persistence.QuasselDatabase import de.kuschku.quasseldroid.settings.ConnectionSettings import de.kuschku.quasseldroid.settings.Settings +import de.kuschku.quasseldroid.ssl.QuasselHostnameVerifier +import de.kuschku.quasseldroid.ssl.QuasselTrustManager +import de.kuschku.quasseldroid.ssl.custom.QuasselCertificateManager +import de.kuschku.quasseldroid.ssl.custom.QuasselHostnameManager import de.kuschku.quasseldroid.util.QuasseldroidNotificationManager import de.kuschku.quasseldroid.util.backport.DaggerLifecycleService import de.kuschku.quasseldroid.util.compatibility.AndroidHandlerService @@ -28,7 +36,6 @@ import de.kuschku.quasseldroid.util.helper.sharedPreferences import de.kuschku.quasseldroid.util.helper.toLiveData import io.reactivex.subjects.PublishSubject import org.threeten.bp.Instant -import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.net.ssl.X509TrustManager @@ -158,15 +165,11 @@ class QuasselService : DaggerLifecycleService(), private lateinit var clientData: ClientData - private val trustManager = object : X509TrustManager { - @SuppressLint("TrustAllX509TrustManager") - override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) = Unit + private lateinit var trustManager: X509TrustManager - @SuppressLint("TrustAllX509TrustManager") - override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) = Unit + private lateinit var hostnameVerifier: HostnameVerifier - override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray() - } + private lateinit var certificateManager: QuasselCertificateManager private val backendImplementation = object : Backend { override fun updateUserDataAndLogin(user: String, pass: String) { @@ -188,7 +191,7 @@ class QuasselService : DaggerLifecycleService(), override fun connect(address: SocketAddress, user: String, pass: String, reconnect: Boolean) { disconnect() sessionManager.connect( - clientData, trustManager, address, user to pass, reconnect + clientData, trustManager, hostnameVerifier, address, user to pass, reconnect ) } @@ -228,6 +231,11 @@ class QuasselService : DaggerLifecycleService(), override fun onCreate() { super.onCreate() + + certificateManager = QuasselCertificateManager(database.validityWhitelist()) + hostnameVerifier = QuasselHostnameVerifier(QuasselHostnameManager(database.hostnameWhitelist())) + trustManager = QuasselTrustManager(certificateManager) + sessionManager = SessionManager( ISession.NULL, QuasselBacklogStorage(database), diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/BrowserCompatibleHostnameVerifier.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/BrowserCompatibleHostnameVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..baf8bb6fb8174a2dac8b9a3f9bae4cea2023abea --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/BrowserCompatibleHostnameVerifier.kt @@ -0,0 +1,45 @@ +package de.kuschku.quasseldroid.ssl + +import de.kuschku.libquassel.connection.HostnameVerifier +import de.kuschku.libquassel.connection.SocketAddress +import java.net.IDN +import java.security.cert.X509Certificate +import javax.net.ssl.SSLException + +class BrowserCompatibleHostnameVerifier : HostnameVerifier { + override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) { + val leafCertificate = chain.firstOrNull() ?: throw SSLException("No Certificate found") + val hostnames = hostnames(leafCertificate).toList() + if (hostnames.none { matches(it, address.host) }) + throw SSLException("Hostname does not match") + } + + private fun matches(name: String, host: String): Boolean { + val normalizedName = IDN.toASCII(name).trimEnd('.') + val normalizedHost = IDN.toASCII(host).trimEnd('.') + return normalizedName.equals(normalizedHost, ignoreCase = true) + } + + private fun hostnames(certificate: X509Certificate): Sequence<String> = + (sequenceOf(commonName(certificate)) + subjectAlternativeNames(certificate)) + .filterNotNull() + .distinct() + + private val COMMON_NAME = Regex("""(?:^|,\s?)(?:CN=("(?:[^"]|"")+"|[^,]+))""") + private fun commonName(certificate: X509Certificate): String? { + return COMMON_NAME.find(certificate.subjectX500Principal.name)?.groups?.get(1)?.value + } + + private fun subjectAlternativeNames(certificate: X509Certificate): Sequence<String> = + certificate.subjectAlternativeNames.orEmpty().asSequence().mapNotNull { + val type = it[0] as? Int + val name = it[1] as? String + if (type != null && name != null) Pair(type, name) + else null + }.filter { (type, _) -> + // 2 is DNS Name + type == 2 + }.map { (_, name) -> + name + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..e22d23dac188b295bd8b36ba27dacd64ebd27414 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt @@ -0,0 +1,23 @@ +package de.kuschku.quasseldroid.ssl + +import de.kuschku.libquassel.connection.HostnameVerifier +import de.kuschku.libquassel.connection.QuasselSecurityException +import de.kuschku.libquassel.connection.SocketAddress +import de.kuschku.quasseldroid.ssl.custom.QuasselHostnameManager +import java.security.cert.X509Certificate +import javax.net.ssl.SSLException + +class QuasselHostnameVerifier( + private val hostnameManager: QuasselHostnameManager, + private val hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier() +) : HostnameVerifier { + override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) { + try { + if (!hostnameManager.isValid(address, chain)) { + hostnameVerifier.checkValid(address, chain) + } + } catch (e: SSLException) { + throw QuasselSecurityException.Hostname(chain, address, e) + } + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselTrustManager.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselTrustManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d5dbeee9dd6e2d5f9ca802625ea01da29e2b8cf --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselTrustManager.kt @@ -0,0 +1,50 @@ +package de.kuschku.quasseldroid.ssl + +import de.kuschku.libquassel.connection.QuasselSecurityException +import de.kuschku.quasseldroid.ssl.custom.QuasselCertificateManager +import java.security.GeneralSecurityException +import java.security.KeyStore +import java.security.cert.X509Certificate +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +class QuasselTrustManager private constructor( + private val certificateManager: QuasselCertificateManager, + private val trustManager: X509TrustManager? +) : X509TrustManager { + constructor( + certificateManager: QuasselCertificateManager, + factory: TrustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { + init(null as KeyStore?) + } + ) : this( + certificateManager, + factory.trustManagers.mapNotNull { + it as? X509TrustManager + }.firstOrNull() + ) + + override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { + try { + trustManager?.checkClientTrusted(chain, authType) + ?: throw GeneralSecurityException("No TrustManager available") + } catch (e: GeneralSecurityException) { + throw QuasselSecurityException.Certificate(chain, e) + } + } + + override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { + try { + if (!certificateManager.isServerTrusted(chain)) { + trustManager?.checkServerTrusted(chain, authType) + ?: throw GeneralSecurityException("No TrustManager available") + } + } catch (e: GeneralSecurityException) { + throw QuasselSecurityException.Certificate(chain, e) + } + } + + override fun getAcceptedIssuers(): Array<X509Certificate> = + trustManager?.acceptedIssuers ?: emptyArray() +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselCertificateManager.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselCertificateManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..bdb899c072b6ec02ebecb46c96d780ad445bd099 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselCertificateManager.kt @@ -0,0 +1,24 @@ +package de.kuschku.quasseldroid.ssl.custom + +import de.kuschku.quasseldroid.persistence.QuasselDatabase +import de.kuschku.quasseldroid.util.helper.fingerprint +import de.kuschku.quasseldroid.util.helper.isValid +import java.security.cert.X509Certificate + +class QuasselCertificateManager( + private val validityWhitelist: QuasselDatabase.SslValidityWhitelistDao +) { + fun isServerTrusted(chain: Array<out X509Certificate>?): Boolean { + // Verify input conditions + // If no certificate exists, this can’t be valid + val leafCertificate = chain?.lastOrNull() ?: return false + return isServerTrusted(leafCertificate) + } + + private fun isServerTrusted(leafCertificate: X509Certificate): Boolean { + // Verify if a whitelist entry exists + return validityWhitelist.find(leafCertificate.fingerprint)?.let { + it.ignoreDate || leafCertificate.isValid + } ?: false + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..88320e9719794b975837e14b2d943f2695a9f484 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt @@ -0,0 +1,17 @@ +package de.kuschku.quasseldroid.ssl.custom + +import de.kuschku.libquassel.connection.SocketAddress +import de.kuschku.quasseldroid.persistence.QuasselDatabase +import de.kuschku.quasseldroid.util.helper.fingerprint +import java.security.cert.X509Certificate + +class QuasselHostnameManager( + private val hostnameWhitelist: QuasselDatabase.SslHostnameWhitelistDao +) { + fun isValid(address: SocketAddress, chain: Array<out X509Certificate>): Boolean { + val leafCertificate = chain.firstOrNull() ?: return false + val whitelistEntry = hostnameWhitelist.find(leafCertificate.fingerprint, address.host) + val all = hostnameWhitelist.all() + return whitelistEntry != null + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt index 0af0dc4077f2661af288ebf3fab4d2dfcce78f86..82b9bff21f47cf751916f9898444d540417d2eff 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt @@ -21,11 +21,13 @@ import butterknife.BindView import butterknife.ButterKnife import com.afollestad.materialdialogs.MaterialDialog import com.sothree.slidinguppanel.SlidingUpPanelLayout +import de.kuschku.libquassel.connection.ConnectionState +import de.kuschku.libquassel.connection.QuasselSecurityException import de.kuschku.libquassel.protocol.Buffer_Type import de.kuschku.libquassel.protocol.Message import de.kuschku.libquassel.protocol.Message_Type import de.kuschku.libquassel.protocol.message.HandshakeMessage -import de.kuschku.libquassel.session.ConnectionState +import de.kuschku.libquassel.session.Error import de.kuschku.libquassel.util.flag.and import de.kuschku.libquassel.util.flag.hasFlag import de.kuschku.libquassel.util.flag.or @@ -39,14 +41,17 @@ import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteAdapter import de.kuschku.quasseldroid.ui.chat.input.ChatlineFragment import de.kuschku.quasseldroid.ui.clientsettings.app.AppSettingsActivity import de.kuschku.quasseldroid.ui.coresettings.CoreSettingsActivity -import de.kuschku.quasseldroid.util.helper.editCommit -import de.kuschku.quasseldroid.util.helper.invoke -import de.kuschku.quasseldroid.util.helper.retint -import de.kuschku.quasseldroid.util.helper.toLiveData +import de.kuschku.quasseldroid.util.helper.* import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer import de.kuschku.quasseldroid.util.service.ServiceBoundActivity import de.kuschku.quasseldroid.util.ui.MaterialContentLoadingProgressBar import de.kuschku.quasseldroid.viewmodel.data.BufferData +import org.threeten.bp.Instant +import org.threeten.bp.ZoneId +import org.threeten.bp.format.DateTimeFormatter +import org.threeten.bp.format.FormatStyle +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException import javax.inject.Inject class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener { @@ -154,83 +159,231 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } } + val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + viewModel.errors.toLiveData().observe(this, Observer { error -> error?.orNull()?.let { when (it) { - is HandshakeMessage.ClientInitReject -> - MaterialDialog.Builder(this) - .title(R.string.label_error_init) - .content(Html.fromHtml(it.errorString)) - .neutralText(R.string.label_close) - .titleColorAttr(R.attr.colorTextPrimary) - .backgroundColorAttr(R.attr.colorBackgroundCard) - .contentColorAttr(R.attr.colorTextPrimary) - .build() - .show() - is HandshakeMessage.CoreSetupReject -> - MaterialDialog.Builder(this) - .title(R.string.label_error_setup) - .content(Html.fromHtml(it.errorString)) - .neutralText(R.string.label_close) - .titleColorAttr(R.attr.colorTextPrimary) - .backgroundColorAttr(R.attr.colorBackgroundCard) - .contentColorAttr(R.attr.colorTextPrimary) - .build() - .show() - is HandshakeMessage.ClientLoginReject -> - MaterialDialog.Builder(this) - .title(R.string.label_error_login) - .content(Html.fromHtml(it.errorString)) - .negativeText(R.string.label_disconnect) - .positiveText("Change User/Password") - .onNegative { _, _ -> - disconnect() - } - .onPositive { _, _ -> - runInBackground { - val account = accountDatabase.accounts().findById(accountId) - - runOnUiThread { - val dialog = MaterialDialog.Builder(this) - .title("Login Required") - .customView(R.layout.setup_account_user, false) - .negativeText(R.string.label_disconnect) - .positiveText(R.string.label_save) - .onNegative { _, _ -> - disconnect() - } - .onPositive { dialog, _ -> + is Error.HandshakeError -> it.message.let { + when (it) { + is HandshakeMessage.ClientInitReject -> + MaterialDialog.Builder(this) + .title(R.string.label_error_init) + .content(Html.fromHtml(it.errorString)) + .neutralText(R.string.label_close) + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() + is HandshakeMessage.CoreSetupReject -> + MaterialDialog.Builder(this) + .title(R.string.label_error_setup) + .content(Html.fromHtml(it.errorString)) + .neutralText(R.string.label_close) + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() + is HandshakeMessage.ClientLoginReject -> + MaterialDialog.Builder(this) + .title(R.string.label_error_login) + .content(Html.fromHtml(it.errorString)) + .negativeText(R.string.label_disconnect) + .positiveText(R.string.label_update_user_password) + .onNegative { _, _ -> + disconnect() + } + .onPositive { _, _ -> + runInBackground { + val account = accountDatabase.accounts().findById(accountId) + + runOnUiThread { + val dialog = MaterialDialog.Builder(this) + .title(R.string.label_error_login) + .customView(R.layout.setup_account_user, false) + .negativeText(R.string.label_disconnect) + .positiveText(R.string.label_save) + .onNegative { _, _ -> + disconnect() + } + .onPositive { dialog, _ -> + dialog.customView?.run { + val userField = findViewById<EditText>(R.id.user) + val passField = findViewById<EditText>(R.id.pass) + + val user = userField.text.toString() + val pass = passField.text.toString() + + backend.value.orNull()?.updateUserDataAndLogin(user, pass) + } + } + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() dialog.customView?.run { val userField = findViewById<EditText>(R.id.user) val passField = findViewById<EditText>(R.id.pass) - val user = userField.text.toString() - val pass = passField.text.toString() - - backend.value.orNull()?.updateUserDataAndLogin(user, pass) + account?.let { + userField.setText(it.user) + } + } + dialog.show() + } + } + } + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() + } + } + is Error.SslError -> { + it.exception.let { + val leafCertificate = it.certificateChain?.firstOrNull() + if (leafCertificate == null) { + MaterialDialog.Builder(this) + .title(R.string.label_error_certificate) + .content(R.string.label_error_certificate_no_certificate) + .neutralText(R.string.label_close) + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() + } else { + when { + it is QuasselSecurityException.Certificate && + (it.cause is CertificateNotYetValidException || + it.cause is CertificateExpiredException) -> { + MaterialDialog.Builder(this) + .title(R.string.label_error_certificate) + .content( + Html.fromHtml( + getString( + R.string.label_error_certificate_invalid, + leafCertificate.fingerprint, + dateTimeFormatter.format(Instant.ofEpochMilli(leafCertificate.notBefore.time) + .atZone(ZoneId.systemDefault())), + dateTimeFormatter.format(Instant.ofEpochMilli(leafCertificate.notAfter.time) + .atZone(ZoneId.systemDefault())) + ) + ) + ) + .negativeText(R.string.label_disconnect) + .positiveText(R.string.label_whitelist) + .onNegative { _, _ -> + disconnect() + } + .onPositive { _, _ -> + runInBackground { + database.validityWhitelist().save( + QuasselDatabase.SslValidityWhitelistEntry( + fingerprint = leafCertificate.fingerprint, + ignoreDate = true + ) + ) + + runOnUiThread { + backend.value.orNull()?.reconnect() + } } } .titleColorAttr(R.attr.colorTextPrimary) .backgroundColorAttr(R.attr.colorBackgroundCard) .contentColorAttr(R.attr.colorTextPrimary) .build() - dialog.customView?.run { - val userField = findViewById<EditText>(R.id.user) - val passField = findViewById<EditText>(R.id.pass) - - account?.let { - userField.setText(it.user) + .show() + } + it is QuasselSecurityException.Certificate -> { + MaterialDialog.Builder(this) + .title(R.string.label_error_certificate) + .content( + Html.fromHtml( + getString( + R.string.label_error_certificate_untrusted, + leafCertificate.fingerprint + ) + ) + ) + .negativeText(R.string.label_disconnect) + .positiveText(R.string.label_whitelist) + .onNegative { _, _ -> + disconnect() } - } - dialog.show() + .onPositive { _, _ -> + runInBackground { + database.validityWhitelist().save( + QuasselDatabase.SslValidityWhitelistEntry( + fingerprint = leafCertificate.fingerprint, + ignoreDate = !leafCertificate.isValid + ) + ) + accountDatabase.accounts().findById(accountId)?.let { + database.hostnameWhitelist().save( + QuasselDatabase.SslHostnameWhitelistEntry( + fingerprint = leafCertificate.fingerprint, + hostname = it.host + ) + ) + } + + runOnUiThread { + backend.value.orNull()?.reconnect() + } + } + } + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() + } + it is QuasselSecurityException.Hostname -> { + MaterialDialog.Builder(this) + .title(R.string.label_error_certificate) + .content( + Html.fromHtml( + getString( + R.string.label_error_certificate_no_match, + leafCertificate.fingerprint, + it.address.host + ) + ) + ) + .negativeText(R.string.label_disconnect) + .positiveText(R.string.label_whitelist) + .onNegative { _, _ -> + disconnect() + } + .onPositive { _, _ -> + runInBackground { + database.hostnameWhitelist().save( + QuasselDatabase.SslHostnameWhitelistEntry( + fingerprint = leafCertificate.fingerprint, + hostname = it.address.host + ) + ) + + runOnUiThread { + backend.value.orNull()?.reconnect() + } + } + } + .titleColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() } } } - .titleColorAttr(R.attr.colorTextPrimary) - .backgroundColorAttr(R.attr.colorBackgroundCard) - .contentColorAttr(R.attr.colorTextPrimary) - .build() - .show() + } + } } } }) diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/X509CertificateHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/X509CertificateHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..b4f58f40135acda41211b154b3a1a271f8e98f69 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/X509CertificateHelper.kt @@ -0,0 +1,26 @@ +package de.kuschku.quasseldroid.util.helper + +import org.apache.commons.codec.digest.DigestUtils +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException +import java.security.cert.X509Certificate + +val X509Certificate.isValid: Boolean + get() = try { + checkValidity() + true + } catch (e: CertificateExpiredException) { + false + } catch (e: CertificateNotYetValidException) { + false + } + +val X509Certificate.fingerprint: String + get() = DigestUtils.sha1(encoded).joinToString(":") { + (it.toInt() and 0xff).toString(16) + } + +val javax.security.cert.X509Certificate.fingerprint: String + get() = DigestUtils.sha1(encoded).joinToString(":") { + (it.toInt() and 0xff).toString(16) + } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5423e39630e41fb086139006a53e8b9925af1a90..651de5611c19892b00584b79418f04eba91bc146 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -12,6 +12,7 @@ <string name="label_back">Zurück</string> <string name="label_buffer_name">Chatname</string> <string name="label_cancel">Abbrechen</string> + <string name="label_update_user_password">Benutzernamen/Passwort ändern</string> <string name="label_close">Schließen</string> <string name="label_colors_custom">Anpassen</string> <string name="label_colors_mirc">mIRC</string> @@ -72,6 +73,7 @@ <string name="label_topic">Kanal-Thema</string> <string name="label_unhide">Nicht mehr ausblenden</string> <string name="label_website">Webseite</string> + <string name="label_whitelist">Ignorieren</string> <string name="label_who">Who</string> <string name="label_who_long">Informationen aller Nutzer aktualisieren</string> <string name="label_whois">Whois</string> diff --git a/app/src/main/res/values-de/strings_error.xml b/app/src/main/res/values-de/strings_error.xml index 0ff779063466fb2b4c17de34565b56424fd8279b..b18e43f925c8f928c66af0c17966b7263adb386a 100644 --- a/app/src/main/res/values-de/strings_error.xml +++ b/app/src/main/res/values-de/strings_error.xml @@ -3,4 +3,16 @@ <string name="label_error_login">Loginfehler</string> <string name="label_error_setup">Einrichtungsfehler</string> <string name="label_error_init">Verbindungsfehler</string> -</resources> \ No newline at end of file + <string name="label_error_certificate">Zertifikatsfehler</string> + <string name="label_error_certificate_no_certificate">Kein Zertifikat verfügbar</string> + <string name="label_error_certificate_no_hostname">Kein Rechnername verfügbar</string> + <string name="label_error_certificate_invalid"><![CDATA[ + <p>Das Zertifikat mit Fingerabdruck <code>%1$s</code> ist nur gültig im Zeitraum von %2$s bis %3$s</p> + ]]></string> + <string name="label_error_certificate_untrusted"><![CDATA[ + <p>Das Zertifikat mit Fingerabdruck <code>%1$s</code> ist nicht vertrauenswürdig.</p> + ]]></string> + <string name="label_error_certificate_no_match"><![CDATA[ + <p>Das Zertifikat mit Fingerabdruck <code>%1$s</code> ist nicht gültig für %2$s.</p> + ]]></string> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3cf9462591b1997fbe07593f862bda9a73966f9d..97b10b2f9a01b1f5598a6c27b5feb74efa362e5f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ <string name="label_back">Back</string> <string name="label_buffer_name">Buffer Name</string> <string name="label_cancel">Cancel</string> + <string name="label_update_user_password">Update User/Password</string> <string name="label_close">Close</string> <string name="label_colors_custom">Custom</string> <string name="label_colors_mirc">mIRC</string> @@ -72,6 +73,7 @@ <string name="label_topic">Channel Topic</string> <string name="label_unhide">Make Visible</string> <string name="label_website">Website</string> + <string name="label_whitelist">Ignore</string> <string name="label_who">Who</string> <string name="label_who_long">Update user information of all users</string> <string name="label_whois">Whois</string> diff --git a/app/src/main/res/values/strings_error.xml b/app/src/main/res/values/strings_error.xml index 7e9e24a255f8dc64a7fccae8f3425fe513e3659e..4e9bdca3a71713fcfdadccaaefee475cec2939fc 100644 --- a/app/src/main/res/values/strings_error.xml +++ b/app/src/main/res/values/strings_error.xml @@ -3,4 +3,16 @@ <string name="label_error_login">Login Error</string> <string name="label_error_setup">Setup Error</string> <string name="label_error_init">Connection Error</string> -</resources> \ No newline at end of file + <string name="label_error_certificate">Certificate Error</string> + <string name="label_error_certificate_no_certificate">No certificate available</string> + <string name="label_error_certificate_no_hostname">No hostname available</string> + <string name="label_error_certificate_invalid"><![CDATA[ + <p>The certificate <code>%1$s</code> is only valid from %2$s to %3$s</p> + ]]></string> + <string name="label_error_certificate_untrusted"><![CDATA[ + <p>The certificate <code>%1$s</code> is not trusted.</p> + ]]></string> + <string name="label_error_certificate_no_match"><![CDATA[ + <p>The certificate <code>%1$s</code> is not valid for %2$s.</p> + ]]></string> +</resources> diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ConnectionState.kt b/lib/src/main/java/de/kuschku/libquassel/connection/ConnectionState.kt similarity index 71% rename from lib/src/main/java/de/kuschku/libquassel/session/ConnectionState.kt rename to lib/src/main/java/de/kuschku/libquassel/connection/ConnectionState.kt index 69fdb7f0ddca6f257a46e30c9471ddba0f1b4470..af1876f5b52f7bc5d3e9986f488ef78301c7b768 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/ConnectionState.kt +++ b/lib/src/main/java/de/kuschku/libquassel/connection/ConnectionState.kt @@ -1,4 +1,4 @@ -package de.kuschku.libquassel.session +package de.kuschku.libquassel.connection enum class ConnectionState { DISCONNECTED, diff --git a/lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt b/lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt similarity index 85% rename from lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt rename to lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt index 69b4dd3b0888da4c344b055788159359c0ca4bc9..83755afa7e31a13f6a32b9b966bc3b6e1c7a0881 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt +++ b/lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt @@ -1,4 +1,4 @@ -package de.kuschku.libquassel.session +package de.kuschku.libquassel.connection import de.kuschku.libquassel.protocol.ClientData import de.kuschku.libquassel.protocol.message.HandshakeMessage @@ -8,6 +8,7 @@ import de.kuschku.libquassel.protocol.primitive.serializer.IntSerializer import de.kuschku.libquassel.protocol.primitive.serializer.ProtocolInfoSerializer import de.kuschku.libquassel.protocol.primitive.serializer.VariantListSerializer import de.kuschku.libquassel.quassel.ProtocolFeature +import de.kuschku.libquassel.session.ProtocolHandler import de.kuschku.libquassel.util.compatibility.CompatibilityUtils import de.kuschku.libquassel.util.compatibility.HandlerService import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log @@ -33,8 +34,10 @@ class CoreConnection( private val clientData: ClientData, private val features: Features, private val trustManager: X509TrustManager, + private val hostnameVerifier: HostnameVerifier, private val address: SocketAddress, - private val handlerService: HandlerService + private val handlerService: HandlerService, + private val securityExceptionCallback: (QuasselSecurityException) -> Unit ) : Thread(), Closeable { companion object { private const val TAG = "CoreConnection" @@ -93,7 +96,7 @@ class CoreConnection( // Wrap socket in SSL context if ssl is enabled if (protocol.flags.hasFlag(ProtocolFeature.TLS)) { - channel = channel?.withSSL(trustManager, address) + channel = channel?.withSSL(trustManager, hostnameVerifier, address) } // Wrap socket in deflater if compression is enabled @@ -126,7 +129,8 @@ class CoreConnection( setState(ConnectionState.CLOSED) interrupt() } catch (e: Throwable) { - log(WARN, TAG, "Error encountered while closing connection", e) + log(WARN, + TAG, "Error encountered while closing connection", e) } } @@ -141,7 +145,8 @@ class CoreConnection( ) ) } catch (e: Throwable) { - log(WARN, TAG, "Error encountered while serializing handshake message", e) + log(WARN, + TAG, "Error encountered while serializing handshake message", e) } } } @@ -157,7 +162,8 @@ class CoreConnection( ) ) } catch (e: Throwable) { - log(WARN, TAG, "Error encountered while serializing sigproxy message", e) + log(WARN, + TAG, "Error encountered while serializing sigproxy message", e) } } } @@ -194,9 +200,14 @@ class CoreConnection( } } } + } catch (e: QuasselSecurityException) { + close() + securityExceptionCallback(e) } catch (e: Throwable) { - log(WARN, TAG, "Error encountered in connection", e) - log(WARN, TAG, "Last sent message: ${MessageRunnable.lastSent.get()}") + log(WARN, + TAG, "Error encountered in connection", e) + log(WARN, + TAG, "Last sent message: ${MessageRunnable.lastSent.get()}") close() } } @@ -210,13 +221,15 @@ class CoreConnection( try { handler.handle(msg) } catch (e: Throwable) { - log(WARN, TAG, "Error encountered while handling sigproxy message", e) + log(WARN, + TAG, "Error encountered while handling sigproxy message", e) log(WARN, TAG, msg.toString()) } } } catch (e: Throwable) { - log(WARN, TAG, "Error encountered while parsing sigproxy message", e) + log(WARN, + TAG, "Error encountered while parsing sigproxy message", e) dataBuffer.hexDump() } } @@ -228,12 +241,14 @@ class CoreConnection( try { handler.handle(msg) } catch (e: Throwable) { - log(WARN, TAG, "Error encountered while handling handshake message", e) + log(WARN, + TAG, "Error encountered while handling handshake message", e) log(WARN, TAG, msg.toString()) } } catch (e: Throwable) { log( - WARN, TAG, "Error encountered while parsing handshake message", e + WARN, + TAG, "Error encountered while parsing handshake message", e ) } diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Features.kt b/lib/src/main/java/de/kuschku/libquassel/connection/Features.kt similarity index 88% rename from lib/src/main/java/de/kuschku/libquassel/session/Features.kt rename to lib/src/main/java/de/kuschku/libquassel/connection/Features.kt index e8a1d04b04dda0e5c4f2bb6f627b7f39a7d9727e..bdde0458b3850cdbfd1f7895139b29aaa3c19708 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/Features.kt +++ b/lib/src/main/java/de/kuschku/libquassel/connection/Features.kt @@ -1,4 +1,4 @@ -package de.kuschku.libquassel.session +package de.kuschku.libquassel.connection import de.kuschku.libquassel.quassel.QuasselFeatures diff --git a/lib/src/main/java/de/kuschku/libquassel/connection/HostnameVerifier.kt b/lib/src/main/java/de/kuschku/libquassel/connection/HostnameVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..94309d66562a1e79cc839f88d684a44c5a439aac --- /dev/null +++ b/lib/src/main/java/de/kuschku/libquassel/connection/HostnameVerifier.kt @@ -0,0 +1,9 @@ +package de.kuschku.libquassel.connection + +import java.security.cert.X509Certificate +import javax.net.ssl.SSLException + +interface HostnameVerifier { + @Throws(SSLException::class) + fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) +} diff --git a/lib/src/main/java/de/kuschku/libquassel/session/MessageRunnable.kt b/lib/src/main/java/de/kuschku/libquassel/connection/MessageRunnable.kt similarity index 96% rename from lib/src/main/java/de/kuschku/libquassel/session/MessageRunnable.kt rename to lib/src/main/java/de/kuschku/libquassel/connection/MessageRunnable.kt index 5ac489f341cb4077af93c68043b865cb541919db..deb00128a3f9fa8dc56f7447ca87a1e42f15724f 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/MessageRunnable.kt +++ b/lib/src/main/java/de/kuschku/libquassel/connection/MessageRunnable.kt @@ -1,4 +1,4 @@ -package de.kuschku.libquassel.session +package de.kuschku.libquassel.connection import de.kuschku.libquassel.protocol.primitive.serializer.Serializer import de.kuschku.libquassel.quassel.QuasselFeatures diff --git a/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt b/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3c40f33e10d354dd3defd1110d1efd8cfe5ff06 --- /dev/null +++ b/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt @@ -0,0 +1,20 @@ +package de.kuschku.libquassel.connection + +import java.security.GeneralSecurityException +import java.security.cert.X509Certificate + +sealed class QuasselSecurityException( + val certificateChain: Array<out X509Certificate>?, + cause: Throwable +) : GeneralSecurityException(cause) { + class Certificate( + certificateChain: Array<out X509Certificate>?, + cause: Exception + ) : QuasselSecurityException(certificateChain, cause) + + class Hostname( + certificateChain: Array<out X509Certificate>?, + val address: SocketAddress, + cause: Exception + ) : QuasselSecurityException(certificateChain, cause) +} diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SocketAddress.kt b/lib/src/main/java/de/kuschku/libquassel/connection/SocketAddress.kt similarity index 77% rename from lib/src/main/java/de/kuschku/libquassel/session/SocketAddress.kt rename to lib/src/main/java/de/kuschku/libquassel/connection/SocketAddress.kt index 3f7214c598a7c37dddc3617a5daaffe761c30ba3..77cf96c3416bb87d2004f30eb6a884a95f49a55d 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/SocketAddress.kt +++ b/lib/src/main/java/de/kuschku/libquassel/connection/SocketAddress.kt @@ -1,4 +1,4 @@ -package de.kuschku.libquassel.session +package de.kuschku.libquassel.connection import java.net.InetSocketAddress diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt b/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt index 2af74cb885c257690ecfe9b414079ac684c7d152..6945b68a55224cee05ade42b4331996d9558fef3 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt @@ -1,5 +1,7 @@ package de.kuschku.libquassel.session +import de.kuschku.libquassel.connection.SocketAddress + interface Backend { fun connectUnlessConnected(address: SocketAddress, user: String, pass: String, reconnect: Boolean) fun connect(address: SocketAddress, user: String, pass: String, reconnect: Boolean) diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Error.kt b/lib/src/main/java/de/kuschku/libquassel/session/Error.kt new file mode 100644 index 0000000000000000000000000000000000000000..51ca4601638f28f8e7c96da86c6a35e7e39d1975 --- /dev/null +++ b/lib/src/main/java/de/kuschku/libquassel/session/Error.kt @@ -0,0 +1,9 @@ +package de.kuschku.libquassel.session + +import de.kuschku.libquassel.connection.QuasselSecurityException +import de.kuschku.libquassel.protocol.message.HandshakeMessage + +sealed class Error { + data class HandshakeError(val message: HandshakeMessage) : Error() + data class SslError(val exception: QuasselSecurityException) : Error() +} diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt b/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt index 6891eb9c558f7ce535ecec1c73797b339974b73b..32929ca1cf11ef95291e689882ee1bcfeb1d73fd 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt @@ -1,8 +1,9 @@ package de.kuschku.libquassel.session +import de.kuschku.libquassel.connection.ConnectionState +import de.kuschku.libquassel.connection.Features import de.kuschku.libquassel.protocol.IdentityId import de.kuschku.libquassel.protocol.NetworkId -import de.kuschku.libquassel.protocol.message.HandshakeMessage import de.kuschku.libquassel.quassel.QuasselFeatures import de.kuschku.libquassel.quassel.syncables.* import io.reactivex.BackpressureStrategy @@ -36,7 +37,7 @@ interface ISession : Closeable { val initStatus: Observable<Pair<Int, Int>> val proxy: SignalProxy - val error: Flowable<HandshakeMessage> + val error: Flowable<Error> val lag: Observable<Long> fun login(user: String, pass: String) @@ -44,10 +45,11 @@ interface ISession : Closeable { companion object { val NULL = object : ISession { override val proxy: SignalProxy = SignalProxy.NULL - override val error = BehaviorSubject.create<HandshakeMessage>() - .toFlowable(BackpressureStrategy.BUFFER) + override val error = BehaviorSubject.create<Error>().toFlowable(BackpressureStrategy.BUFFER) override val state = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED) - override val features: Features = Features(QuasselFeatures.empty(), QuasselFeatures.empty()) + override val features: Features = Features( + QuasselFeatures.empty(), + QuasselFeatures.empty()) override val sslSession: SSLSession? = null override val rpcHandler: RpcHandler? = null diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt index e9c44c094d2615f0c0f1482b2da18f1017fcb8a4..8c049f7765f01d22f30a11aef659fc5dac3f56a9 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt @@ -1,5 +1,6 @@ package de.kuschku.libquassel.session +import de.kuschku.libquassel.connection.* import de.kuschku.libquassel.protocol.* import de.kuschku.libquassel.protocol.message.HandshakeMessage import de.kuschku.libquassel.protocol.message.SignalProxyMessage @@ -17,6 +18,7 @@ import javax.net.ssl.X509TrustManager class Session( clientData: ClientData, trustManager: X509TrustManager, + hostnameVerifier: HostnameVerifier, address: SocketAddress, private val handlerService: HandlerService, backlogStorage: BacklogStorage, @@ -26,17 +28,18 @@ class Session( ) : ProtocolHandler(exceptionHandler), ISession { override val objectStorage: ObjectStorage = ObjectStorage(this) override val proxy: SignalProxy = this - override val features = Features(clientData.clientFeatures, QuasselFeatures.empty()) + override val features = Features(clientData.clientFeatures, + QuasselFeatures.empty()) override val sslSession get() = coreConnection.sslSession private val coreConnection = CoreConnection( - this, clientData, features, trustManager, address, handlerService + this, clientData, features, trustManager, hostnameVerifier, address, handlerService, ::handle ) override val state = coreConnection.state - private val _error = PublishSubject.create<HandshakeMessage>() + private val _error = PublishSubject.create<Error>() override val error = _error.toFlowable(BackpressureStrategy.BUFFER) override val aliasManager = AliasManager(this) @@ -77,7 +80,7 @@ class Session( if (f.coreConfigured == true) { login() } else { - _error.onNext(f) + _error.onNext(Error.HandshakeError(f)) } return true } @@ -102,20 +105,24 @@ class Session( } override fun handle(f: HandshakeMessage.ClientInitReject): Boolean { - _error.onNext(f) + _error.onNext(Error.HandshakeError(f)) return true } override fun handle(f: HandshakeMessage.CoreSetupReject): Boolean { - _error.onNext(f) + _error.onNext(Error.HandshakeError(f)) return true } override fun handle(f: HandshakeMessage.ClientLoginReject): Boolean { - _error.onNext(f) + _error.onNext(Error.HandshakeError(f)) return true } + fun handle(f: QuasselSecurityException) { + _error.onNext(Error.SslError(f)) + } + fun addNetwork(networkId: NetworkId) { val network = Network(networkId, this) networks[networkId] = network diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt index 5dd5851a24ebad8ed4ee0028dd445aeeff0fa1be..9adb12b36212b7b23c4520f5cd606c2b273d0df6 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt @@ -1,7 +1,9 @@ package de.kuschku.libquassel.session +import de.kuschku.libquassel.connection.ConnectionState +import de.kuschku.libquassel.connection.HostnameVerifier +import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.protocol.ClientData -import de.kuschku.libquassel.protocol.message.HandshakeMessage import de.kuschku.libquassel.quassel.syncables.interfaces.invokers.Invokers import de.kuschku.libquassel.util.compatibility.HandlerService import de.kuschku.libquassel.util.compatibility.LoggingHandler @@ -24,6 +26,7 @@ class SessionManager( private var lastClientData: ClientData? = null private var lastTrustManager: X509TrustManager? = null + private var lastHostnameVerifier: HostnameVerifier? = null private var lastAddress: SocketAddress? = null private var lastUserData: Pair<String, String>? = null private var lastShouldReconnect = false @@ -39,7 +42,7 @@ class SessionManager( else lastSession } - val error: Observable<HandshakeMessage> + val error: Observable<Error> get() = inProgressSession .toFlowable(BackpressureStrategy.LATEST) .switchMap(ISession::error) @@ -75,6 +78,7 @@ class SessionManager( fun connect( clientData: ClientData, trustManager: X509TrustManager, + hostnameVerifier: HostnameVerifier, address: SocketAddress, userData: Pair<String, String>, shouldReconnect: Boolean = false @@ -82,6 +86,7 @@ class SessionManager( inProgressSession.value.close() lastClientData = clientData lastTrustManager = trustManager + lastHostnameVerifier = hostnameVerifier lastAddress = address lastUserData = userData lastShouldReconnect = shouldReconnect @@ -89,6 +94,7 @@ class SessionManager( Session( clientData, trustManager, + hostnameVerifier, address, handlerService, backlogStorage, @@ -103,11 +109,12 @@ class SessionManager( if (lastShouldReconnect || forceReconnect) { val clientData = lastClientData val trustManager = lastTrustManager + val hostnameVerifier = lastHostnameVerifier val address = lastAddress val userData = lastUserData - if (clientData != null && trustManager != null && address != null && userData != null) { - connect(clientData, trustManager, address, userData, forceReconnect) + if (clientData != null && trustManager != null && hostnameVerifier != null && address != null && userData != null) { + connect(clientData, trustManager, hostnameVerifier, address, userData, forceReconnect) } } } 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 558603f3670193d19bf388138684f820cb51f251..79f2c76b6a666fcfe7ed12c6d145e583fc42faf9 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 @@ -1,6 +1,7 @@ package de.kuschku.libquassel.util.nio -import de.kuschku.libquassel.session.SocketAddress +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.StreamChannelFactory import java.io.Flushable @@ -15,9 +16,11 @@ import java.nio.channels.InterruptibleChannel import java.nio.channels.ReadableByteChannel import java.nio.channels.WritableByteChannel import java.security.GeneralSecurityException +import java.security.cert.X509Certificate import java.util.zip.InflaterInputStream import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager class WrappedChannel( @@ -57,13 +60,22 @@ class WrappedChannel( } @Throws(GeneralSecurityException::class, IOException::class) - fun withSSL(certificateManager: X509TrustManager, address: SocketAddress): WrappedChannel { + fun withSSL(certificateManager: X509TrustManager, hostnameVerifier: HostnameVerifier, + address: SocketAddress): WrappedChannel { val context = SSLContext.getInstance("TLSv1.2") val managers = arrayOf(certificateManager) context.init(null, managers, null) val factory = context.socketFactory + SSLSocketFactory.getDefault() + val socket = factory.createSocket(socket, address.host, address.port, true) as SSLSocket socket.useClientMode = true + socket.addHandshakeCompletedListener { + hostnameVerifier.checkValid( + address, + socket.session.peerCertificates.map { it as X509Certificate }.toTypedArray() + ) + } socket.startHandshake() return WrappedChannel.ofSocket(socket) } diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt index 9ba9c3abfc9ab74cce163590f427a47beb2cb0d3..d76b9060fa3c11af659937dc00279c2dc5c4bc46 100644 --- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt +++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt @@ -14,12 +14,14 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase.* import io.reactivex.Flowable import org.threeten.bp.Instant -@Database(entities = [DatabaseMessage::class, Filtered::class, SslException::class], version = 9) +@Database(entities = [DatabaseMessage::class, Filtered::class, SslValidityWhitelistEntry::class, SslHostnameWhitelistEntry::class], + version = 13) @TypeConverters(DatabaseMessage.MessageTypeConverters::class) abstract class QuasselDatabase : RoomDatabase() { abstract fun message(): MessageDao abstract fun filtered(): FilteredDao - abstract fun sslExceptions(): SslExceptionDao + abstract fun validityWhitelist(): SslValidityWhitelistDao + abstract fun hostnameWhitelist(): SslHostnameWhitelistDao @Entity(tableName = "message", indices = [Index("bufferId"), Index("ignored")]) data class DatabaseMessage( @@ -142,29 +144,41 @@ abstract class QuasselDatabase : RoomDatabase() { fun clear(accountId: Long, bufferId: Int) } - @Entity(tableName = "ssl_exception", primaryKeys = ["accountId", "certificateFingerprint"]) - data class SslException( - var accountId: Long, - var certificateFingerprint: String, - var ignoreValidityDate: Boolean + @Entity(tableName = "ssl_validity_whitelist") + data class SslValidityWhitelistEntry( + @PrimaryKey + var fingerprint: String, + var ignoreDate: Boolean ) @Dao - interface SslExceptionDao { + interface SslValidityWhitelistDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun save(vararg entities: SslException) + fun save(vararg entities: SslValidityWhitelistEntry) - @Query("SELECT * FROM ssl_exception WHERE accountId = :accountId AND certificateFingerprint = :certificateFingerprint") - fun all(accountId: Long, certificateFingerprint: String): List<SslException> + @Query("SELECT * FROM ssl_validity_whitelist") + fun all(): List<SslValidityWhitelistEntry> - @Query("DELETE FROM ssl_exception") - fun clear() + @Query("SELECT * FROM ssl_validity_whitelist WHERE fingerprint = :fingerprint") + fun find(fingerprint: String): SslValidityWhitelistEntry? + } - @Query("DELETE FROM ssl_exception WHERE accountId = :accountId") - fun clear(accountId: Long) + @Entity(tableName = "ssl_hostname_whitelist", primaryKeys = ["fingerprint", "hostname"]) + data class SslHostnameWhitelistEntry( + var fingerprint: String, + var hostname: String + ) - @Query("DELETE FROM ssl_exception WHERE accountId = :accountId AND certificateFingerprint = :certificateFingerprint") - fun clear(accountId: Long, certificateFingerprint: String) + @Dao + interface SslHostnameWhitelistDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun save(vararg entities: SslHostnameWhitelistEntry) + + @Query("SELECT * FROM ssl_hostname_whitelist") + fun all(): List<SslHostnameWhitelistEntry> + + @Query("SELECT * FROM ssl_hostname_whitelist WHERE fingerprint = :fingerprint AND hostname = :hostname") + fun find(fingerprint: String, hostname: String): SslHostnameWhitelistEntry? } object Creator { @@ -223,6 +237,33 @@ abstract class QuasselDatabase : RoomDatabase() { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("create table ssl_exception (accountId INTEGER not null, certificateFingerprint TEXT not null, ignoreValidityDate INTEGER not null, primary key(accountId, certificateFingerprint));") } + }, + object : Migration(9, 10) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("drop table ssl_exception;") + database.execSQL("create table ssl_exception (accountId INTEGER not null, hostName TEXT not null, certificateFingerprint TEXT not null, ignoreValidityDate INTEGER not null, primary key(accountId, hostName, certificateFingerprint));") + } + }, + object : Migration(10, 11) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("drop table ssl_exception;") + database.execSQL("create table ssl_exception (accountId INTEGER not null, certificateFingerprint TEXT not null, ignoreValidityDate INTEGER not null, primary key(accountId, certificateFingerprint));") + } + }, + object : Migration(11, 12) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("drop table ssl_exception;") + database.execSQL("create table ssl_validity_whitelist (fingerprint TEXT not null, ignoreDate INTEGER not null, primary key(fingerprint));") + database.execSQL("create table ssl_hostname_whitelist (fingerprint TEXT not null, hostname TEXT not null, primary key(fingerprint, hostname));") + } + }, + object : Migration(12, 13) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("drop table ssl_validity_whitelist;") + database.execSQL("drop table ssl_hostname_whitelist;") + database.execSQL("create table ssl_validity_whitelist (fingerprint TEXT not null, ignoreDate INTEGER not null, primary key(fingerprint));") + database.execSQL("create table ssl_hostname_whitelist (fingerprint TEXT not null, hostname TEXT not null, primary key(fingerprint, hostname));") + } } ).build() } diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt index 623d7e6668633ff7e21e3b42aa28d6416e3ae0ff..eda7456ad462841dfac89913e7c4dd73f942c5e8 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt @@ -1,6 +1,7 @@ package de.kuschku.quasseldroid.viewmodel import android.arch.lifecycle.ViewModel +import de.kuschku.libquassel.connection.ConnectionState import de.kuschku.libquassel.protocol.* import de.kuschku.libquassel.quassel.BufferInfo import de.kuschku.libquassel.quassel.syncables.BufferViewConfig @@ -8,7 +9,6 @@ import de.kuschku.libquassel.quassel.syncables.IrcChannel import de.kuschku.libquassel.quassel.syncables.IrcUser import de.kuschku.libquassel.quassel.syncables.Network import de.kuschku.libquassel.session.Backend -import de.kuschku.libquassel.session.ConnectionState import de.kuschku.libquassel.session.ISession import de.kuschku.libquassel.session.SessionManager import de.kuschku.libquassel.util.Optional