From 0c1cb3a309cc8dff9ba2bdfe64f4cf40c2d8e869 Mon Sep 17 00:00:00 2001 From: Corewala Date: Tue, 17 May 2022 13:45:54 -0400 Subject: [PATCH] Biometric verification for client certificates --- .../corewala/buran/io/gemini/Datasource.kt | 2 +- .../buran/io/gemini/GeminiDatasource.kt | 20 +++---- .../io/keymanager/BuranBiometricManager.kt | 11 +++- .../buran/io/keymanager/BuranKeyManager.kt | 9 ++- .../java/corewala/buran/ui/GemActivity.kt | 57 ++++++++++++++++++- .../java/corewala/buran/ui/GemViewModel.kt | 10 ++-- 6 files changed, 84 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/corewala/buran/io/gemini/Datasource.kt b/app/src/main/java/corewala/buran/io/gemini/Datasource.kt index 4c2c85f..0cc81f1 100644 --- a/app/src/main/java/corewala/buran/io/gemini/Datasource.kt +++ b/app/src/main/java/corewala/buran/io/gemini/Datasource.kt @@ -6,7 +6,7 @@ import corewala.buran.io.database.history.BuranHistory import java.net.URI interface Datasource { - fun request(address: String, forceDownload: Boolean, clientCertAllowed: Boolean, onUpdate: (state: GemState) -> Unit) + fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, onUpdate: (state: GemState) -> Unit) fun canGoBack(): Boolean fun goBack(onUpdate: (state: GemState) -> Unit) diff --git a/app/src/main/java/corewala/buran/io/gemini/GeminiDatasource.kt b/app/src/main/java/corewala/buran/io/gemini/GeminiDatasource.kt index 71f9e83..8995c36 100644 --- a/app/src/main/java/corewala/buran/io/gemini/GeminiDatasource.kt +++ b/app/src/main/java/corewala/buran/io/gemini/GeminiDatasource.kt @@ -33,7 +33,7 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory): private var socketFactory: SSLSocketFactory? = null - override fun request(address: String, forceDownload: Boolean, clientCertAllowed: Boolean, onUpdate: (state: GemState) -> Unit) { + override fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, onUpdate: (state: GemState) -> Unit) { this.forceDownload = forceDownload this.onUpdate = onUpdate @@ -43,29 +43,29 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory): onUpdate(GemState.Requesting(uri)) GlobalScope.launch { - geminiRequest(uri, onUpdate, clientCertAllowed) + geminiRequest(uri, onUpdate, clientCertPassword) } } - private fun initSSLFactory(protocol: String, clientCertAllowed: Boolean){ + private fun initSSLFactory(protocol: String, clientCertPassword: String?){ val sslContext = when (protocol) { "TLS_ALL" -> SSLContext.getInstance("TLS") else -> SSLContext.getInstance(protocol) } - sslContext.init(buranKeyManager.getFactory(clientCertAllowed)?.keyManagers, DummyTrustManager.get(), null) + sslContext.init(buranKeyManager.getFactory(clientCertPassword)?.keyManagers, DummyTrustManager.get(), null) socketFactory = sslContext.socketFactory } - private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit, clientCertAllowed: Boolean){ + private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit, clientCertPassword: String?){ val protocol = "TLS" val useClientCert = prefs.getBoolean(Buran.PREF_KEY_CLIENT_CERT_ACTIVE, false) //Update factory if operating mode has changed when { - socketFactory == null -> initSSLFactory(protocol!!, clientCertAllowed) - useClientCert && !buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!, clientCertAllowed) - !useClientCert && buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!, clientCertAllowed) + socketFactory == null -> initSSLFactory(protocol!!, clientCertPassword) + useClientCert && !buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!, clientCertPassword) + !useClientCert && buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!, clientCertPassword) } val socket: SSLSocket? @@ -122,7 +122,7 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory): when { header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header)) - header.code == GeminiResponse.REDIRECT -> request(resolve(uri.host, header.meta), false, false, onUpdate) + header.code == GeminiResponse.REDIRECT -> request(resolve(uri.host, header.meta), false, null, onUpdate) header.code == GeminiResponse.CLIENT_CERTIFICATE_REQUIRED -> onUpdate(GemState.ClientCertRequired(uri, header)) header.code != GeminiResponse.SUCCESS -> onUpdate(GemState.ResponseError(header)) header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, uri, header, onUpdate) @@ -228,7 +228,7 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory): override fun goBack(onUpdate: (state: GemState) -> Unit) { runtimeHistory.removeLast() - request(runtimeHistory.last().toString(), false, false, onUpdate) + request(runtimeHistory.last().toString(), false, null, onUpdate) } //This just forces the factory to rebuild before the next request diff --git a/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt b/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt index 0b8e0b5..1940748 100644 --- a/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt +++ b/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt @@ -1,6 +1,5 @@ package corewala.buran.io.keymanager -import android.app.Activity import android.content.Context import android.os.Build import android.security.keystore.KeyGenParameterSpec @@ -37,6 +36,16 @@ class BuranBiometricManager { .setNegativeButtonText(context.getString(R.string.cancel).toUpperCase()) .build() } + fun createBiometricPrompt(context: Context, activity: FragmentActivity, callback: BiometricPrompt.AuthenticationCallback){ + val executor = ContextCompat.getMainExecutor(context) + biometricPrompt = BiometricPrompt(activity, executor, callback) + promptInfo = BiometricPrompt.PromptInfo.Builder() + .setConfirmationRequired(false) + .setTitle(context.getString(R.string.confirm_your_identity)) + .setSubtitle(context.getString(R.string.use_biometric_unlock)) + .setNegativeButtonText(context.getString(R.string.cancel).toUpperCase()) + .build() + } fun authenticateToEncryptData() { val cipher = getCipher() diff --git a/app/src/main/java/corewala/buran/io/keymanager/BuranKeyManager.kt b/app/src/main/java/corewala/buran/io/keymanager/BuranKeyManager.kt index 28eb076..89247f3 100644 --- a/app/src/main/java/corewala/buran/io/keymanager/BuranKeyManager.kt +++ b/app/src/main/java/corewala/buran/io/keymanager/BuranKeyManager.kt @@ -19,23 +19,22 @@ class BuranKeyManager(val context: Context, val onKeyError: (error: String) -> U var lastCallUsedKey = false //If the user has a key loaded load it here - or else return null - fun getFactory(clientCertAllowed: Boolean): KeyManagerFactory? { + fun getFactory(clientCertPassword: String?): KeyManagerFactory? { val isClientCertActive = prefs.getBoolean(Buran.PREF_KEY_CLIENT_CERT_ACTIVE, false) return when { - isClientCertActive and clientCertAllowed -> { + isClientCertActive and (clientCertPassword != null) -> { lastCallUsedKey = true val keyStore: KeyStore = KeyStore.getInstance("pkcs12") val uriStr = prefs.getString(Buran.PREF_KEY_CLIENT_CERT_URI, "") - val password = prefs.getString(Buran.PREF_KEY_CLIENT_CERT_PASSWORD, "") val uri = Uri.parse(uriStr) try { context.contentResolver?.openInputStream(uri)?.use { try { - keyStore.load(it, password?.toCharArray()) + keyStore.load(it, clientCertPassword?.toCharArray()) val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("X509") - keyManagerFactory.init(keyStore, password?.toCharArray()) + keyManagerFactory.init(keyStore, clientCertPassword?.toCharArray()) return@use keyManagerFactory } catch (ioe: IOException) { onKeyError("${ioe.message}") diff --git a/app/src/main/java/corewala/buran/ui/GemActivity.kt b/app/src/main/java/corewala/buran/ui/GemActivity.kt index 0f08d15..6291792 100644 --- a/app/src/main/java/corewala/buran/ui/GemActivity.kt +++ b/app/src/main/java/corewala/buran/ui/GemActivity.kt @@ -20,6 +20,7 @@ import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.biometric.BiometricPrompt import androidx.browser.customtabs.CustomTabsIntent import androidx.databinding.DataBindingUtil import androidx.preference.PreferenceManager @@ -35,6 +36,7 @@ import corewala.buran.io.database.BuranDatabase import corewala.buran.io.database.bookmarks.BookmarksDatasource import corewala.buran.io.gemini.Datasource import corewala.buran.io.gemini.GeminiResponse +import corewala.buran.io.keymanager.BuranBiometricManager import corewala.buran.io.update.BuranUpdates import corewala.buran.ui.bookmarks.BookmarkDialog import corewala.buran.ui.bookmarks.BookmarksDialog @@ -72,7 +74,7 @@ class GemActivity : AppCompatActivity() { private val omniTerm = OmniTerm(object : OmniTerm.Listener { override fun request(address: String) { - model.request(address, false) + model.request(address, null) } override fun openExternal(address: String) = openExternalLink(address) @@ -416,7 +418,17 @@ class GemActivity : AppCompatActivity() { if(prefs.getString(Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE, null) != null){ builder .setPositiveButton(getString(R.string.use_client_certificate).toUpperCase()) { _, _ -> - model.request(state.uri.toString(), true) + if(prefs.getBoolean("use_biometrics", false)){ + biometricSecureRequest(state.uri.toString()) + }else{ + model.request( + state.uri.toString(), + prefs.getString( + Buran.PREF_KEY_CLIENT_CERT_PASSWORD, + null + ) + ) + } } .setNegativeButton(getString(R.string.cancel).toUpperCase()) { _, _ -> } .show() @@ -511,6 +523,45 @@ class GemActivity : AppCompatActivity() { } } + private fun biometricSecureRequest(address: String){ + val biometricManager = BuranBiometricManager() + + val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + println("Authentication error: $errorCode: $errString") + } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + println("Authentication failed") + } + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + println("Authentication succeeded") + + val ciphertext = biometricManager.decodeByteArray( + prefs.getString( + "password_ciphertext", + null + )!! + ) + + val certPassword = biometricManager.decryptData(ciphertext, result.cryptoObject?.cipher!!) + model.request(address, certPassword) + } + } + + val initializationVector = biometricManager.decodeByteArray( + prefs.getString( + "password_init_vector", + null + )!! + ) + + biometricManager.authenticateToDecryptData(initializationVector) + biometricManager.createBiometricPrompt(this, this, callback) + } + private fun showAlert(message: String) = runOnUiThread{ loadingView(false) @@ -709,7 +760,7 @@ class GemActivity : AppCompatActivity() { if(getInternetStatus()){ if(initialised){ loadingView(true) - return model.request(address, false) + return model.request(address, null) }else{ val intent = baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName) intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) diff --git a/app/src/main/java/corewala/buran/ui/GemViewModel.kt b/app/src/main/java/corewala/buran/ui/GemViewModel.kt index 0bc1c15..7c968d6 100644 --- a/app/src/main/java/corewala/buran/ui/GemViewModel.kt +++ b/app/src/main/java/corewala/buran/ui/GemViewModel.kt @@ -19,24 +19,24 @@ class GemViewModel: ViewModel() { this.db = db this.onState = onState - request(home, false) + request(home, null) } - fun request(address: String, clientCertAllowed: Boolean) { - gemini.request(address, false, clientCertAllowed){ state -> + fun request(address: String, clientCertPassword: String?) { + gemini.request(address, false, clientCertPassword){ state -> onState(state) } } fun requestBinaryDownload(uri: URI) { - gemini.request(uri.toString(), true, false){ state -> + gemini.request(uri.toString(), true, null){ state -> onState(state) } } //todo - same action as above... refactor fun requestInlineImage(uri: URI, onImageReady: (cacheUri: Uri?) -> Unit){ - gemini.request(uri.toString(), false, false){ state -> + gemini.request(uri.toString(), false, null){ state -> when (state) { is GemState.ResponseImage -> onImageReady(state.cacheUri) else -> onState(state)