Biometric verification for client certificates

This commit is contained in:
Corewala 2022-05-17 13:45:54 -04:00
parent 55db823420
commit 0c1cb3a309
6 changed files with 84 additions and 25 deletions

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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}")

View File

@ -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)

View File

@ -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)