Switching to androix.biometric

This commit is contained in:
Matéo Duparc 2020-10-22 22:14:22 +02:00
parent 1bde428ace
commit 9a8023fc33
13 changed files with 339 additions and 438 deletions

View File

@ -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"
}

View File

@ -9,12 +9,18 @@
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-feature android:name="android.hardware.camera.any" android:required="false"/>
<uses-feature android:name="android.hardware.fingerprint" android:required="false"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove"/> <!--removing this permission automatically added by exoplayer-->
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
<uses-feature
android:name="android.hardware.fingerprint"
android:required="false" />
<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE"
tools:node="remove" /> <!--removing this permission automatically added by exoplayer-->
<application
android:name=".ColoredApplication"
@ -24,7 +30,9 @@
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".CameraActivity" android:screenOrientation="nosensor"/>
<activity
android:name=".CameraActivity"
android:screenOrientation="nosensor" />
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"

View File

@ -10,6 +10,7 @@ import sushi.hardcore.droidfs.widgets.ThemeColor
open class BaseActivity: CyaneaAppCompatActivity() {
protected lateinit var sharedPrefs: SharedPreferences
protected var isRecreating = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
@ -24,6 +25,7 @@ open class BaseActivity: CyaneaAppCompatActivity() {
fun changeThemeColor(themeColor: Int? = null){
val accentColor = themeColor ?: ThemeColor.getThemeColor(this)
val backgroundColor = ContextCompat.getColor(this, R.color.backgroundColor)
isRecreating = true
cyanea.edit{
accent(accentColor)
//accentDark(themeColor)

View File

@ -11,36 +11,24 @@ import android.widget.AdapterView.OnItemClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_change_password.*
import kotlinx.android.synthetic.main.activity_change_password.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.fingerprint_stuff.FingerprintPasswordHashSaver
import sushi.hardcore.droidfs.util.*
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File
import java.util.*
class ChangePasswordActivity : BaseActivity() {
class ChangePasswordActivity : 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 usf_fingerprint = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_change_password)
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
@ -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)

View File

@ -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)

View File

@ -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()
}
}
}

View File

@ -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() {

View File

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

View File

@ -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<List<Any>> = 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),

View File

@ -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<TextView>(R.id.text_volume)
text_volume.text = volume_path
image_fingerprint = view.findViewById(R.id.image_fingerprint)
val text_action_description = view.findViewById<TextView>(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()
}
}

View File

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

View File

@ -75,19 +75,15 @@
<string name="fingerprint_save_checkbox_text">Save password hash using fingerprint</string>
<string name="fingerprint_instruction">Please touch the fingerprint sensor</string>
<string name="open_failed_hash_msg">Failed to open the volume. The password may have changed.</string>
<string name="authenticated">Authenticated !</string>
<string name="authentication_error">Authentication error</string>
<string name="authentication_failed">Authentication failed</string>
<string name="delete_hash_or_all">Delete only password hash or all saved volume data ? (This won\'t delete the volume itself)</string>
<string name="delete_all">Delete all</string>
<string name="delete_hash">Delete password hash</string>
<string name="ask_delete_volume_path">Are you sure you want to forget this volume path ? (This won\'t delete the volume itself)</string>
<string name="fingerprint_perm_denied">Fingerprint permission denied</string>
<string name="no_fingerprint_sensor">No fingerprint sensor detected</string>
<string name="no_fingerprint_configured">No fingerprint configured</string>
<string name="authentication_error_msg">This can happen if you have added a new fingerprint. The only solution is to reset the hash storage.</string>
<string name="illegal_block_size_exception">IllegalBlockSizeException</string>
<string name="illegal_block_size_exception_msg">This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem.</string>
<string name="reset_hash_storage">Reset hash storage</string>
<string name="MAC_verification_failed">Signature/MAC verification failed. Either Android KeyStore or the saved hash has been modified. Reseting hash storage can solve this problem.</string>
<string name="MAC_verification_failed">Signature/MAC verification failed. Either Android KeyStore or the saved hash has been modified. Resetting hash storage can solve this problem.</string>
<string name="hash_storage_reset">Hash storage successfully reset</string>
<string name="encrypt_action_description">Encrypting and saving password hash.</string>
<string name="decrypt_action_description">Decrypting password hash.</string>
@ -179,4 +175,7 @@
<string name="file_write_failed">Failed to write the file.</string>
<string name="error_not_a_volume">Gocryptfs volume not recognized. Please check the selected path.</string>
<string name="version">Version</string>
<string name="error_cipher_null">Error cipher is null</string>
<string name="key_permanently_invalidated_exception">KeyPermanentlyInvalidatedException</string>
<string name="key_permanently_invalidated_exception_msg">It looks like you have added a new fingerprint. Saved passwords hash have become unusable.</string>
</resources>

View File

@ -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"
}
}