From 64f3308e1b6b62a9007ea072c9600d3e81d87a6e Mon Sep 17 00:00:00 2001 From: Corewala Date: Mon, 16 May 2022 20:37:15 -0400 Subject: [PATCH] Added biometric cert password encryption to settings Does nothing useful right now and just makes your client cert unusable while enabled. Obviously I plan to change that in the near-ish future. --- app/build.gradle | 1 + app/src/main/java/corewala/buran/Buran.kt | 1 + .../io/keymanager/BuranBiometricManager.kt | 106 ++++++++++++++++++ .../buran/ui/settings/SettingsFragment.kt | 87 ++++++++++++-- app/src/main/res/values-fr/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 6 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt diff --git a/app/build.gradle b/app/build.gradle index 4ef38a1..740b5cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.google.android.material:material:1.3.0-rc01' + implementation "androidx.biometric:biometric:1.1.0" //ROOM DB def room_version = "2.2.6" diff --git a/app/src/main/java/corewala/buran/Buran.kt b/app/src/main/java/corewala/buran/Buran.kt index eb486e3..e1fda20 100644 --- a/app/src/main/java/corewala/buran/Buran.kt +++ b/app/src/main/java/corewala/buran/Buran.kt @@ -12,6 +12,7 @@ class Buran: Application() { const val PREF_KEY_CLIENT_CERT_HUMAN_READABLE = "client_cert_uri_human_readable" const val PREF_KEY_CLIENT_CERT_ACTIVE = "client_cert_active" const val PREF_KEY_CLIENT_CERT_PASSWORD = "client_cert_password" + const val CLIENT_CERT_PASSWORD_SECRET_KEY_NAME = "client_cert_secret_key_name" const val PREF_KEY_USE_CUSTOM_TAB = "use_custom_tabs" } } \ No newline at end of file diff --git a/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt b/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt new file mode 100644 index 0000000..414f523 --- /dev/null +++ b/app/src/main/java/corewala/buran/io/keymanager/BuranBiometricManager.kt @@ -0,0 +1,106 @@ +package corewala.buran.io.keymanager + +import android.content.Context +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import corewala.buran.Buran +import corewala.buran.R +import java.nio.charset.Charset +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +data class EncryptedData(val ciphertext: ByteArray, val initializationVector: ByteArray) + +@RequiresApi(Build.VERSION_CODES.P) +class BuranBiometricManager { + + private lateinit var biometricPrompt: BiometricPrompt + private lateinit var promptInfo: BiometricPrompt.PromptInfo + + private val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + private val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE + private val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + + fun createBiometricPrompt(context: Context, fragment: Fragment, callback: BiometricPrompt.AuthenticationCallback){ + val executor = ContextCompat.getMainExecutor(context) + biometricPrompt = BiometricPrompt(fragment, 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() + val secretKey = getSecretKey(Buran.CLIENT_CERT_PASSWORD_SECRET_KEY_NAME) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) + } + + fun authenticateToDecryptData(initializationVector: ByteArray) { + val cipher = getCipher() + val secretKey = getSecretKey(Buran.CLIENT_CERT_PASSWORD_SECRET_KEY_NAME) + cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, initializationVector)) + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) + } + + // Allows ByteArrays to be stored in prefs as strings. Possibly the most horrifying function I've ever written. + fun decodeByteArray(encodedByteArray: String): ByteArray{ + val byteList = encodedByteArray.substring(1, encodedByteArray.length - 1).split(", ") + var decodedByteArray = byteArrayOf() + for(byte in byteList){ + decodedByteArray += byte.toInt().toByte() + } + println(decodedByteArray.contentToString()) + return decodedByteArray + } + + fun encryptData(plaintext: String, cipher: Cipher): EncryptedData { + val ciphertext = cipher.doFinal(plaintext.toByteArray(Charset.forName("UTF-8"))) + return EncryptedData(ciphertext,cipher.iv) + } + + fun decryptData(ciphertext: ByteArray, cipher: Cipher): String { + val plaintext = cipher.doFinal(ciphertext) + return String(plaintext, Charset.forName("UTF-8")) + } + + private fun getCipher(): Cipher { + val transformation = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING" + return Cipher.getInstance(transformation) + } + + private fun getSecretKey(keyName: String): SecretKey { + val androidKeystore = "AndroidKeyStore" + val keyStore = KeyStore.getInstance(androidKeystore) + keyStore.load(null) + keyStore.getKey(keyName, null)?.let { return it as SecretKey } + + val keyGenParams = KeyGenParameterSpec.Builder( + keyName, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ).apply { + setBlockModes(ENCRYPTION_BLOCK_MODE) + setEncryptionPaddings(ENCRYPTION_PADDING) + setKeySize(256) + setUserAuthenticationRequired(true) + }.build() + + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + androidKeystore + ) + keyGenerator.init(keyGenParams) + return keyGenerator.generateKey() + } +} \ No newline at end of file diff --git a/app/src/main/java/corewala/buran/ui/settings/SettingsFragment.kt b/app/src/main/java/corewala/buran/ui/settings/SettingsFragment.kt index 1a22673..aecfd41 100644 --- a/app/src/main/java/corewala/buran/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/corewala/buran/ui/settings/SettingsFragment.kt @@ -7,13 +7,16 @@ import android.content.SharedPreferences import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.net.Uri +import android.os.Build import android.os.Bundle import android.provider.OpenableColumns import android.view.inputmethod.EditorInfo import androidx.appcompat.app.AppCompatDelegate +import androidx.biometric.BiometricPrompt import androidx.preference.* import corewala.buran.Buran import corewala.buran.R +import corewala.buran.io.keymanager.BuranBiometricManager const val PREFS_SET_CLIENT_CERT_REQ = 20 @@ -272,28 +275,98 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang clientCertPassword.key = Buran.PREF_KEY_CLIENT_CERT_PASSWORD clientCertPassword.title = getString(R.string.client_certificate_password) - val certPasword = preferenceManager.sharedPreferences.getString( + val certPassword = preferenceManager.sharedPreferences.getString( Buran.PREF_KEY_CLIENT_CERT_PASSWORD, null ) - if (certPasword != null && certPasword.isNotEmpty()) { - clientCertPassword.summary = getDots(certPasword) + + if (certPassword != null && certPassword.isNotEmpty()) { + clientCertPassword.summary = getDots(certPassword) } else { clientCertPassword.summary = getString(R.string.no_password) } - clientCertPassword.dialogTitle = getString(R.string.client_certificate_password) + clientCertPassword.isVisible = !preferenceManager.sharedPreferences.getBoolean("use_biometrics", false) + certificateCategory.addPreference(clientCertPassword) + + val useBiometrics = SwitchPreferenceCompat(context) + useBiometrics.setDefaultValue(false) + useBiometrics.key = "use_biometrics" + useBiometrics.title = getString(R.string.biometric_cert_verification) + useBiometrics.isVisible = false + certificateCategory.addPreference(useBiometrics) + + + val passwordCiphertext = EditTextPreference(context) + passwordCiphertext.key = "password_ciphertext" + passwordCiphertext.isVisible = false + certificateCategory.addPreference(passwordCiphertext) + + val passwordInitVector = EditTextPreference(context) + passwordInitVector.key = "password_init_vector" + passwordInitVector.isVisible = false + certificateCategory.addPreference(passwordInitVector) + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){ + useBiometrics.isVisible = certPassword?.isNotEmpty() ?: false + + useBiometrics.setOnPreferenceChangeListener { _, newValue -> + val biometricManager = BuranBiometricManager() + + val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + println("Authentication error: $errorCode: $errString") + useBiometrics.isChecked = !(newValue as Boolean) + } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + println("Authentication failed") + useBiometrics.isChecked = !(newValue as Boolean) + } + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + println("Authentication succeeded") + + if(newValue as Boolean){ + val encryptedData = biometricManager.encryptData(certPassword!!, result.cryptoObject?.cipher!!) + val ciphertext = encryptedData.ciphertext + val initializationVector = encryptedData.initializationVector + passwordInitVector.text = initializationVector.contentToString() + passwordCiphertext.text = ciphertext.contentToString() + clientCertPassword.text = "encrypted" + }else{ + val ciphertext = biometricManager.decodeByteArray(passwordCiphertext.text) + clientCertPassword.text = biometricManager.decryptData(ciphertext, result.cryptoObject?.cipher!!) + clientCertPassword.summary = getDots(clientCertPassword.text) + } + clientCertPassword.isVisible = !(newValue as Boolean) + } + } + + biometricManager.createBiometricPrompt(requireContext(), this, callback) + + if(newValue as Boolean){ + biometricManager.authenticateToEncryptData() + }else{ + val initializationVector = biometricManager.decodeByteArray(passwordInitVector.text) + biometricManager.authenticateToDecryptData(initializationVector) + } + + true + } + } + clientCertPassword.setOnPreferenceChangeListener { _, newValue -> val passphrase = "$newValue" if (passphrase.isEmpty()) { clientCertPassword.summary = getString(R.string.no_password) + useBiometrics.isVisible = false } else { clientCertPassword.summary = getDots(passphrase) + useBiometrics.isVisible = true } - true//update the value } - - certificateCategory.addPreference(clientCertPassword) } private fun getDots(value: String): String { diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9a89123..7f750db 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -70,6 +70,9 @@ Pas de mot de passe Utiliser Certificat Client Certificat Client Requis + Confirmez votre identité + Utilisez vos informations biométriques pour continuer + Certificat Client biométrique Choisir comme capsule d\'accueil Rechercher des nouvelles versions Nouvelle version disponible diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98d4886..db4018d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,6 +70,9 @@ No Password Use Client Certificate Client Certificate Required + Confirm your identity + Verify your biometric credentials to continue + Client Certificate biometrics Set home capsule Check for updates New version available