From 4f05b3406f7b4fac5ba341d7e28b9d4dd950c608 Mon Sep 17 00:00:00 2001 From: Thomas Moulin Date: Fri, 30 Jan 2026 15:25:12 -0500 Subject: [PATCH 1/4] implent mtls authentication --- .../ui/activities/MainActivity.kt | 3 + .../ui/components/LoginView.kt | 52 +++++++++++++++++- .../ui/components/NCPSettings.kt | 2 + .../ui/components/Settings.kt | 30 ++++++++++ .../nextcloudpasswords/utils/OkHttpRequest.kt | 55 +++++++++++++++++-- .../utils/PreferencesManager.kt | 4 ++ app/src/main/res/values/strings.xml | 5 ++ 7 files changed, 146 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt index f27cb1b5..1cd46914 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt @@ -36,6 +36,9 @@ class MainActivity : FragmentActivity() { if (BuildConfig.DEBUG) LogHelper.getInstance() super.onCreate(savedInstanceState) + + OkHttpRequest.getInstance().initClient(this) + if (!UserController.getInstance(this).isLoggedIn) { login() return diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt index 7da37624..4932e773 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.http.SslError +import android.security.KeyChain +import android.security.KeyChainAliasCallback import android.view.View import android.view.ViewGroup import android.webkit.SslErrorHandler @@ -62,6 +64,12 @@ import androidx.compose.ui.viewinterop.AndroidView import com.hegocre.nextcloudpasswords.R import com.hegocre.nextcloudpasswords.ui.theme.NextcloudPasswordsTheme import com.hegocre.nextcloudpasswords.utils.PreferencesManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.security.PrivateKey +import java.security.cert.X509Certificate +import android.webkit.ClientCertRequest @Composable fun NCPLoginScreen( @@ -207,6 +215,8 @@ fun LoginCard( onDone = onLoginButtonClick ) + val context = LocalContext.current + Button( modifier = Modifier.align(Alignment.End), onClick = onLoginButtonClick @@ -281,9 +291,49 @@ fun NCPWebLoginScreen( super.onReceivedSslError(view, handler, error) } } +// ...existing code... + override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest?) { + val alias = PreferencesManager.getInstance(context).getClientCertAlias() + if (alias != null) { + GlobalScope.launch(Dispatchers.IO) { + try { + val privateKey = KeyChain.getPrivateKey(context, alias) + val chain = KeyChain.getCertificateChain(context, alias) + request?.proceed(privateKey, chain) + } catch (e: Exception) { + request?.cancel() + } + } + } else { + KeyChain.choosePrivateKeyAlias( + context as Activity, + { selectedAlias -> + if (selectedAlias != null) { + PreferencesManager.getInstance(context).setClientCertAlias(selectedAlias) + // Also init OkHttp client for future API requests + com.hegocre.nextcloudpasswords.utils.OkHttpRequest.getInstance().initClient(context) + + GlobalScope.launch(Dispatchers.IO) { + try { + val privateKey = KeyChain.getPrivateKey(context, selectedAlias) + val chain = KeyChain.getCertificateChain(context, selectedAlias) + request?.proceed(privateKey, chain) + } catch (e: Exception) { + request?.cancel() + } + } + } else { + request?.cancel() + } + }, + request?.keyTypes, request?.principals, request?.host, request?.port ?: -1, + null + ) + } + } } } - +// ...existing code... val (loadingProgress, setLoadingProgress) = remember { mutableIntStateOf(0) } Scaffold( diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt index ed03dde3..4a900e3d 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt @@ -378,6 +378,7 @@ fun NCPSettingsScreen( } showDeletePasscodeDialog = false }, + onDismissRequest = { showDeletePasscodeDialog = false } @@ -385,6 +386,7 @@ fun NCPSettingsScreen( } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val autofillManager = context.getSystemService(AutofillManager::class.java) var autofillEnabled by remember { mutableStateOf(autofillManager.hasEnabledAutofillServices()) } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt index f31b8c99..157ab2f7 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt @@ -127,6 +127,36 @@ fun ListPreference( } } +@Composable +fun ClickablePreference( + onClick: () -> Unit, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + subtitle: (@Composable () -> Unit)? = null, + enabled: Boolean = true +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { if (enabled) onClick() } + .padding(vertical = 12.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + Modifier.weight(1f) + ) { + title() + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontSize = 13.sp) + ) { + subtitle?.let { + it() + } + } + } + } +} + @Preview @Composable fun PreferencesPreview() { diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt index f299f430..82281a6a 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt @@ -1,6 +1,14 @@ package com.hegocre.nextcloudpasswords.utils import android.annotation.SuppressLint +import android.content.Context +import android.security.KeyChain +import java.security.KeyStore +import java.security.cert.X509Certificate +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager import okhttp3.Credentials import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -11,10 +19,7 @@ import okhttp3.Response import java.io.IOException import java.net.MalformedURLException import java.net.URL -import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.X509TrustManager /** * Class to manage the [OkHttpRequest] requests, and make them using always the same client, as suggested @@ -24,7 +29,7 @@ import javax.net.ssl.X509TrustManager class OkHttpRequest private constructor() { var allowInsecureRequests = false - private val secureClient = OkHttpClient.Builder() + private var secureClient = OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() @@ -56,6 +61,48 @@ class OkHttpRequest private constructor() { .build() } + fun initClient(context: Context) { + val alias = PreferencesManager.getInstance(context).getClientCertAlias() + + if (alias == null) { + secureClient = OkHttpClient.Builder() + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + return + } + + Thread { + try { + val privateKey = KeyChain.getPrivateKey(context, alias) + val chain = KeyChain.getCertificateChain(context, alias) + + if (privateKey != null && chain != null) { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null, null) + keyStore.setKeyEntry(alias, privateKey, null, chain) + + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(keyStore, null) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(kmf.keyManagers, null, null) + + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + tmf.init(null as KeyStore?) + val trustManagers = tmf.trustManagers + val x509TrustManager = trustManagers.first { it is X509TrustManager } as X509TrustManager + + secureClient = secureClient.newBuilder() + .sslSocketFactory(sslContext.socketFactory, x509TrustManager) + .build() + } + } catch (e: Exception) { + e.printStackTrace() + } + }.start() + } + @Throws( MalformedURLException::class, diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt index 60972e56..550a6106 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt @@ -90,6 +90,10 @@ class PreferencesManager private constructor(context: Context) { fun setMasterPassword(value: String?): Boolean = _encryptedSharedPrefs.edit().putString("MASTER_KEY", value).commit() + fun getClientCertAlias(): String? = _encryptedSharedPrefs.getString("CLIENT_CERT_ALIAS", null) + fun setClientCertAlias(value: String?): Boolean = + _encryptedSharedPrefs.edit().putString("CLIENT_CERT_ALIAS", value).commit() + fun getCSEv1Keychain(): String? = _encryptedSharedPrefs.getString("CSE_V1_KEYCHAIN", null) fun setCSEv1Keychain(value: String?): Boolean = _encryptedSharedPrefs.edit().putString("CSE_V1_KEYCHAIN", value).commit() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c5ab6ce..762da94d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,4 +130,9 @@ Oldest first Newest first Order content by + Client Certificate + Manage client certificate for mTLS + Clear client certificate + Client certificate cleared + Select Client Certificate \ No newline at end of file From 6825df53f9fcf61591a4e5b4ed45f4bac04ffe63 Mon Sep 17 00:00:00 2001 From: Thomas Moulin Date: Fri, 30 Jan 2026 15:37:37 -0500 Subject: [PATCH 2/4] remember which certificate to use --- .../ui/components/LoginView.kt | 15 ++++++++----- .../nextcloudpasswords/utils/OkHttpRequest.kt | 21 ++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt index 4932e773..dbc1259b 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt @@ -291,7 +291,7 @@ fun NCPWebLoginScreen( super.onReceivedSslError(view, handler, error) } } -// ...existing code... + override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest?) { val alias = PreferencesManager.getInstance(context).getClientCertAlias() if (alias != null) { @@ -299,18 +299,22 @@ fun NCPWebLoginScreen( try { val privateKey = KeyChain.getPrivateKey(context, alias) val chain = KeyChain.getCertificateChain(context, alias) - request?.proceed(privateKey, chain) + if (privateKey != null && chain != null) { + request?.proceed(privateKey, chain) + } else { + PreferencesManager.getInstance(context).setClientCertAlias(null) + request?.cancel() + } } catch (e: Exception) { request?.cancel() } } } else { - KeyChain.choosePrivateKeyAlias( + KeyChain.choosePrivateKeyAlias( context as Activity, { selectedAlias -> if (selectedAlias != null) { PreferencesManager.getInstance(context).setClientCertAlias(selectedAlias) - // Also init OkHttp client for future API requests com.hegocre.nextcloudpasswords.utils.OkHttpRequest.getInstance().initClient(context) GlobalScope.launch(Dispatchers.IO) { @@ -333,8 +337,9 @@ fun NCPWebLoginScreen( } } } -// ...existing code... + val (loadingProgress, setLoadingProgress) = remember { mutableIntStateOf(0) } +// ...existing code... Scaffold( modifier = modifier, diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt index 82281a6a..2b13534a 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt @@ -28,6 +28,8 @@ import java.util.concurrent.TimeUnit */ class OkHttpRequest private constructor() { var allowInsecureRequests = false + private val initLock = java.lang.Object() + @Volatile private var initializing = false private var secureClient = OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) @@ -36,7 +38,18 @@ class OkHttpRequest private constructor() { private val insecureClient: OkHttpClient val client: OkHttpClient - get() = if (allowInsecureRequests) insecureClient else secureClient + get() { + if (allowInsecureRequests) return insecureClient + + if (initializing) { + synchronized(initLock) { + while (initializing) { + try { initLock.wait() } catch (_: InterruptedException) {} + } + } + } + return secureClient + } init { val insecureTrustManager = @SuppressLint("CustomX509TrustManager") @@ -72,6 +85,7 @@ class OkHttpRequest private constructor() { return } + initializing = true Thread { try { val privateKey = KeyChain.getPrivateKey(context, alias) @@ -99,6 +113,11 @@ class OkHttpRequest private constructor() { } } catch (e: Exception) { e.printStackTrace() + } finally { + initializing = false + synchronized(initLock) { + initLock.notifyAll() + } } }.start() } From 33f33557c1231c33957a168ff22f5fbea7703b1b Mon Sep 17 00:00:00 2001 From: Thomas Moulin Date: Sat, 31 Jan 2026 21:42:52 -0500 Subject: [PATCH 3/4] remove unnecessary changes --- .../ui/activities/MainActivity.kt | 1 + .../ui/components/LoginView.kt | 3 -- .../ui/components/NCPSettings.kt | 2 -- .../ui/components/Settings.kt | 30 ------------------- 4 files changed, 1 insertion(+), 35 deletions(-) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt index 1cd46914..fa9bf277 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt @@ -37,6 +37,7 @@ class MainActivity : FragmentActivity() { super.onCreate(savedInstanceState) + // Initialize OkHttp client with the previously selected client certificate if available, so it persists across app restarts. OkHttpRequest.getInstance().initClient(this) if (!UserController.getInstance(this).isLoggedIn) { diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt index dbc1259b..ae8f396f 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt @@ -215,8 +215,6 @@ fun LoginCard( onDone = onLoginButtonClick ) - val context = LocalContext.current - Button( modifier = Modifier.align(Alignment.End), onClick = onLoginButtonClick @@ -339,7 +337,6 @@ fun NCPWebLoginScreen( } val (loadingProgress, setLoadingProgress) = remember { mutableIntStateOf(0) } -// ...existing code... Scaffold( modifier = modifier, diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt index 4a900e3d..ed03dde3 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPSettings.kt @@ -378,7 +378,6 @@ fun NCPSettingsScreen( } showDeletePasscodeDialog = false }, - onDismissRequest = { showDeletePasscodeDialog = false } @@ -386,7 +385,6 @@ fun NCPSettingsScreen( } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val autofillManager = context.getSystemService(AutofillManager::class.java) var autofillEnabled by remember { mutableStateOf(autofillManager.hasEnabledAutofillServices()) } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt index 157ab2f7..f31b8c99 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/Settings.kt @@ -127,36 +127,6 @@ fun ListPreference( } } -@Composable -fun ClickablePreference( - onClick: () -> Unit, - title: @Composable () -> Unit, - modifier: Modifier = Modifier, - subtitle: (@Composable () -> Unit)? = null, - enabled: Boolean = true -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { if (enabled) onClick() } - .padding(vertical = 12.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - Modifier.weight(1f) - ) { - title() - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontSize = 13.sp) - ) { - subtitle?.let { - it() - } - } - } - } -} - @Preview @Composable fun PreferencesPreview() { From 6403d2c425109c041909441a636ca062397d1437 Mon Sep 17 00:00:00 2001 From: Thomas Moulin Date: Sun, 1 Feb 2026 17:29:03 -0500 Subject: [PATCH 4/4] Fix: changes regarding copilot code review --- .../ui/activities/MainActivity.kt | 7 +- .../ui/components/LoginView.kt | 79 +++++++++++++------ .../nextcloudpasswords/utils/OkHttpRequest.kt | 29 +++++-- 3 files changed, 78 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt index fa9bf277..25642cb4 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/MainActivity.kt @@ -37,14 +37,15 @@ class MainActivity : FragmentActivity() { super.onCreate(savedInstanceState) - // Initialize OkHttp client with the previously selected client certificate if available, so it persists across app restarts. - OkHttpRequest.getInstance().initClient(this) - if (!UserController.getInstance(this).isLoggedIn) { login() return } + // Initialize OkHttp client with the previously selected client certificate if available, so it persists across app restarts. + // Moved after login check to avoid unnecessary initialization if redirecting. + OkHttpRequest.getInstance().initClient(this) + val passwordsViewModel by viewModels() val autofillRequested = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt index ae8f396f..64dbf778 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/LoginView.kt @@ -2,10 +2,10 @@ package com.hegocre.nextcloudpasswords.ui.components import android.annotation.SuppressLint import android.app.Activity +import android.content.Context import android.content.Intent import android.net.http.SslError import android.security.KeyChain -import android.security.KeyChainAliasCallback import android.view.View import android.view.ViewGroup import android.webkit.SslErrorHandler @@ -48,6 +48,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -65,10 +66,7 @@ import com.hegocre.nextcloudpasswords.R import com.hegocre.nextcloudpasswords.ui.theme.NextcloudPasswordsTheme import com.hegocre.nextcloudpasswords.utils.PreferencesManager import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.security.PrivateKey -import java.security.cert.X509Certificate import android.webkit.ClientCertRequest @Composable @@ -235,6 +233,7 @@ fun NCPWebLoginScreen( ) { NextcloudPasswordsTheme { val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() var showTlsDialog by rememberSaveable { mutableStateOf(false) } @@ -293,7 +292,7 @@ fun NCPWebLoginScreen( override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest?) { val alias = PreferencesManager.getInstance(context).getClientCertAlias() if (alias != null) { - GlobalScope.launch(Dispatchers.IO) { + coroutineScope.launch(Dispatchers.IO) { try { val privateKey = KeyChain.getPrivateKey(context, alias) val chain = KeyChain.getCertificateChain(context, alias) @@ -304,33 +303,50 @@ fun NCPWebLoginScreen( request?.cancel() } } catch (e: Exception) { + PreferencesManager.getInstance(context).setClientCertAlias(null) request?.cancel() } } } else { - KeyChain.choosePrivateKeyAlias( - context as Activity, - { selectedAlias -> - if (selectedAlias != null) { - PreferencesManager.getInstance(context).setClientCertAlias(selectedAlias) - com.hegocre.nextcloudpasswords.utils.OkHttpRequest.getInstance().initClient(context) - - GlobalScope.launch(Dispatchers.IO) { - try { - val privateKey = KeyChain.getPrivateKey(context, selectedAlias) - val chain = KeyChain.getCertificateChain(context, selectedAlias) - request?.proceed(privateKey, chain) - } catch (e: Exception) { - request?.cancel() + val activity = context.findActivity() + if (activity != null) { + KeyChain.choosePrivateKeyAlias( + activity, + { selectedAlias -> + if (selectedAlias != null) { + PreferencesManager.getInstance(context) + .setClientCertAlias(selectedAlias) + com.hegocre.nextcloudpasswords.utils.OkHttpRequest.getInstance() + .initClient(context) + + coroutineScope.launch(Dispatchers.IO) { + try { + val privateKey = + KeyChain.getPrivateKey(context, selectedAlias) + val chain = KeyChain.getCertificateChain( + context, + selectedAlias + ) + request?.proceed(privateKey, chain) + } catch (e: Exception) { + PreferencesManager.getInstance(context) + .setClientCertAlias(null) + request?.cancel() + } } + } else { + request?.cancel() } - } else { - request?.cancel() - } - }, - request?.keyTypes, request?.principals, request?.host, request?.port ?: -1, - null - ) + }, + request?.keyTypes, + request?.principals, + request?.host, + request?.port ?: -1, + null + ) + } else { + request?.cancel() + } } } } @@ -434,3 +450,14 @@ fun PreviewCard() { LoginCard("", {}, "") {} } } + +private fun Context.findActivity(): Activity? { + var currentContext = this + while (currentContext is android.content.ContextWrapper) { + if (currentContext is Activity) { + return currentContext + } + currentContext = currentContext.baseContext + } + return null +} diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt index 2b13534a..de486cf6 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/OkHttpRequest.kt @@ -2,6 +2,7 @@ package com.hegocre.nextcloudpasswords.utils import android.annotation.SuppressLint import android.content.Context +import android.util.Log import android.security.KeyChain import java.security.KeyStore import java.security.cert.X509Certificate @@ -20,6 +21,9 @@ import java.io.IOException import java.net.MalformedURLException import java.net.URL import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch /** * Class to manage the [OkHttpRequest] requests, and make them using always the same client, as suggested @@ -78,15 +82,21 @@ class OkHttpRequest private constructor() { val alias = PreferencesManager.getInstance(context).getClientCertAlias() if (alias == null) { - secureClient = OkHttpClient.Builder() + val newClient = OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() + + synchronized(initLock) { + secureClient = newClient + initializing = false + initLock.notifyAll() + } return } initializing = true - Thread { + CoroutineScope(Dispatchers.IO).launch { try { val privateKey = KeyChain.getPrivateKey(context, alias) val chain = KeyChain.getCertificateChain(context, alias) @@ -105,21 +115,24 @@ class OkHttpRequest private constructor() { val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) tmf.init(null as KeyStore?) val trustManagers = tmf.trustManagers - val x509TrustManager = trustManagers.first { it is X509TrustManager } as X509TrustManager + val x509TrustManager = trustManagers.firstOrNull { it is X509TrustManager } as? X509TrustManager - secureClient = secureClient.newBuilder() - .sslSocketFactory(sslContext.socketFactory, x509TrustManager) - .build() + if (x509TrustManager != null) { + secureClient = secureClient.newBuilder() + .sslSocketFactory(sslContext.socketFactory, x509TrustManager) + .build() + } } } catch (e: Exception) { - e.printStackTrace() + Log.e("OkHttpRequest", "Failed to initialize SSL context with client certificate alias: $alias", e) + PreferencesManager.getInstance(context).setClientCertAlias(null) } finally { initializing = false synchronized(initLock) { initLock.notifyAll() } } - }.start() + } }