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()) as Set 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){ 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() } } }