DroidFS/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintPasswordHashSave...

260 lines
13 KiB
Kotlin

package sushi.hardcore.droidfs.fingerprint_stuff
import android.Manifest
import android.app.KeyguardManager
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.hardware.biometrics.BiometricPrompt
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import android.os.Handler
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.security.KeyStore
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
@RequiresApi(Build.VERSION_CODES.M)
class FingerprintPasswordHashSaver(private val activityContext: AppCompatActivity, private val shared_prefs: SharedPreferences) {
private var isPrepared = false
var isListening = false
private var authenticationFailed = false
private val sharedPrefsEditor: SharedPreferences.Editor = shared_prefs.edit()
private val fingerprintManager = activityContext.getSystemService(Context.FINGERPRINT_SERVICE) as FingerprintManager
private lateinit var rootCipherDir: String
private lateinit var actionDescription: String
private lateinit var onAuthenticationResult: (success: Boolean) -> Unit
private lateinit var onPasswordDecrypted: (password: ByteArray) -> Unit
private lateinit var keyStore: KeyStore
private lateinit var key: SecretKey
lateinit var fingerprintFragment: FingerprintFragment
private val handler = Handler()
private lateinit var cancellationSignal: CancellationSignal
private var actionMode: Int? = null
private lateinit var dataToProcess: ByteArray
private lateinit var cipher: Cipher
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val KEY_ALIAS = "Hash Key"
private const val KEY_SIZE = 256
private const val GCM_TAG_LEN = 128
private const val CIPHER_TYPE = "AES/GCM/NoPadding"
private const val SUCCESS_DISMISS_DIALOG_DELAY: Long = 400
private const val FAILED_DISMISS_DIALOG_DELAY: Long = 800
}
private fun resetHashStorage() {
keyStore.deleteEntry(KEY_ALIAS)
val savedVolumePaths = shared_prefs.getStringSet(ConstValues.saved_volumes_key, HashSet<String>()) as Set<String>
for (path in savedVolumePaths){
val savedHash = shared_prefs.getString(path, null)
if (savedHash != null){
sharedPrefsEditor.remove(path)
}
}
sharedPrefsEditor.apply()
Toast.makeText(activityContext, activityContext.getString(R.string.hash_storage_reset), Toast.LENGTH_SHORT).show()
}
fun canAuthenticate(): Boolean{
if (ContextCompat.checkSelfPermission(activityContext, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED){
Toast.makeText(activityContext, activityContext.getString(R.string.fingerprint_perm_denied), Toast.LENGTH_SHORT).show()
} else if (!fingerprintManager.isHardwareDetected){
Toast.makeText(activityContext, activityContext.getString(R.string.no_fingerprint_sensor), Toast.LENGTH_SHORT).show()
} else {
val keyguardManager = activityContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (!keyguardManager.isKeyguardSecure || !fingerprintManager.hasEnrolledFingerprints()) {
Toast.makeText(activityContext, activityContext.getString(R.string.no_fingerprint_configured), Toast.LENGTH_SHORT).show()
} else {
return true
}
}
return false
}
private fun prepare() {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
key = if (keyStore.containsAlias(KEY_ALIAS)){
keyStore.getKey(KEY_ALIAS, null) as SecretKey
} else {
val builder = KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
builder.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
builder.setKeySize(KEY_SIZE)
builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
builder.setUserAuthenticationRequired(true)
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}
cipher = Cipher.getInstance(CIPHER_TYPE)
fingerprintFragment = FingerprintFragment(rootCipherDir, actionDescription, ::stopListening)
isPrepared = true
}
fun encryptAndSave(plainText: ByteArray, root_cipher_dir: String, onAuthenticationResult: (success: Boolean) -> Unit){
if (shared_prefs.getString(root_cipher_dir, null) == null){
this.rootCipherDir = root_cipher_dir
this.actionDescription = activityContext.getString(R.string.encrypt_action_description)
this.onAuthenticationResult = onAuthenticationResult
if (!isPrepared){
prepare()
}
dataToProcess = plainText
actionMode = Cipher.ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, key)
startListening()
}
}
fun decrypt(cipherText: String, root_cipher_dir: String, onPasswordDecrypted: (password: ByteArray) -> Unit){
this.rootCipherDir = root_cipher_dir
this.actionDescription = activityContext.getString(R.string.decrypt_action_description)
this.onPasswordDecrypted = onPasswordDecrypted
if (!isPrepared){
prepare()
}
actionMode = Cipher.DECRYPT_MODE
val encodedElements = cipherText.split(":")
dataToProcess = Base64.decode(encodedElements[1], 0)
val iv = Base64.decode(encodedElements[0], 0)
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
startListening()
}
private fun startListening(){
cancellationSignal = CancellationSignal()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val biometricPrompt = BiometricPrompt.Builder(activityContext)
.setTitle(rootCipherDir)
.setSubtitle(actionDescription)
.setDescription(activityContext.getString(R.string.fingerprint_instruction))
.setNegativeButton(activityContext.getString(R.string.cancel), activityContext.mainExecutor, DialogInterface.OnClickListener{_, _ ->
cancellationSignal.cancel()
callbackOnAuthenticationFailed() //toggle on onAuthenticationResult
}).build()
biometricPrompt.authenticate(BiometricPrompt.CryptoObject(cipher), cancellationSignal, activityContext.mainExecutor, object: BiometricPrompt.AuthenticationCallback(){
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
callbackOnAuthenticationError()
}
override fun onAuthenticationFailed() {
callbackOnAuthenticationFailed()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
callbackOnAuthenticationSucceeded()
}
})
} else {
fingerprintFragment.show(activityContext.supportFragmentManager, null)
fingerprintManager.authenticate(FingerprintManager.CryptoObject(cipher), cancellationSignal, 0, object: FingerprintManager.AuthenticationCallback(){
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
callbackOnAuthenticationError()
}
override fun onAuthenticationFailed() {
callbackOnAuthenticationFailed()
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult?) {
callbackOnAuthenticationSucceeded()
}
}, null)
}
isListening = true
}
fun stopListening(){
cancellationSignal.cancel()
isListening = false
}
fun callbackOnAuthenticationError() {
if (!authenticationFailed){
if (fingerprintFragment.isAdded){
fingerprintFragment.image_fingerprint.setColorFilter(ContextCompat.getColor(activityContext, R.color.fingerprint_failed))
fingerprintFragment.text_instruction.text = activityContext.getString(R.string.authentication_error)
handler.postDelayed({ fingerprintFragment.dismiss() }, 1000)
}
if (actionMode == Cipher.ENCRYPT_MODE){
handler.postDelayed({ onAuthenticationResult(false) }, FAILED_DISMISS_DIALOG_DELAY)
}
}
}
fun callbackOnAuthenticationFailed() {
authenticationFailed = true
if (fingerprintFragment.isAdded){
fingerprintFragment.image_fingerprint.setColorFilter(ContextCompat.getColor(activityContext, R.color.fingerprint_failed))
fingerprintFragment.text_instruction.text = activityContext.getString(R.string.authentication_failed)
handler.postDelayed({ fingerprintFragment.dismiss() }, FAILED_DISMISS_DIALOG_DELAY)
stopListening()
} else {
handler.postDelayed({ stopListening() }, FAILED_DISMISS_DIALOG_DELAY)
}
if (actionMode == Cipher.ENCRYPT_MODE){
handler.postDelayed({ onAuthenticationResult(false) }, FAILED_DISMISS_DIALOG_DELAY)
}
}
fun callbackOnAuthenticationSucceeded() {
if (fingerprintFragment.isAdded){
fingerprintFragment.image_fingerprint.setColorFilter(ContextCompat.getColor(activityContext, R.color.fingerprint_success))
fingerprintFragment.text_instruction.text = activityContext.getString(R.string.authenticated)
}
try {
when (actionMode) {
Cipher.ENCRYPT_MODE -> {
val cipherText = cipher.doFinal(dataToProcess)
val encodedCipherText = Base64.encodeToString(cipherText, 0)
val encodedIv = Base64.encodeToString(cipher.iv, 0)
sharedPrefsEditor.putString(rootCipherDir, "$encodedIv:$encodedCipherText")
sharedPrefsEditor.apply()
handler.postDelayed({
if (fingerprintFragment.isAdded){
fingerprintFragment.dismiss()
}
onAuthenticationResult(true)
}, SUCCESS_DISMISS_DIALOG_DELAY)
}
Cipher.DECRYPT_MODE -> {
try {
val plainText = cipher.doFinal(dataToProcess)
handler.postDelayed({
if (fingerprintFragment.isAdded){
fingerprintFragment.dismiss()
}
onPasswordDecrypted(plainText)
}, SUCCESS_DISMISS_DIALOG_DELAY)
} catch (e: AEADBadTagException){
ColoredAlertDialogBuilder(activityContext)
.setTitle(R.string.error)
.setMessage(activityContext.getString(R.string.MAC_verification_failed))
.setPositiveButton(activityContext.getString(R.string.reset_hash_storage)) { _, _ ->
resetHashStorage()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
} catch (e: IllegalBlockSizeException){
stopListening()
ColoredAlertDialogBuilder(activityContext)
.setTitle(R.string.authentication_error)
.setMessage(activityContext.getString(R.string.authentication_error_msg))
.setPositiveButton(activityContext.getString(R.string.reset_hash_storage)) { _, _ ->
resetHashStorage()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}