260 lines
13 KiB
Kotlin
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()
|
|
}
|
|
|
|
}
|
|
}
|