mirror of https://github.com/Corewala/Buran
Biometric verification for client certificates
This commit is contained in:
parent
55db823420
commit
0c1cb3a309
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue