mirror of https://github.com/Corewala/Buran
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.
This commit is contained in:
parent
88583d1f49
commit
64f3308e1b
|
@ -56,6 +56,7 @@ dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.3.0-rc01'
|
implementation 'com.google.android.material:material:1.3.0-rc01'
|
||||||
|
implementation "androidx.biometric:biometric:1.1.0"
|
||||||
|
|
||||||
//ROOM DB
|
//ROOM DB
|
||||||
def room_version = "2.2.6"
|
def room_version = "2.2.6"
|
||||||
|
|
|
@ -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_HUMAN_READABLE = "client_cert_uri_human_readable"
|
||||||
const val PREF_KEY_CLIENT_CERT_ACTIVE = "client_cert_active"
|
const val PREF_KEY_CLIENT_CERT_ACTIVE = "client_cert_active"
|
||||||
const val PREF_KEY_CLIENT_CERT_PASSWORD = "client_cert_password"
|
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"
|
const val PREF_KEY_USE_CUSTOM_TAB = "use_custom_tabs"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,16 @@ import android.content.SharedPreferences
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import corewala.buran.Buran
|
import corewala.buran.Buran
|
||||||
import corewala.buran.R
|
import corewala.buran.R
|
||||||
|
import corewala.buran.io.keymanager.BuranBiometricManager
|
||||||
|
|
||||||
|
|
||||||
const val PREFS_SET_CLIENT_CERT_REQ = 20
|
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.key = Buran.PREF_KEY_CLIENT_CERT_PASSWORD
|
||||||
clientCertPassword.title = getString(R.string.client_certificate_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,
|
Buran.PREF_KEY_CLIENT_CERT_PASSWORD,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
if (certPasword != null && certPasword.isNotEmpty()) {
|
|
||||||
clientCertPassword.summary = getDots(certPasword)
|
if (certPassword != null && certPassword.isNotEmpty()) {
|
||||||
|
clientCertPassword.summary = getDots(certPassword)
|
||||||
} else {
|
} else {
|
||||||
clientCertPassword.summary = getString(R.string.no_password)
|
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 ->
|
clientCertPassword.setOnPreferenceChangeListener { _, newValue ->
|
||||||
val passphrase = "$newValue"
|
val passphrase = "$newValue"
|
||||||
if (passphrase.isEmpty()) {
|
if (passphrase.isEmpty()) {
|
||||||
clientCertPassword.summary = getString(R.string.no_password)
|
clientCertPassword.summary = getString(R.string.no_password)
|
||||||
|
useBiometrics.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
clientCertPassword.summary = getDots(passphrase)
|
clientCertPassword.summary = getDots(passphrase)
|
||||||
|
useBiometrics.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
true//update the value
|
true//update the value
|
||||||
}
|
}
|
||||||
|
|
||||||
certificateCategory.addPreference(clientCertPassword)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDots(value: String): String {
|
private fun getDots(value: String): String {
|
||||||
|
|
|
@ -70,6 +70,9 @@
|
||||||
<string name="no_password">Pas de mot de passe</string>
|
<string name="no_password">Pas de mot de passe</string>
|
||||||
<string name="use_client_certificate">Utiliser Certificat Client</string>
|
<string name="use_client_certificate">Utiliser Certificat Client</string>
|
||||||
<string name="client_certificate_required">Certificat Client Requis</string>
|
<string name="client_certificate_required">Certificat Client Requis</string>
|
||||||
|
<string name="confirm_your_identity">Confirmez votre identité</string>
|
||||||
|
<string name="use_biometric_unlock">Utilisez vos informations biométriques pour continuer</string>
|
||||||
|
<string name="biometric_cert_verification">Certificat Client biométrique</string>
|
||||||
<string name="set_home_capsule">Choisir comme capsule d\'accueil</string>
|
<string name="set_home_capsule">Choisir comme capsule d\'accueil</string>
|
||||||
<string name="check_for_updates">Rechercher des nouvelles versions</string>
|
<string name="check_for_updates">Rechercher des nouvelles versions</string>
|
||||||
<string name="new_version_available">Nouvelle version disponible</string>
|
<string name="new_version_available">Nouvelle version disponible</string>
|
||||||
|
|
|
@ -70,6 +70,9 @@
|
||||||
<string name="no_password">No Password</string>
|
<string name="no_password">No Password</string>
|
||||||
<string name="use_client_certificate">Use Client Certificate</string>
|
<string name="use_client_certificate">Use Client Certificate</string>
|
||||||
<string name="client_certificate_required">Client Certificate Required</string>
|
<string name="client_certificate_required">Client Certificate Required</string>
|
||||||
|
<string name="confirm_your_identity">Confirm your identity</string>
|
||||||
|
<string name="use_biometric_unlock">Verify your biometric credentials to continue</string>
|
||||||
|
<string name="biometric_cert_verification">Client Certificate biometrics</string>
|
||||||
<string name="set_home_capsule">Set home capsule</string>
|
<string name="set_home_capsule">Set home capsule</string>
|
||||||
<string name="check_for_updates">Check for updates</string>
|
<string name="check_for_updates">Check for updates</string>
|
||||||
<string name="new_version_available">New version available</string>
|
<string name="new_version_available">New version available</string>
|
||||||
|
|
Loading…
Reference in New Issue