diff --git a/app/build.gradle b/app/build.gradle index 1bcc5af..d884a31 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,7 @@ android { versionCode 4 versionName "1.1.9" + android.ndkVersion = "21.2.6472646" ndk { abiFilters 'x86_64', 'armeabi-v7a', 'arm64-v8a' } @@ -48,12 +49,13 @@ dependencies { implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.constraintlayout:constraintlayout:2.0.2" - implementation "androidx.sqlite:sqlite:2.1.0" - implementation "androidx.preference:preference:1.1.1" + implementation "androidx.sqlite:sqlite-ktx:2.1.0" + implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "com.jaredrummler:cyanea:1.0.2" implementation "com.github.bumptech.glide:glide:4.11.0" implementation "com.google.android.exoplayer:exoplayer-core:2.11.7" implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7" implementation "com.otaliastudios:cameraview:2.6.3" + implementation "androidx.biometric:biometric:1.0.1" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 777d9da..e8d1dd2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,12 +9,18 @@ android:protectionLevel="signature" /> - - - - + + + + - + = Build.VERSION_CODES.M && usf_fingerprint) { - fingerprintPasswordHashSaver = FingerprintPasswordHashSaver(this, sharedPrefs) - } else { - WidgetUtil.hide(checkbox_save_password) - } + setupActionBar() + setupFingerprintStuff() savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs) if (savedVolumesAdapter.count > 0){ saved_path_listview.adapter = savedVolumesAdapter @@ -147,15 +135,15 @@ class ChangePasswordActivity : BaseActivity() { override fun doTask(activity: AppCompatActivity) { val oldPassword = edit_old_password.text.toString().toCharArray() var returnedHash: ByteArray? = null - if (usf_fingerprint && checkbox_save_password.isChecked) { + if (checkbox_save_password.isChecked) { returnedHash = ByteArray(GocryptfsVolume.KeyLen) } var changePasswordImmediately = true if (givenHash == null) { val cipherText = sharedPrefs.getString(rootCipherDir, null) - if (cipherText != null) { //password hash saved + if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //password hash saved stopTask { - fingerprintPasswordHashSaver.decrypt(cipherText, rootCipherDir, ::changePassword) + loadPasswordHash(cipherText, ::changePassword) } changePasswordImmediately = false } @@ -177,9 +165,11 @@ class ChangePasswordActivity : BaseActivity() { if (checkbox_remember_path.isChecked) { savedVolumesAdapter.addVolumePath(rootCipherDir) } - if (checkbox_save_password.isChecked && returnedHash != null) { - fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ -> - stopTask { onPasswordChanged() } + if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ + stopTask { + savePasswordHash(returnedHash) { + onPasswordChanged() + } } } else { stopTask { onPasswordChanged() } @@ -213,32 +203,12 @@ class ChangePasswordActivity : BaseActivity() { .show() } - fun onClickSavePasswordHash(view: View) { - if (checkbox_save_password.isChecked){ - if (!fingerprintPasswordHashSaver.canAuthenticate()){ - checkbox_save_password.isChecked = false - } else { - checkbox_remember_path.isChecked = checkbox_remember_path.isEnabled - } - } - } - fun onClickRememberPath(view: View) { if (!checkbox_remember_path.isChecked){ checkbox_save_password.isChecked = false } } - override fun onPause() { - super.onPause() - if (::fingerprintPasswordHashSaver.isInitialized && fingerprintPasswordHashSaver.isListening){ - fingerprintPasswordHashSaver.stopListening() - if (fingerprintPasswordHashSaver.fingerprintFragment.isAdded){ - fingerprintPasswordHashSaver.fingerprintFragment.dismiss() - } - } - } - override fun onDestroy() { super.onDestroy() Wiper.wipeEditText(edit_old_password) diff --git a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt index b97112b..7488f9f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt @@ -9,34 +9,26 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_create.* import kotlinx.android.synthetic.main.checkboxes_section.* -import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.volume_path_section.* import sushi.hardcore.droidfs.explorers.ExplorerActivity -import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver -import sushi.hardcore.droidfs.util.* +import sushi.hardcore.droidfs.util.GocryptfsVolume +import sushi.hardcore.droidfs.util.LoadingTask +import sushi.hardcore.droidfs.util.PathUtils +import sushi.hardcore.droidfs.util.Wiper import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import java.io.File import java.util.* -class CreateActivity : BaseActivity() { +class CreateActivity : VolumeActionActivity() { companion object { private const val PICK_DIRECTORY_REQUEST_CODE = 1 } - private lateinit var fingerprintPasswordHashSaver: FingerprintPasswordHashSaver - private lateinit var rootCipherDir: String private var sessionID = -1 - private var usf_fingerprint = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_create) - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) { - fingerprintPasswordHashSaver = FingerprintPasswordHashSaver(this, sharedPrefs) - } else { - WidgetUtil.hide(checkbox_save_password) - } + setupActionBar() + setupFingerprintStuff() edit_password_confirm.setOnEditorActionListener { v, _, _ -> onClickCreate(v) true @@ -127,7 +119,7 @@ class CreateActivity : BaseActivity() { if (goodDirectory) { if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) { var returnedHash: ByteArray? = null - if (usf_fingerprint && checkbox_save_password.isChecked){ + if (checkbox_save_password.isChecked){ returnedHash = ByteArray(GocryptfsVolume.KeyLen) } sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash) @@ -146,9 +138,11 @@ class CreateActivity : BaseActivity() { } editor.apply() } - if (checkbox_save_password.isChecked && returnedHash != null){ - fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir){ _ -> - stopTask { startExplorer() } + if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ + stopTask { + savePasswordHash(returnedHash) { + startExplorer() + } } } else { stopTask { startExplorer() } @@ -191,32 +185,12 @@ class CreateActivity : BaseActivity() { .show() } - fun onClickSavePasswordHash(view: View) { - if (checkbox_save_password.isChecked){ - if (!fingerprintPasswordHashSaver.canAuthenticate()){ - checkbox_save_password.isChecked = false - } else { - checkbox_remember_path.isChecked = true - } - } - } - fun onClickRememberPath(view: View) { if (!checkbox_remember_path.isChecked){ checkbox_save_password.isChecked = false } } - override fun onPause() { - super.onPause() - if (::fingerprintPasswordHashSaver.isInitialized && fingerprintPasswordHashSaver.isListening){ - fingerprintPasswordHashSaver.stopListening() - if (fingerprintPasswordHashSaver.fingerprintFragment.isAdded){ - fingerprintPasswordHashSaver.fingerprintFragment.dismiss() - } - } - } - override fun onDestroy() { super.onDestroy() Wiper.wipeEditText(edit_password) diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt index 8e9b345..7b5cd54 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt @@ -22,21 +22,29 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + - ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSIONS_REQUEST) - } else { - if (checkStorageAvailability()){ - checkFirstOpening() - } - } - } val metrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(metrics) image_logo.layoutParams.height = (metrics.heightPixels/2.2).toInt() Glide.with(this).load(R.drawable.logo).into(image_logo) + if (!isRecreating){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + + ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSIONS_REQUEST) + } else { + onStoragePermissionGranted() + } + } else { + onStoragePermissionGranted() + } + } + } + + private fun onStoragePermissionGranted(){ + if (checkStorageAvailability()){ + checkFirstOpening() + } } private fun checkStorageAvailability(): Boolean { @@ -78,11 +86,10 @@ class MainActivity : BaseActivity() { ColoredAlertDialogBuilder(this) .setTitle(R.string.storage_perm_denied) .setMessage(R.string.storage_perm_denied_msg) - .setCancelable(false) - .setPositiveButton(R.string.ok) { _, _ -> finish() }.show() + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> finish() }.show() } else { - checkStorageAvailability() - checkFirstOpening() + onStoragePermissionGranted() } } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt index 31f0493..e72d45f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt @@ -12,43 +12,31 @@ import android.widget.AdapterView.OnItemClickListener import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_open.* -import kotlinx.android.synthetic.main.activity_open.saved_path_listview import kotlinx.android.synthetic.main.checkboxes_section.* -import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.volume_path_section.* import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter import sushi.hardcore.droidfs.explorers.ExplorerActivity import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop import sushi.hardcore.droidfs.explorers.ExplorerActivityPick -import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver import sushi.hardcore.droidfs.provider.RestrictedFileProvider import sushi.hardcore.droidfs.util.* import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import java.io.File import java.util.* -class OpenActivity : BaseActivity() { +class OpenActivity : VolumeActionActivity() { companion object { private const val PICK_DIRECTORY_REQUEST_CODE = 1 } private lateinit var savedVolumesAdapter: SavedVolumesAdapter - private lateinit var fingerprintPasswordHashSaver: FingerprintPasswordHashSaver - private lateinit var rootCipherDir: String private var sessionID = -1 private var isStartingActivity = false private var isFinishingIntentionally = false - private var usf_fingerprint = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_open) - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) { - fingerprintPasswordHashSaver = FingerprintPasswordHashSaver(this, sharedPrefs) - } else { - WidgetUtil.hide(checkbox_save_password) - } + setupActionBar() + setupFingerprintStuff() savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs) if (savedVolumesAdapter.count > 0){ saved_path_listview.adapter = savedVolumesAdapter @@ -56,8 +44,8 @@ class OpenActivity : BaseActivity() { rootCipherDir = savedVolumesAdapter.getItem(position) edit_volume_path.setText(rootCipherDir) val cipherText = sharedPrefs.getString(rootCipherDir, null) - if (cipherText != null){ //password hash saved - fingerprintPasswordHashSaver.decrypt(cipherText, rootCipherDir, ::openUsingPasswordHash) + if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ //password hash saved + loadPasswordHash(cipherText, ::openUsingPasswordHash) } } } else { @@ -165,7 +153,7 @@ class OpenActivity : BaseActivity() { override fun doTask(activity: AppCompatActivity) { val password = edit_password.text.toString().toCharArray() var returnedHash: ByteArray? = null - if (usf_fingerprint && checkbox_save_password.isChecked){ + if (checkbox_save_password.isChecked){ returnedHash = ByteArray(GocryptfsVolume.KeyLen) } sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash) @@ -173,10 +161,12 @@ class OpenActivity : BaseActivity() { if (checkbox_remember_path.isChecked) { savedVolumesAdapter.addVolumePath(rootCipherDir) } - if (checkbox_save_password.isChecked && returnedHash != null){ - fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ -> - stopTask { startExplorer() } - } + if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ + stopTask { + savePasswordHash(returnedHash) { + startExplorer() + } + } } else { stopTask { startExplorer() } } @@ -238,16 +228,6 @@ class OpenActivity : BaseActivity() { finish() } - fun onClickSavePasswordHash(view: View) { - if (checkbox_save_password.isChecked){ - if (!fingerprintPasswordHashSaver.canAuthenticate()){ - checkbox_save_password.isChecked = false - } else { - checkbox_remember_path.isChecked = checkbox_remember_path.isEnabled - } - } - } - fun onClickRememberPath(view: View) { if (!checkbox_remember_path.isChecked){ checkbox_save_password.isChecked = false @@ -268,12 +248,6 @@ class OpenActivity : BaseActivity() { finish() } } - if (::fingerprintPasswordHashSaver.isInitialized && fingerprintPasswordHashSaver.isListening){ - fingerprintPasswordHashSaver.stopListening() - if (fingerprintPasswordHashSaver.fingerprintFragment.isAdded){ - fingerprintPasswordHashSaver.fingerprintFragment.dismiss() - } - } } override fun onDestroy() { diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt new file mode 100644 index 0000000..6ea3200 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt @@ -0,0 +1,253 @@ +package sushi.hardcore.droidfs + +import android.app.KeyguardManager +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.KeyProperties +import android.util.Base64 +import android.view.View +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import kotlinx.android.synthetic.main.checkboxes_section.* +import kotlinx.android.synthetic.main.toolbar.* +import sushi.hardcore.droidfs.util.WidgetUtil +import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder +import java.security.KeyStore +import javax.crypto.* +import javax.crypto.spec.GCMParameterSpec + +open class VolumeActionActivity : BaseActivity() { + protected lateinit var rootCipherDir: String + private var usf_fingerprint = false + private var biometricCanAuthenticateCode: Int = -1 + private lateinit var biometricManager: BiometricManager + private lateinit var biometricPrompt: BiometricPrompt + private lateinit var keyStore: KeyStore + private lateinit var key: SecretKey + private lateinit var cipher: Cipher + private var actionMode: Int? = null + private lateinit var onAuthenticationResult: (success: Boolean) -> Unit + private lateinit var onPasswordDecrypted: (password: ByteArray) -> Unit + private lateinit var dataToProcess: ByteArray + 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 + } + + protected fun setupFingerprintStuff(){ + usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) { + biometricManager = BiometricManager.from(this) + biometricCanAuthenticateCode = canAuthenticate() + if (biometricCanAuthenticateCode == 0){ + val executor = ContextCompat.getMainExecutor(this) + val activityContext = this + val callback = object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Toast.makeText(applicationContext, errString, Toast.LENGTH_SHORT).show() + } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Toast.makeText(applicationContext, R.string.authentication_failed, Toast.LENGTH_SHORT).show() + } + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + val cipherObject = result.cryptoObject?.cipher + if (cipherObject != null){ + try { + when (actionMode) { + Cipher.ENCRYPT_MODE -> { + val cipherText = cipherObject.doFinal(dataToProcess) + val encodedCipherText = Base64.encodeToString(cipherText, 0) + val encodedIv = Base64.encodeToString(cipherObject.iv, 0) + val sharedPrefsEditor = sharedPrefs.edit() + sharedPrefsEditor.putString(rootCipherDir, "$encodedIv:$encodedCipherText") + sharedPrefsEditor.apply() + onAuthenticationResult(true) + } + Cipher.DECRYPT_MODE -> { + try { + val plainText = cipherObject.doFinal(dataToProcess) + onPasswordDecrypted(plainText) + } catch (e: AEADBadTagException){ + ColoredAlertDialogBuilder(activityContext) + .setTitle(R.string.error) + .setMessage(R.string.MAC_verification_failed) + .setPositiveButton(R.string.reset_hash_storage) { _, _ -> + resetHashStorage() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + } + } catch (e: IllegalBlockSizeException){ + ColoredAlertDialogBuilder(activityContext) + .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, null) + .show() + } + } else { + Toast.makeText(applicationContext, R.string.error_cipher_null, Toast.LENGTH_SHORT).show() + } + } + } + biometricPrompt = BiometricPrompt(this, executor, callback) + } + } else { + WidgetUtil.hide(checkbox_save_password) + } + } + + protected fun setupActionBar(){ + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + private fun canAuthenticate(): Int { + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + return if (!keyguardManager.isKeyguardSecure) { + 1 + } else { + when (biometricManager.canAuthenticate()){ + BiometricManager.BIOMETRIC_SUCCESS -> 0 + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> 2 + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> 3 + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> 4 + else -> -1 + } + } + } + + private fun printAuthenticateImpossibleError() { + Toast.makeText(this, when (biometricCanAuthenticateCode){ + 1 -> R.string.fingerprint_error_no_fingerprints + 2 -> R.string.fingerprint_error_hw_not_present + 3 -> R.string.fingerprint_error_hw_not_available + 4 -> R.string.fingerprint_error_no_fingerprints + else -> R.string.error + }, Toast.LENGTH_SHORT).show() + } + + fun onClickSavePasswordHash(view: View) { + if (checkbox_save_password.isChecked){ + if (biometricCanAuthenticateCode == 0){ + checkbox_remember_path.isChecked = checkbox_remember_path.isEnabled + } else { + checkbox_save_password.isChecked = false + printAuthenticateImpossibleError() + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun prepareCipher() { + 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(KeyProperties.KEY_ALGORITHM_AES+"/"+KeyProperties.BLOCK_MODE_GCM+"/"+KeyProperties.ENCRYPTION_PADDING_NONE) + } + + private fun alertKeyPermanentlyInvalidatedException(){ + ColoredAlertDialogBuilder(this) + .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, null) + .show() + } + + @RequiresApi(Build.VERSION_CODES.M) + protected fun savePasswordHash(plainText: ByteArray, onAuthenticationResult: (success: Boolean) -> Unit){ + val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(rootCipherDir) + .setSubtitle(getString(R.string.encrypt_action_description)) + .setDescription(getString(R.string.fingerprint_instruction)) + .setNegativeButtonText(getString(R.string.cancel)) + .setDeviceCredentialAllowed(false) + .setConfirmationRequired(false) + .build() + if (!::cipher.isInitialized){ + prepareCipher() + } + actionMode = Cipher.ENCRYPT_MODE + try { + cipher.init(Cipher.ENCRYPT_MODE, key) + this.onAuthenticationResult = onAuthenticationResult + dataToProcess = plainText + biometricPrompt.authenticate(biometricPromptInfo, BiometricPrompt.CryptoObject(cipher)) + } catch (e: KeyPermanentlyInvalidatedException){ + alertKeyPermanentlyInvalidatedException() + } + } + + @RequiresApi(Build.VERSION_CODES.M) + protected fun loadPasswordHash(cipherText: String, onPasswordDecrypted: (password: ByteArray) -> Unit){ + val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(rootCipherDir) + .setSubtitle(getString(R.string.decrypt_action_description)) + .setDescription(getString(R.string.fingerprint_instruction)) + .setNegativeButtonText(getString(R.string.cancel)) + .setDeviceCredentialAllowed(false) + .setConfirmationRequired(false) + .build() + this.onPasswordDecrypted = onPasswordDecrypted + actionMode = Cipher.DECRYPT_MODE + if (!::cipher.isInitialized){ + prepareCipher() + } + val encodedElements = cipherText.split(":") + dataToProcess = Base64.decode(encodedElements[1], 0) + val iv = Base64.decode(encodedElements[0], 0) + 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() + } + } + + private fun resetHashStorage() { + keyStore.deleteEntry(KEY_ALIAS) + val savedVolumePaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set + val sharedPrefsEditor = sharedPrefs.edit() + for (path in savedVolumePaths){ + val savedHash = sharedPrefs.getString(path, null) + if (savedHash != null){ + sharedPrefsEditor.remove(path) + } + } + sharedPrefsEditor.apply() + Toast.makeText(this, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/OpenAsDialogAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/OpenAsDialogAdapter.kt index 20f0f35..c7f4161 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/OpenAsDialogAdapter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/OpenAsDialogAdapter.kt @@ -4,7 +4,7 @@ import android.content.Context import sushi.hardcore.droidfs.R class OpenAsDialogAdapter(context: Context, showOpenWithExternalApp: Boolean) : IconTextDialogAdapter(context) { - private val openAsItems = mutableListOf( + private val openAsItems: MutableList> = mutableListOf( listOf("image", R.string.image, R.drawable.icon_file_image), listOf("video", R.string.video, R.drawable.icon_file_video), listOf("audio", R.string.audio, R.drawable.icon_file_audio), diff --git a/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintFragment.kt b/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintFragment.kt deleted file mode 100644 index 64538fd..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintFragment.kt +++ /dev/null @@ -1,31 +0,0 @@ -package sushi.hardcore.droidfs.fingerprint_stuff - -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.fragment.app.DialogFragment -import sushi.hardcore.droidfs.R - -class FingerprintFragment(val volume_path: String, val action_description: String, val callbackOnDismiss: () -> Unit) : DialogFragment() { - lateinit var image_fingerprint: ImageView - lateinit var text_instruction: TextView - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.fragment_fingerprint, container, false) - val text_volume = view.findViewById(R.id.text_volume) - text_volume.text = volume_path - image_fingerprint = view.findViewById(R.id.image_fingerprint) - val text_action_description = view.findViewById(R.id.text_action_description) - text_action_description.text = action_description - text_instruction = view.findViewById(R.id.text_instruction) - return view - } - - override fun onDismiss(dialog: DialogInterface) { - super.onDismiss(dialog) - callbackOnDismiss() - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintPasswordHashSaver.kt b/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintPasswordHashSaver.kt deleted file mode 100644 index 85473e6..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/fingerprint_stuff/FingerprintPasswordHashSaver.kt +++ /dev/null @@ -1,257 +0,0 @@ -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() - } - - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc1cec2..08de5bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,19 +75,15 @@ Save password hash using fingerprint Please touch the fingerprint sensor Failed to open the volume. The password may have changed. - Authenticated ! - Authentication error Authentication failed Delete only password hash or all saved volume data ? (This won\'t delete the volume itself) Delete all Delete password hash Are you sure you want to forget this volume path ? (This won\'t delete the volume itself) - Fingerprint permission denied - No fingerprint sensor detected - No fingerprint configured - This can happen if you have added a new fingerprint. The only solution is to reset the hash storage. + IllegalBlockSizeException + This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem. Reset hash storage - Signature/MAC verification failed. Either Android KeyStore or the saved hash has been modified. Reseting hash storage can solve this problem. + Signature/MAC verification failed. Either Android KeyStore or the saved hash has been modified. Resetting hash storage can solve this problem. Hash storage successfully reset Encrypting and saving password hash. Decrypting password hash. @@ -179,4 +175,7 @@ Failed to write the file. Gocryptfs volume not recognized. Please check the selected path. Version + Error cipher is null + KeyPermanentlyInvalidatedException + It looks like you have added a new fingerprint. Saved passwords hash have become unusable. diff --git a/build.gradle b/build.gradle index 74a610c..d5f6eb6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.3.72" + ext.kotlin_version = "1.4.10" repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } }