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