From 4fac9b14bc4e35fd317bcb75ae5e7f39c22b3c75 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 26 Nov 2018 13:00:33 +0100 Subject: [PATCH 01/37] Migrate to SDK level 28 and AndroidX --- build.gradle | 14 ++++++------- gradle.properties | 1 + .../cert4android/CustomCertManagerTest.kt | 20 +++++++++---------- .../bitfire/cert4android/CustomCertService.kt | 2 +- .../bitfire/cert4android/NotificationUtils.kt | 2 +- .../cert4android/TrustCertificateActivity.kt | 2 +- .../res/layout/activity_trust_certificate.xml | 8 ++++---- 7 files changed, 25 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index 5dc5fb9..0d6e7ab 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.2.71' + ext.kotlin_version = '1.3.10' ext.dokka_version = '0.9.17' repositories { @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.2.0' + classpath 'com.android.tools.build:gradle:3.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}" } @@ -39,18 +39,18 @@ android { } defaultConfig { - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support:cardview-v7:28.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.cardview:cardview:1.0.0' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test:rules:1.0.2' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test:rules:1.1.0' testImplementation 'junit:junit:4.12' } diff --git a/gradle.properties b/gradle.properties index 8bd86f6..d9cf55d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true diff --git a/src/androidTest/java/at/bitfire/cert4android/CustomCertManagerTest.kt b/src/androidTest/java/at/bitfire/cert4android/CustomCertManagerTest.kt index 02172de..042439f 100644 --- a/src/androidTest/java/at/bitfire/cert4android/CustomCertManagerTest.kt +++ b/src/androidTest/java/at/bitfire/cert4android/CustomCertManagerTest.kt @@ -11,9 +11,8 @@ package at.bitfire.cert4android import android.app.Service import android.content.Intent import android.os.IBinder -import android.support.test.InstrumentationRegistry.getContext -import android.support.test.InstrumentationRegistry.getTargetContext -import android.support.test.rule.ServiceTestRule +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.rule.ServiceTestRule import org.junit.After import org.junit.Assert.assertNotNull import org.junit.Assume.assumeNotNull @@ -70,12 +69,13 @@ class CustomCertManagerTest { val binder = bindService(CustomCertService::class.java) assertNotNull(binder) - CustomCertManager.resetCertificates(getContext()) + val context = getInstrumentation().context + CustomCertManager.resetCertificates(context) - certManager = CustomCertManager(getContext(), false) + certManager = CustomCertManager(context, false) assertNotNull(certManager) - paranoidCertManager = CustomCertManager(getContext(), false, false) + paranoidCertManager = CustomCertManager(context, false, false) assertNotNull(paranoidCertManager) } @@ -114,7 +114,7 @@ class CustomCertManagerTest { // remove certificate and check again // should now be rejected for the whole session, i.e. no timeout anymore - val intent = Intent(getContext(), CustomCertService::class.java) + val intent = Intent(getInstrumentation().context, CustomCertService::class.java) intent.action = CustomCertService.CMD_CERTIFICATION_DECISION intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, siteCerts!!.first().encoded) intent.putExtra(CustomCertService.EXTRA_TRUSTED, false) @@ -124,7 +124,7 @@ class CustomCertManagerTest { private fun addCustomCertificate() { // add certificate and check again - val intent = Intent(getContext(), CustomCertService::class.java) + val intent = Intent(getInstrumentation().context, CustomCertService::class.java) intent.action = CustomCertService.CMD_CERTIFICATION_DECISION intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, siteCerts!!.first().encoded) intent.putExtra(CustomCertService.EXTRA_TRUSTED, true) @@ -133,10 +133,10 @@ class CustomCertManagerTest { private fun bindService(clazz: Class): IBinder { - var binder = serviceTestRule.bindService(Intent(getTargetContext(), clazz)) + var binder = serviceTestRule.bindService(Intent(getInstrumentation().targetContext, clazz)) var it = 0 while (binder == null && it++ <100) { - binder = serviceTestRule.bindService(Intent(getTargetContext(), clazz)) + binder = serviceTestRule.bindService(Intent(getInstrumentation().targetContext, clazz)) System.err.println("Waiting for ServiceTestRule.bindService") Thread.sleep(50) } diff --git a/src/main/java/at/bitfire/cert4android/CustomCertService.kt b/src/main/java/at/bitfire/cert4android/CustomCertService.kt index 61aee67..f07e020 100644 --- a/src/main/java/at/bitfire/cert4android/CustomCertService.kt +++ b/src/main/java/at/bitfire/cert4android/CustomCertService.kt @@ -12,8 +12,8 @@ import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent -import android.support.v4.app.NotificationCompat import android.widget.Toast +import androidx.core.app.NotificationCompat import java.io.ByteArrayInputStream import java.io.File import java.io.FileInputStream diff --git a/src/main/java/at/bitfire/cert4android/NotificationUtils.kt b/src/main/java/at/bitfire/cert4android/NotificationUtils.kt index d0371f9..2f32f01 100644 --- a/src/main/java/at/bitfire/cert4android/NotificationUtils.kt +++ b/src/main/java/at/bitfire/cert4android/NotificationUtils.kt @@ -12,7 +12,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.os.Build -import android.support.v4.app.NotificationManagerCompat +import androidx.core.app.NotificationManagerCompat object NotificationUtils { diff --git a/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt b/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt index cec7b42..5b2693e 100644 --- a/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt +++ b/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt @@ -10,11 +10,11 @@ package at.bitfire.cert4android import android.content.Intent import android.os.Bundle -import android.support.v7.app.AppCompatActivity import android.view.View import android.widget.Button import android.widget.CheckBox import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import java.io.ByteArrayInputStream import java.security.MessageDigest import java.security.cert.CertificateFactory diff --git a/src/main/res/layout/activity_trust_certificate.xml b/src/main/res/layout/activity_trust_certificate.xml index e49d290..f978e3c 100644 --- a/src/main/res/layout/activity_trust_certificate.xml +++ b/src/main/res/layout/activity_trust_certificate.xml @@ -18,7 +18,7 @@ android:text="@string/trust_certificate_unknown_certificate_found" android:textAppearance="?android:attr/textAppearanceMedium"/> - @@ -97,7 +97,7 @@ android:layout_marginBottom="8dp" android:text="@string/trust_certificate_fingerprint_verified"/> - @@ -118,11 +118,11 @@ android:text="@string/trust_certificate_reject" android:onClick="rejectCertificate"/> - + - + Date: Fri, 30 Nov 2018 12:48:10 +0100 Subject: [PATCH 02/37] Don't do emulator checks (because we can only use shared runners at the moment); minor lint --- .gitlab-ci.yml | 5 +++-- .../at/bitfire/cert4android/CustomCertManager.kt | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 809a32a..7f79ead 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,8 +9,9 @@ cache: test: script: - - (cd /sdk/emulator; ./emulator @test -no-audio -no-window & wait-for-emulator.sh) - - ./gradlew check connectedCheck +# - (cd /sdk/emulator; ./emulator @test -no-audio -no-window & wait-for-emulator.sh) +# - ./gradlew check connectedCheck + - ./gradlew check artifacts: paths: - build/outputs/lint-results-debug.html diff --git a/src/main/java/at/bitfire/cert4android/CustomCertManager.kt b/src/main/java/at/bitfire/cert4android/CustomCertManager.kt index 380d504..a6830db 100644 --- a/src/main/java/at/bitfire/cert4android/CustomCertManager.kt +++ b/src/main/java/at/bitfire/cert4android/CustomCertManager.kt @@ -62,7 +62,7 @@ class CustomCertManager @JvmOverloads constructor( } var service: ICustomCertService? = null - private var serviceConnection: ServiceConnection? + private var serviceConn: ServiceConnection? = null private var serviceLock = Object() /** system-default trust store */ @@ -75,7 +75,7 @@ class CustomCertManager @JvmOverloads constructor( init { - serviceConnection = object: ServiceConnection { + val newServiceConn = object: ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { Constants.log.fine("Connected to service") synchronized(serviceLock) { @@ -96,7 +96,9 @@ class CustomCertManager @JvmOverloads constructor( throw IllegalStateException("must not be run on main thread") Constants.log.fine("Binding to service") - if (context.bindService(Intent(context, CustomCertService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)) { + if (context.bindService(Intent(context, CustomCertService::class.java), newServiceConn, Context.BIND_AUTO_CREATE)) { + serviceConn = newServiceConn + Constants.log.fine("Waiting for service to be bound") synchronized(serviceLock) { while (service == null) @@ -110,10 +112,10 @@ class CustomCertManager @JvmOverloads constructor( } override fun close() { - serviceConnection?.let { + serviceConn?.let { try { context.unbindService(it) - serviceConnection = null + serviceConn = null } catch (e: Exception) { Constants.log.log(Level.WARNING, "Couldn't unbind CustomCertService", e) } -- GitLab From 960d33c71956e96decb9699b589427989e501f51 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 3 Dec 2018 11:18:12 +0100 Subject: [PATCH 03/37] Fetch translations from Transifex --- src/main/res/values-sr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index d79c291..c931a8f 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -4,6 +4,8 @@ Безбедност везе Прегледајте сертификат + Сертификат привремено одбијен + серт-за-Андроид је наишао на непознат сертификат. Желите ли да се поуздате у њега? Детаљи X509 сертификата Издат за -- GitLab From 75dafa4397aacd98efda846818ad3c23367bcf05 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 6 Jan 2019 18:13:45 +0100 Subject: [PATCH 04/37] Update gradle, Kotlin --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 0d6e7ab..7ea47ae 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.3.10' + ext.kotlin_version = '1.3.11' ext.dokka_version = '0.9.17' repositories { @@ -49,8 +49,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.cardview:cardview:1.0.0' - androidTestImplementation 'androidx.test:runner:1.1.0' - androidTestImplementation 'androidx.test:rules:1.1.0' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test:rules:1.1.1' testImplementation 'junit:junit:4.12' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5801d8e..a547d65 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-all.zip -- GitLab From dce08592cb6c9431002f9e3727965793e386b8e6 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 6 Jan 2019 18:43:40 +0100 Subject: [PATCH 05/37] Fix CI --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7f79ead..7031330 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: registry.gitlab.com/bitfireat/davdroid:latest +image: registry.gitlab.com/bitfireat/davx5-ose:latest before_script: - export GRADLE_USER_HOME=`pwd`/.gradle; chmod +x gradlew -- GitLab From 0ba3ff0271b2d6afb897859281a78d211513e5e4 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 18 Jan 2019 09:51:29 +0100 Subject: [PATCH 06/37] Fetch translations from Transifex --- src/main/res/values-fa/strings.xml | 4 ++++ src/main/res/values-gl/strings.xml | 21 +++++++++++++++++++++ src/main/res/values-sl-rSI/strings.xml | 4 ++++ 3 files changed, 29 insertions(+) create mode 100644 src/main/res/values-fa/strings.xml create mode 100644 src/main/res/values-gl/strings.xml create mode 100644 src/main/res/values-sl-rSI/strings.xml diff --git a/src/main/res/values-fa/strings.xml b/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000..583641d --- /dev/null +++ b/src/main/res/values-fa/strings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000..c2dabb2 --- /dev/null +++ b/src/main/res/values-gl/strings.xml @@ -0,0 +1,21 @@ + + + + Seguridade da conexión + Por favor revise o certificado + + Certificado temporalmente rexeitado + + cert4android atopou un certificado descoñecido. Quere confiar nel? + Detalles do certificado X509 + Proporcionado para + Proporcionado por + Válido por + %1$s – %2$s (non será imposto) + Pegadas dixitais + Verifiquei manualmente toda a pegada dixital. + Aceptar + Rexeitar + Pode restablecer os certificados personalizados nos axustes da app. + + \ No newline at end of file diff --git a/src/main/res/values-sl-rSI/strings.xml b/src/main/res/values-sl-rSI/strings.xml new file mode 100644 index 0000000..583641d --- /dev/null +++ b/src/main/res/values-sl-rSI/strings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file -- GitLab From 99e61963607a0566eb49561d8d7a8074a7bc7cee Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 29 Jan 2019 19:59:39 +0100 Subject: [PATCH 07/37] Update gradle, Kotlin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7ea47ae..54a637c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.3.11' + ext.kotlin_version = '1.3.20' ext.dokka_version = '0.9.17' repositories { @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}" } -- GitLab From 1f797b70ccaa2df0f3ed589f90e89be7d26e3ec4 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 6 Feb 2019 16:38:43 +0100 Subject: [PATCH 08/37] Fetch translations from Transifex --- src/main/res/values-el/strings.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/res/values-el/strings.xml diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml new file mode 100644 index 0000000..28f051f --- /dev/null +++ b/src/main/res/values-el/strings.xml @@ -0,0 +1,21 @@ + + + + Ασφάλεια σύνδεσης + Παρακαλώ ελέγξτε το πιστοποιητικό + + Το πιστοποιητικό απορρίφθηκε προσωρινά + + Το cert4android εντόπισε ένα άγνωστο πιστοποιητικό. Θέλετε να το εμπιστεύεστε; + Λεπτομέρειες πιστοποιητικού X509 + Εκδόθηκε για + Εκδόθηκε από + Περίοδος ισχύος + %1$s – %2$s (δεν θα εφαρμοστεί) + Αποτυπώματα + Έχω πιστοποιήσει χειροκίνητα το δακτυλικό αποτύπωμα. + Αποδοχή + Απόρριψη + Μπορείτε να επαναφέρετε όλα τα προσαρμοσμένα πιστοποιητικά στις ρυθμίσεις της εφαρμογής. + + \ No newline at end of file -- GitLab From fa58cc80d7e8262b67223178b81b5a08680e34a0 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 20 Feb 2019 17:08:09 +0100 Subject: [PATCH 09/37] Add custom socket factory to support client certificates and more ciphers on Android >= 4.4 <6 --- .../cert4android/CertTlsSocketFactory.kt | 168 ++++++++++++++++++ .../bitfire/cert4android/CustomCertManager.kt | 10 +- 2 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 src/main/java/at/bitfire/cert4android/CertTlsSocketFactory.kt diff --git a/src/main/java/at/bitfire/cert4android/CertTlsSocketFactory.kt b/src/main/java/at/bitfire/cert4android/CertTlsSocketFactory.kt new file mode 100644 index 0000000..9a860ef --- /dev/null +++ b/src/main/java/at/bitfire/cert4android/CertTlsSocketFactory.kt @@ -0,0 +1,168 @@ +/* + * Copyright © Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.bitfire.cert4android + +import android.os.Build +import android.util.Log +import java.io.IOException +import java.net.InetAddress +import java.net.Socket +import java.security.GeneralSecurityException +import java.util.* +import javax.net.ssl.* + +/** + * Custom TLS socket factory with support for + * - enabling/disabling algorithms depending on the Android version, + * - client certificate authentication + * + * @param keyManager a key manager which provides client certificates, or null + * @param trustManager trust manager to use (most likely a [CustomCertManager] instance) + */ +class CertTlsSocketFactory( + keyManager: KeyManager?, + trustManager: X509TrustManager +): SSLSocketFactory() { + + private var delegate: SSLSocketFactory + + companion object { + // Android 5.0+ (API level 21) provides reasonable default settings + // but it still allows SSLv3 + // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html + var protocols: Array? = null + var cipherSuites: Array? = null + init { + if (Build.VERSION.SDK_INT >= 23) { + // Since Android 6.0 (API level 23), + // - TLSv1.1 and TLSv1.2 is enabled by default + // - SSLv3 is disabled by default + // - all modern ciphers are activated by default + protocols = null + cipherSuites = null + Log.d(Constants.TAG, "Using device default TLS protocols/ciphers") + } else { + (SSLSocketFactory.getDefault().createSocket() as? SSLSocket)?.use { socket -> + try { + /* set reasonable protocol versions */ + // - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0) + // - remove all SSL versions (especially SSLv3) because they're insecure now + val whichProtocols = LinkedList() + for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) }) + whichProtocols += protocol + Log.i(Constants.TAG, "Enabling (only) these TLS protocols: ${whichProtocols.joinToString(", ")}") + protocols = whichProtocols.toTypedArray() + + /* set up reasonable cipher suites */ + val knownCiphers = arrayOf( + // TLS 1.2 + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + // maximum interoperability + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + // additionally + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" + ) + val availableCiphers = socket.supportedCipherSuites + Log.i(Constants.TAG, "Available cipher suites: ${availableCiphers.joinToString(", ")}") + + /* For maximum security, preferredCiphers should *replace* enabled ciphers (thus + * disabling ciphers which are enabled by default, but have become unsecure), but for + * the security level of DAVx5 and maximum compatibility, disabling of insecure + * ciphers should be a server-side task */ + + // for the final set of enabled ciphers, take the ciphers enabled by default, ... + val whichCiphers = LinkedList() + whichCiphers.addAll(socket.enabledCipherSuites) + Log.i(Constants.TAG, "Cipher suites enabled by default: ${whichCiphers.joinToString(", ")}") + // ... add explicitly allowed ciphers ... + whichCiphers.addAll(knownCiphers) + // ... and keep only those which are actually available + whichCiphers.retainAll(availableCiphers) + + Log.i(Constants.TAG, "Enabling (only) these TLS ciphers: " + whichCiphers.joinToString(", ")) + cipherSuites = whichCiphers.toTypedArray() + } catch (e: IOException) { + Log.e(Constants.TAG, "Couldn't determine default TLS settings") + } + } + } + } + } + + + init { + try { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init( + if (keyManager != null) arrayOf(keyManager) else null, + arrayOf(trustManager), + null) + delegate = sslContext.socketFactory + } catch (e: GeneralSecurityException) { + throw IllegalStateException() // system has no TLS + } + } + + override fun getDefaultCipherSuites(): Array? = cipherSuites ?: delegate.defaultCipherSuites + override fun getSupportedCipherSuites(): Array? = cipherSuites ?: delegate.supportedCipherSuites + + override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { + val ssl = delegate.createSocket(s, host, port, autoClose) + if (ssl is SSLSocket) + upgradeTLS(ssl) + return ssl + } + + override fun createSocket(host: String, port: Int): Socket { + val ssl = delegate.createSocket(host, port) + if (ssl is SSLSocket) + upgradeTLS(ssl) + return ssl + } + + override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket { + val ssl = delegate.createSocket(host, port, localHost, localPort) + if (ssl is SSLSocket) + upgradeTLS(ssl) + return ssl + } + + override fun createSocket(host: InetAddress, port: Int): Socket { + val ssl = delegate.createSocket(host, port) + if (ssl is SSLSocket) + upgradeTLS(ssl) + return ssl + } + + override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket { + val ssl = delegate.createSocket(address, port, localAddress, localPort) + if (ssl is SSLSocket) + upgradeTLS(ssl) + return ssl + } + + + private fun upgradeTLS(ssl: SSLSocket) { + protocols?.let { ssl.enabledProtocols = it } + cipherSuites?.let { ssl.enabledCipherSuites = it } + } + +} diff --git a/src/main/java/at/bitfire/cert4android/CustomCertManager.kt b/src/main/java/at/bitfire/cert4android/CustomCertManager.kt index a6830db..dcbc69e 100644 --- a/src/main/java/at/bitfire/cert4android/CustomCertManager.kt +++ b/src/main/java/at/bitfire/cert4android/CustomCertManager.kt @@ -34,6 +34,7 @@ import javax.net.ssl.X509TrustManager * @param interactive true: users will be notified in case of unknown certificates; * false: unknown certificates will be rejected (only uses custom certificate key store) * @param trustSystemCerts whether system certificates will be trusted + * @param appInForeground Whether to launch [TrustCertificateActivity] directly. The notification will always be shown. * * @constructor Creates a new instance, using a certain [CustomCertService] messenger (for testing). * Must not be run from the main thread because this constructor may request binding to [CustomCertService]. @@ -45,7 +46,10 @@ import javax.net.ssl.X509TrustManager class CustomCertManager @JvmOverloads constructor( val context: Context, val interactive: Boolean = true, - trustSystemCerts: Boolean = true + trustSystemCerts: Boolean = true, + + @Volatile + var appInForeground: Boolean = false ): X509TrustManager, Closeable { companion object { @@ -70,10 +74,6 @@ class CustomCertManager @JvmOverloads constructor( if (trustSystemCerts) CertUtils.getTrustManager(null) else null - /** Whether to launch [TrustCertificateActivity] directly. The notification will always be shown. */ - var appInForeground = false - - init { val newServiceConn = object: ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { -- GitLab From aaeb4de094b613e1be68d170522c42a3d974edf4 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 20 Feb 2019 18:15:20 +0100 Subject: [PATCH 10/37] Add test --- build.gradle | 7 +- .../cert4android/CertTlsSocketFactoryTest.kt | 127 ++++++++++++++++++ src/androidTest/resources/sample.crt | 24 ++++ src/androidTest/resources/sample.key | Bin 0 -> 1220 bytes 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/androidTest/java/at/bitfire/cert4android/CertTlsSocketFactoryTest.kt create mode 100644 src/androidTest/resources/sample.crt create mode 100644 src/androidTest/resources/sample.key diff --git a/build.gradle b/build.gradle index 54a637c..9415182 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.3.20' + ext.kotlin_version = '1.3.21' ext.dokka_version = '0.9.17' repositories { @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}" } @@ -51,6 +51,9 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1' + androidTestImplementation 'commons-io:commons-io:2.6' + androidTestImplementation 'org.apache.commons:commons-lang3:3.8.1' testImplementation 'junit:junit:4.12' } diff --git a/src/androidTest/java/at/bitfire/cert4android/CertTlsSocketFactoryTest.kt b/src/androidTest/java/at/bitfire/cert4android/CertTlsSocketFactoryTest.kt new file mode 100644 index 0000000..521bfb5 --- /dev/null +++ b/src/androidTest/java/at/bitfire/cert4android/CertTlsSocketFactoryTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright © Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.bitfire.cert4android + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockWebServer +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.ArrayUtils.contains +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.net.Socket +import java.security.KeyFactory +import java.security.KeyStore +import java.security.Principal +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.spec.PKCS8EncodedKeySpec +import javax.net.ssl.SSLSocket +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509TrustManager + +class CertTlsSocketFactoryTest { + + private lateinit var certMgr: CustomCertManager + private lateinit var factory: CertTlsSocketFactory + private val server = MockWebServer() + + @Before + fun startServer() { + certMgr = CustomCertManager(getInstrumentation().context, false, true) + factory = CertTlsSocketFactory(null, certMgr) + server.start() + } + + @After + fun stopServer() { + server.shutdown() + certMgr.close() + } + + + @Test + fun testSendClientCertificate() { + var public: X509Certificate? = null + javaClass.classLoader!!.getResourceAsStream("sample.crt").use { + public = CertificateFactory.getInstance("X509").generateCertificate(it) as? X509Certificate + } + assertNotNull(public) + + val keyFactory = KeyFactory.getInstance("RSA") + val private = keyFactory.generatePrivate(PKCS8EncodedKeySpec(readResource("sample.key"))) + assertNotNull(private) + + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + val alias = "sample" + keyStore.setKeyEntry(alias, private, null, arrayOf(public)) + assertTrue(keyStore.containsAlias(alias)) + + val trustManagerFactory = TrustManagerFactory.getInstance("X509") + trustManagerFactory.init(null as KeyStore?) + val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager + + val factory = CertTlsSocketFactory(object: X509ExtendedKeyManager() { + override fun getServerAliases(p0: String?, p1: Array?): Array? = null + override fun chooseServerAlias(p0: String?, p1: Array?, p2: Socket?) = null + + override fun getClientAliases(p0: String?, p1: Array?) = + arrayOf(alias) + + override fun chooseClientAlias(p0: Array?, p1: Array?, p2: Socket?) = + alias + + override fun getCertificateChain(forAlias: String?) = + arrayOf(public).takeIf { forAlias == alias } + + override fun getPrivateKey(forAlias: String?) = + private.takeIf { forAlias == alias } + }, trustManager) + + /* known client cert test URLs (thanks!): + * - https://prod.idrix.eu/secure/ + * - https://server.cryptomix.com/secure/ + */ + val client = OkHttpClient.Builder() + .sslSocketFactory(factory, trustManager) + .build() + client.newCall(Request.Builder() + .get() + .url("https://prod.idrix.eu/secure/") + .build()).execute().use { response -> + assertTrue(response.isSuccessful) + assertTrue(response.body()!!.string().contains("CN=User Cert,O=Internet Widgits Pty Ltd,ST=Some-State,C=CA")) + } + } + + @Test + fun testUpgradeTLS() { + val s = factory.createSocket(server.hostName, server.port) + assertTrue(s is SSLSocket) + val ssl = s as SSLSocket + + assertFalse(contains(ssl.enabledProtocols, "SSLv3")) + assertTrue(contains(ssl.enabledProtocols, "TLSv1")) + assertTrue(contains(ssl.enabledProtocols, "TLSv1.1")) + assertTrue(contains(ssl.enabledProtocols, "TLSv1.2")) + } + + + private fun readResource(name: String): ByteArray { + javaClass.classLoader!!.getResourceAsStream(name).use { + return IOUtils.toByteArray(it) + } + } + +} diff --git a/src/androidTest/resources/sample.crt b/src/androidTest/resources/sample.crt new file mode 100644 index 0000000..c813980 --- /dev/null +++ b/src/androidTest/resources/sample.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEzCCAfsCAQEwDQYJKoZIhvcNAQEFBQAwRjELMAkGA1UEBhMCQ0ExEzARBgNV +BAgMClNvbWUtU3RhdGUxEDAOBgNVBAoMB0NBIENlcnQxEDAOBgNVBAMMB0NBIENl +cnQwHhcNMTgwMTEzMjAyOTI5WhcNMTkwMTEzMjAyOTI5WjBZMQswCQYDVQQGEwJD +QTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRIwEAYDVQQDDAlVc2VyIENlcnQwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDqOyHAeG4psE/f6i/eTfwbhn6j7WaFXxZiSOWwpQZmzRrx +MrfkABJCk0X7KNgCaJcmBkG9G1Ri4HfKrxvJFswMXknlq+0ulGBk7oDnZM+pihuX +3D9VCWMMkCqYhLCGADj2zB2mkX4LpcMRi6XoOetKURE/vcIy7rSLAtJM6ZRdftfh +2ZxnautS1Tyujh9Au3NI/+Of80tT/nA+oBJQeT1fB/ga1OQlZP5kjSaA7IPiIbTz +QBO+r898MvqK/lwsvOYnWAp7TY03z+vPfCs0zjijZEl9Wrl0hW6o5db5kU1v5bcr +p87hxFJsGD2HIr2y6kvYfL2hn+h9iANyYdRnUgapAgMBAAEwDQYJKoZIhvcNAQEF +BQADggIBAHANsiJITedXPyp89lVMEmGY3zKtOqgQ3tqjvjlNt2sdPnj7wmZbmrNd +sa90S/UwOn8PzEFOVxYy1BPlljlEjtjmc4OHMcm4P4Zv36uawHilmK8V+zT59gCK +ftB5FP2TLFUFi2X9o8J06d0xJRE77uewN155NV4RmPuP4b/tMmeixoQppHqLqEr5 +lgEUnt3Mh1ctmeFQFJR6lJ01hlB0gdpVHIhzrVLTO3uo8ePLJTmxP6tyKl/HXj9F +mpVsKb1kriKwbkGczfw99OUZeUVbTwQOR07r0SrG71B7IuDvxIORnhQc1OUjt7ob +wjdaZauAHxpGBRu+hw9Yqaxchk9Gldy1nEjGyyVCD0FU5taXbl8PhBWEDc4U9tI+ +xVNmPpsSuCsbz3Mjd1YIVRGL99vLrKsQcj+TNM+jJKKRKes3ihl+l/0FwG6UuO7L +EvjlUg5hOtYi1D7xuYyMjroGBGh7swYMt6w4eCDbcjzcCkaCi0H2pScM/rLBpDjS +LIoGCvZ1LBdi933/iOj1/8dxGZwY6fEgcyiD2n0xAgYIniLWjEZXOMdIK5FNTNga +Tswanvp+6Noa4oIu/hl/LXvPMsouaWfSEbRe0Dshi3GpLj3YtEHoN9DHB8bn7jy5 +34By81GT41m5kq3hWP//x9kSHYSADpbovCbKbElU1qSt6vTVR4nq +-----END CERTIFICATE----- diff --git a/src/androidTest/resources/sample.key b/src/androidTest/resources/sample.key new file mode 100644 index 0000000000000000000000000000000000000000..af8f903d41f5948cda0eb46c3f471d2ed013a099 GIT binary patch literal 1220 zcmXqLVmZLX$Y8+B#;Mij(e|B}k&%&=fu)IMl|d8BGA2d_rY1&4hF8{#2P*P3H~8Ow zrGL-&k91qz;;IFl9_vV{_aq9g_5*{M35s zlVWFh;yj1qh&)IgT=QqvdbpcaW6eA*uC_H zdF(>#{v{rMWBUtRxVboM+`kKn5I?KMCD`h1PH$vKO~ zDW0`aJ4;&gRy@7-bE0ql)9u>J&pkX6lp|r=uC#a4EAJaMdl$}sQQN^>lz1gQh;1bk zGa~~dI3(6+N%X!pH+}9JH`U^W&=#%RmV5G)_psF`6|AUUnsCfej=OU^+nk{Di`wee ztiKqWYW(VWrgBbBEW7`Q;D^kMS8iXCoL#hdrkbqg-~Q=gH+cB6B(J609}^UNm0`Dh zZQ8%`3y0+q9zT#S$ zLi6Ph_gWNrn!o0peMQ<`Qcm{+^M|doS!PSSrWWsb;IlO}lw~sC%bo&$U%vBOvPF8l z)XeOrGw%O$CcIT*^5Ho@`A+;j+9~_e`mMq9h=h3Pm%fT_kAy!`7&y$tZmtq!(YiU<;1qb%G&1@>^7T~ zH8QcDIv00TCvmln>)lv%NGl2wjh zv32%ndUWxM9#8P$r1XU#H#E-kS$aC$OlyI&E#ul-wZ?X9o@U?n)W0)F=daGMyNseM zx8FL|o^$X3*RNSCYRkG<^p~gz>|Jbdvsq!qt-DDKQa4f~Zq&@$kS3hRRa5chVrf~N zgtSy~=S0;-T{e?$T{M#T&gj^)tSsMSwPyFxR|->d{w?p&UnD7|zA_KZzVG2sr0miQ9XZ`$u0?g`02t&y$9;&_&@ucG@b5O|4zom|F0p_*Xols z>lV0LJX!INcl(W(hn{3L3$LVbb)ovPs&!694!MA@a^9k?#&4yteBU{pn>rt*;-)fe_y_>yKuLW7 literal 0 HcmV?d00001 -- GitLab From a24d07c52c18ad573aff5560f9dd9e74cb32c09a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 11 Mar 2019 11:57:17 +0100 Subject: [PATCH 11/37] Update gradle plugin, avoid ConcurrentModificationException --- project.properties | 3 --- .../java/at/bitfire/cert4android/CustomCertService.kt | 8 ++------ 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 project.properties diff --git a/project.properties b/project.properties deleted file mode 100644 index 2e788c3..0000000 --- a/project.properties +++ /dev/null @@ -1,3 +0,0 @@ - -android.library=true - diff --git a/src/main/java/at/bitfire/cert4android/CustomCertService.kt b/src/main/java/at/bitfire/cert4android/CustomCertService.kt index f07e020..b0d1c41 100644 --- a/src/main/java/at/bitfire/cert4android/CustomCertService.kt +++ b/src/main/java/at/bitfire/cert4android/CustomCertService.kt @@ -172,7 +172,7 @@ class CustomCertService: Service() { // bound service - val binder = object: ICustomCertService.Stub() { + private val binder = object: ICustomCertService.Stub() { override fun checkTrusted(raw: ByteArray, interactive: Boolean, foreground: Boolean, callback: IOnCertificateDecision) { val cert: X509Certificate? = try { @@ -246,11 +246,7 @@ class CustomCertService: Service() { override fun abortCheck(callback: IOnCertificateDecision) { for ((cert, list) in pendingDecisions) { - val it = list.listIterator() - while (it.hasNext()) - if (it.next() == callback) - it.remove() - + list.removeAll { it == callback } if (list.isEmpty()) pendingDecisions -= cert } -- GitLab From 0b01d3740aafd5a0f07bfecdd42cb7de2596af2c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 16 Mar 2019 14:00:17 +0100 Subject: [PATCH 12/37] Use ViewModel/data binding for TrustCertificateActivity --- build.gradle | 9 +- .../cert4android/TrustCertificateActivity.kt | 141 ++++++----- .../res/layout/activity_trust_certificate.xml | 234 +++++++++--------- 3 files changed, 207 insertions(+), 177 deletions(-) diff --git a/build.gradle b/build.gradle index 9415182..4a5e279 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:3.3.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}" } @@ -33,6 +33,8 @@ android { targetSdkVersion 28 } + dataBinding.enabled = true + lintOptions { disable 'MissingTranslation', 'ExtraTranslation' // translations from Transifex are not always up to date disable "OnClick" // doesn't recognize Kotlin onClick methods @@ -48,10 +50,13 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'androidx.lifecycle:lifecycle-livedata:2.0.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' - androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.2' androidTestImplementation 'commons-io:commons-io:2.6' androidTestImplementation 'org.apache.commons:commons-lang3:3.8.1' diff --git a/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt b/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt index 5b2693e..f8bfe0f 100644 --- a/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt +++ b/src/main/java/at/bitfire/cert4android/TrustCertificateActivity.kt @@ -11,84 +11,48 @@ package at.bitfire.cert4android import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.Button -import android.widget.CheckBox -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProviders +import at.bitfire.cert4android.databinding.ActivityTrustCertificateBinding import java.io.ByteArrayInputStream import java.security.MessageDigest import java.security.cert.CertificateFactory import java.security.cert.CertificateParsingException import java.security.cert.X509Certificate +import java.security.spec.MGF1ParameterSpec.SHA1 +import java.security.spec.MGF1ParameterSpec.SHA256 import java.text.DateFormat import java.util.* import java.util.logging.Level +import kotlin.concurrent.thread class TrustCertificateActivity: AppCompatActivity() { companion object { const val EXTRA_CERTIFICATE = "certificate" - - val certFactory = CertificateFactory.getInstance("X.509")!! } + private lateinit var model: Model override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_trust_certificate) - showCertificate() + model = ViewModelProviders.of(this).get(Model::class.java) + model.processIntent(intent) + + val binding = DataBindingUtil.setContentView(this, R.layout.activity_trust_certificate) + binding.lifecycleOwner = this + binding.model = model } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - this.intent = intent - showCertificate() + model.processIntent(intent) } - private fun showCertificate() { - val raw = intent.getByteArrayExtra(EXTRA_CERTIFICATE) - (certFactory.generateCertificate(ByteArrayInputStream(raw)) as X509Certificate?)?.let { cert -> - val subject: String - try { - subject = if (cert.issuerAlternativeNames != null) { - val sb = StringBuilder() - for (altName in cert.subjectAlternativeNames.orEmpty()) { - val name = altName[1] - if (name is String) - sb.append("[").append(altName[0]).append("]").append(name).append(" ") - } - sb.toString() - } else - cert.subjectDN.name - - var tv = findViewById(R.id.issuedFor) - tv.text = subject - - tv = findViewById(R.id.issuedBy) - tv.text = cert.issuerDN.toString() - - val formatter = DateFormat.getDateInstance(DateFormat.LONG) - tv = findViewById(R.id.validity_period) - tv.text = getString(R.string.trust_certificate_validity_period_value, - formatter.format(cert.notBefore), - formatter.format(cert.notAfter)) - - tv = findViewById(R.id.fingerprint_sha1) - tv.text = fingerprint(cert, "SHA-1") - tv = findViewById(R.id.fingerprint_sha256) - tv.text = fingerprint(cert, "SHA-256") - } catch(e: CertificateParsingException) { - Constants.log.log(Level.WARNING, "Couldn't parse certificate", e) - } - } - - val btnAccept = findViewById