259 lines
11 KiB
Kotlin
259 lines
11 KiB
Kotlin
package sushi.hardcore.droidfs
|
|
|
|
import android.app.KeyguardManager
|
|
import android.content.Context
|
|
import android.os.Build
|
|
import android.security.keystore.KeyGenParameterSpec
|
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
|
import android.security.keystore.KeyProperties
|
|
import android.widget.Toast
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.biometric.BiometricManager
|
|
import androidx.biometric.BiometricPrompt
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.fragment.app.FragmentActivity
|
|
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
|
import java.security.KeyStore
|
|
import java.security.KeyStoreException
|
|
import java.security.UnrecoverableKeyException
|
|
import javax.crypto.*
|
|
import javax.crypto.spec.GCMParameterSpec
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
class FingerprintProtector private constructor(
|
|
private val activity: FragmentActivity,
|
|
private val themeValue: String,
|
|
private val volumeDatabase: VolumeDatabase,
|
|
) {
|
|
|
|
interface Listener {
|
|
fun onHashStorageReset()
|
|
fun onPasswordHashDecrypted(hash: ByteArray)
|
|
fun onPasswordHashSaved()
|
|
fun onFailed(pending: Boolean)
|
|
}
|
|
|
|
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
|
|
|
|
fun canAuthenticate(context: Context): Int {
|
|
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
return if (!keyguardManager.isKeyguardSecure)
|
|
1
|
|
else when (BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
|
|
BiometricManager.BIOMETRIC_SUCCESS -> 0
|
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> 2
|
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> 3
|
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> 4
|
|
else -> -1
|
|
}
|
|
}
|
|
|
|
fun new(
|
|
activity: FragmentActivity,
|
|
themeValue: String,
|
|
volumeDatabase: VolumeDatabase,
|
|
): FingerprintProtector? {
|
|
return if (canAuthenticate(activity) == 0)
|
|
FingerprintProtector(activity, themeValue, volumeDatabase)
|
|
else
|
|
null
|
|
}
|
|
}
|
|
|
|
lateinit var listener: Listener
|
|
private val biometricPrompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), object: BiometricPrompt.AuthenticationCallback() {
|
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
super.onAuthenticationError(errorCode, errString)
|
|
if (
|
|
errorCode != BiometricPrompt.ERROR_USER_CANCELED &&
|
|
errorCode != BiometricPrompt.ERROR_NEGATIVE_BUTTON &&
|
|
errorCode != BiometricPrompt.ERROR_TIMEOUT
|
|
) {
|
|
Toast.makeText(activity, activity.getString(R.string.biometric_error, errString), Toast.LENGTH_SHORT).show()
|
|
}
|
|
listener.onFailed(false)
|
|
}
|
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
super.onAuthenticationSucceeded(result)
|
|
val cipherObject = result.cryptoObject?.cipher
|
|
if (cipherObject != null) {
|
|
try {
|
|
when (cipherActionMode) {
|
|
Cipher.ENCRYPT_MODE -> {
|
|
val cipherText = cipherObject.doFinal(dataToProcess)
|
|
volume.encryptedHash = cipherText
|
|
volume.iv = cipherObject.iv
|
|
if (volumeDatabase.addHash(volume))
|
|
listener.onPasswordHashSaved()
|
|
else
|
|
listener.onFailed(false)
|
|
}
|
|
Cipher.DECRYPT_MODE -> {
|
|
try {
|
|
val plainText = cipherObject.doFinal(dataToProcess)
|
|
listener.onPasswordHashDecrypted(plainText)
|
|
} catch (e: AEADBadTagException) {
|
|
listener.onFailed(true)
|
|
CustomAlertDialogBuilder(activity, themeValue)
|
|
.setTitle(R.string.error)
|
|
.setMessage(R.string.MAC_verification_failed)
|
|
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
|
resetHashStorage()
|
|
}
|
|
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
|
.setOnCancelListener { listener.onFailed(false) }
|
|
.show()
|
|
}
|
|
}
|
|
}
|
|
} catch (e: IllegalBlockSizeException) {
|
|
listener.onFailed(true)
|
|
CustomAlertDialogBuilder(activity, themeValue)
|
|
.setTitle(R.string.illegal_block_size_exception)
|
|
.setMessage(R.string.illegal_block_size_exception_msg)
|
|
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
|
resetHashStorage()
|
|
}
|
|
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
|
.setOnCancelListener { listener.onFailed(false) }
|
|
.show()
|
|
}
|
|
} else {
|
|
Toast.makeText(activity, R.string.error_cipher_null, Toast.LENGTH_SHORT).show()
|
|
listener.onFailed(false)
|
|
}
|
|
}
|
|
})
|
|
private lateinit var keyStore: KeyStore
|
|
private lateinit var key: SecretKey
|
|
private lateinit var cipher: Cipher
|
|
private var isCipherReady = false
|
|
private var cipherActionMode: Int? = null
|
|
private lateinit var volume: Volume
|
|
private lateinit var dataToProcess: ByteArray
|
|
|
|
private fun resetHashStorage() {
|
|
try {
|
|
keyStore.deleteEntry(KEY_ALIAS)
|
|
} catch (e: KeyStoreException) {
|
|
e.printStackTrace()
|
|
}
|
|
volumeDatabase.getVolumes().forEach { volume ->
|
|
volumeDatabase.removeHash(volume)
|
|
}
|
|
isCipherReady = false
|
|
Toast.makeText(activity, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show()
|
|
listener.onHashStorageReset()
|
|
}
|
|
|
|
private fun prepareCipher(): Boolean {
|
|
if (!isCipherReady) {
|
|
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
|
|
keyStore.load(null)
|
|
key = if (keyStore.containsAlias(KEY_ALIAS)) {
|
|
try {
|
|
keyStore.getKey(KEY_ALIAS, null) as SecretKey
|
|
} catch (e: UnrecoverableKeyException) {
|
|
listener.onFailed(true)
|
|
CustomAlertDialogBuilder(activity, themeValue)
|
|
.setTitle(activity.getString(R.string.unrecoverable_key_exception))
|
|
.setMessage(activity.getString(R.string.unrecoverable_key_exception_msg, e.localizedMessage))
|
|
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
|
resetHashStorage()
|
|
}
|
|
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
|
.setOnCancelListener { listener.onFailed(false) }
|
|
.show()
|
|
return false
|
|
}
|
|
} 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(
|
|
KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE
|
|
)
|
|
isCipherReady = true
|
|
}
|
|
return true
|
|
}
|
|
|
|
private fun alertKeyPermanentlyInvalidatedException() {
|
|
listener.onFailed(true)
|
|
CustomAlertDialogBuilder(activity, themeValue)
|
|
.setTitle(R.string.key_permanently_invalidated_exception)
|
|
.setMessage(R.string.key_permanently_invalidated_exception_msg)
|
|
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
|
resetHashStorage()
|
|
}
|
|
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
|
.setOnCancelListener { listener.onFailed(false) }
|
|
.show()
|
|
}
|
|
|
|
fun savePasswordHash(volume: Volume, plainText: ByteArray) {
|
|
this.volume = volume
|
|
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
.setTitle(activity.getString(R.string.encrypt_action_description))
|
|
.setSubtitle(volume.shortName)
|
|
.setDescription(activity.getString(R.string.fingerprint_instruction))
|
|
.setNegativeButtonText(activity.getString(R.string.cancel))
|
|
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
.setConfirmationRequired(false)
|
|
.build()
|
|
cipherActionMode = Cipher.ENCRYPT_MODE
|
|
if (prepareCipher()) {
|
|
try {
|
|
cipher.init(Cipher.ENCRYPT_MODE, key)
|
|
dataToProcess = plainText
|
|
biometricPrompt.authenticate(
|
|
biometricPromptInfo,
|
|
BiometricPrompt.CryptoObject(cipher)
|
|
)
|
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
|
alertKeyPermanentlyInvalidatedException()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun loadPasswordHash(volumeName: String, cipherText: ByteArray, iv: ByteArray) {
|
|
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
.setTitle(activity.getString(R.string.decrypt_action_description))
|
|
.setSubtitle(volumeName)
|
|
.setDescription(activity.getString(R.string.fingerprint_instruction))
|
|
.setNegativeButtonText(activity.getString(R.string.cancel))
|
|
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
.setConfirmationRequired(false)
|
|
.build()
|
|
cipherActionMode = Cipher.DECRYPT_MODE
|
|
if (prepareCipher()) {
|
|
dataToProcess = cipherText
|
|
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
|
|
try {
|
|
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
|
|
biometricPrompt.authenticate(
|
|
biometricPromptInfo,
|
|
BiometricPrompt.CryptoObject(cipher)
|
|
)
|
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
|
alertKeyPermanentlyInvalidatedException()
|
|
}
|
|
}
|
|
}
|
|
} |