From edd15ee8f8d3384abf6d6ab0a6bcf88d307fff36 Mon Sep 17 00:00:00 2001 From: Voczi Date: Tue, 25 Jun 2024 12:32:50 +0200 Subject: [PATCH] Add support for custom certificate (https://github.com/ankitects/anki/commit/9e3a34f17fb18e89853d2b995df2a049294495d4) --- .../java/com/ichi2/anki/CollectionManager.kt | 25 +++++++++++++++++++ .../src/main/java/com/ichi2/anki/Sync.kt | 6 +++++ .../CustomSyncServerSettingsFragment.kt | 21 ++++++++++++++++ AnkiDroid/src/main/res/values/03-dialogs.xml | 3 +++ .../src/main/res/values/10-preferences.xml | 1 + AnkiDroid/src/main/res/values/preferences.xml | 1 + .../xml/preferences_custom_sync_server.xml | 4 +++ .../analytics/PreferencesAnalyticsTest.kt | 3 ++- 8 files changed, 63 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt index 2632827a9e0d..e916c7ae56cd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt @@ -66,6 +66,8 @@ object CollectionManager { private val testMutex = ReentrantLock() + private var currentSyncCertificate: String = "" + /** * Execute the provided block on a serial background queue, to ensure * concurrent access does not happen. @@ -400,4 +402,27 @@ object CollectionManager { // as it does not seem to be compatible with the test scheduler queue = dispatcher } + + /** + * Update the custom TLS certificate used in the backend for its requests to the sync server. + * + * If the cert parameter hasn't changed from the cached sync certificate, then just return true. + * Otherwise, set the custom certificate in the backend and get the success value. + * + * If cert was a valid certificate, then cache it in currentSyncCertificate and return true. + * Otherwise, return false to indicate that a custom sync certificate was not applied. + * + * Passing in an empty string unsets any custom sync certificate in the backend. + */ + fun updateCustomCertificate(cert: String): Boolean { + if (cert == currentSyncCertificate) { + return true + } + + return getBackend().setCustomCertificate(cert).apply { + if (this) { + currentSyncCertificate = cert + } + } + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt index 3b587982092f..861e7b469a21 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt @@ -62,6 +62,7 @@ object SyncPreferences { const val CURRENT_SYNC_URI = "currentSyncUri" const val CUSTOM_SYNC_URI = "syncBaseUrl" const val CUSTOM_SYNC_ENABLED = CUSTOM_SYNC_URI + VersatileTextWithASwitchPreference.SWITCH_SUFFIX + const val CUSTOM_SYNC_CERTIFICATE = "customSyncCertificate" // Used in the legacy schema path const val HOSTNUM = "hostNum" @@ -79,6 +80,11 @@ interface SyncCompletionListener { fun DeckPicker.syncAuth(): SyncAuth? { val preferences = this.sharedPrefs() + + // Grab custom sync certificate from preferences (default is the empty string) and set it in CollectionManager + val currentSyncCertificate = preferences.getString(SyncPreferences.CUSTOM_SYNC_CERTIFICATE, "") ?: "" + CollectionManager.updateCustomCertificate(currentSyncCertificate) + val hkey = preferences.getString(SyncPreferences.HKEY, null) val resolvedEndpoint = getEndpoint(this) return hkey?.let { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/CustomSyncServerSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/CustomSyncServerSettingsFragment.kt index 1ee2b7d8d4e0..26e334f7d606 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/CustomSyncServerSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/CustomSyncServerSettingsFragment.kt @@ -17,10 +17,14 @@ package com.ichi2.anki.preferences import android.content.SharedPreferences import android.os.Bundle +import androidx.appcompat.app.AlertDialog import androidx.core.content.edit +import com.ichi2.anki.CollectionManager import com.ichi2.anki.R import com.ichi2.anki.SyncPreferences +import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.preferences.VersatileTextPreference +import com.ichi2.utils.show import okhttp3.HttpUrl.Companion.toHttpUrl class CustomSyncServerSettingsFragment : SettingsFragment() { @@ -36,6 +40,23 @@ class CustomSyncServerSettingsFragment : SettingsFragment() { if (value.isNotEmpty()) value.toHttpUrl() } } + + requirePreference(R.string.custom_sync_certificate_key).setOnPreferenceChangeListener { _, newValue: Any? -> + val newCert = newValue as String + + // Empty string input causes the certificate to be unset in the backend, i.e., no error + if (!CollectionManager.updateCustomCertificate(newCert)) { + AlertDialog.Builder(requireContext()).show { + setTitle(R.string.dialog_invalid_custom_certificate_title) + setMessage(R.string.dialog_invalid_custom_certificate) + setPositiveButton(R.string.dialog_ok) { _, _ -> } + } + return@setOnPreferenceChangeListener false + } + + showSnackbar(R.string.dialog_updated_custom_certificate) + true + } } // See discussion at https://github.com/ankidroid/Anki-Android/pull/12367#discussion_r967681337 diff --git a/AnkiDroid/src/main/res/values/03-dialogs.xml b/AnkiDroid/src/main/res/values/03-dialogs.xml index dcb26acbb988..97eb55b84f46 100644 --- a/AnkiDroid/src/main/res/values/03-dialogs.xml +++ b/AnkiDroid/src/main/res/values/03-dialogs.xml @@ -90,6 +90,9 @@ Replace The path specified wasn’t a valid directory + The provided text does not resolve to a valid PEM-encoded X509 certificate + Error parsing certificate + Certificate updated Check media? diff --git a/AnkiDroid/src/main/res/values/10-preferences.xml b/AnkiDroid/src/main/res/values/10-preferences.xml index bc8858e342be..61da788a90db 100644 --- a/AnkiDroid/src/main/res/values/10-preferences.xml +++ b/AnkiDroid/src/main/res/values/10-preferences.xml @@ -201,6 +201,7 @@ Learn more ]]> Sync url + Custom root certificate (PEM) Keyboard Bluetooth diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml index a6ebc9475561..e3e62a675ff2 100644 --- a/AnkiDroid/src/main/res/values/preferences.xml +++ b/AnkiDroid/src/main/res/values/preferences.xml @@ -143,6 +143,7 @@ customSyncServerScreen syncBaseUrl + customSyncCertificate pref_screen_advanced category_workarounds diff --git a/AnkiDroid/src/main/res/xml/preferences_custom_sync_server.xml b/AnkiDroid/src/main/res/xml/preferences_custom_sync_server.xml index 8f1bcd158423..e5b191a81db9 100644 --- a/AnkiDroid/src/main/res/xml/preferences_custom_sync_server.xml +++ b/AnkiDroid/src/main/res/xml/preferences_custom_sync_server.xml @@ -16,4 +16,8 @@ android:title="@string/custom_sync_server_base_url_title" android:inputType="textUri" app:useSimpleSummaryProvider="true" /> + diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt index 3ae1b08a4ffe..7b996e381ed0 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt @@ -68,7 +68,8 @@ class PreferencesAnalyticsTest : RobolectricTest() { // potential personal data "syncAccount", "syncBaseUrl", - "language" + "language", + "customSyncCertificate" ) @Test