forked from hardcoresushi/DroidFS
Switching to androix.biometric
This commit is contained in:
parent
1bde428ace
commit
9a8023fc33
@ -17,6 +17,7 @@ android {
|
|||||||
versionCode 4
|
versionCode 4
|
||||||
versionName "1.1.9"
|
versionName "1.1.9"
|
||||||
|
|
||||||
|
android.ndkVersion = "21.2.6472646"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
abiFilters 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
}
|
}
|
||||||
@ -48,12 +49,13 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.0.2"
|
implementation "androidx.constraintlayout:constraintlayout:2.0.2"
|
||||||
|
|
||||||
implementation "androidx.sqlite:sqlite:2.1.0"
|
implementation "androidx.sqlite:sqlite-ktx:2.1.0"
|
||||||
implementation "androidx.preference:preference:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||||
implementation "com.jaredrummler:cyanea:1.0.2"
|
implementation "com.jaredrummler:cyanea:1.0.2"
|
||||||
implementation "com.github.bumptech.glide:glide:4.11.0"
|
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-core:2.11.7"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7"
|
implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7"
|
||||||
implementation "com.otaliastudios:cameraview:2.6.3"
|
implementation "com.otaliastudios:cameraview:2.6.3"
|
||||||
|
implementation "androidx.biometric:biometric:1.0.1"
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,18 @@
|
|||||||
android:protectionLevel="signature" />
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<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-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
|
<application
|
||||||
android:name=".ColoredApplication"
|
android:name=".ColoredApplication"
|
||||||
@ -24,7 +30,9 @@
|
|||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<activity android:name=".CameraActivity" android:screenOrientation="nosensor"/>
|
<activity
|
||||||
|
android:name=".CameraActivity"
|
||||||
|
android:screenOrientation="nosensor" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings"
|
||||||
|
@ -10,6 +10,7 @@ import sushi.hardcore.droidfs.widgets.ThemeColor
|
|||||||
|
|
||||||
open class BaseActivity: CyaneaAppCompatActivity() {
|
open class BaseActivity: CyaneaAppCompatActivity() {
|
||||||
protected lateinit var sharedPrefs: SharedPreferences
|
protected lateinit var sharedPrefs: SharedPreferences
|
||||||
|
protected var isRecreating = false
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
@ -24,6 +25,7 @@ open class BaseActivity: CyaneaAppCompatActivity() {
|
|||||||
fun changeThemeColor(themeColor: Int? = null){
|
fun changeThemeColor(themeColor: Int? = null){
|
||||||
val accentColor = themeColor ?: ThemeColor.getThemeColor(this)
|
val accentColor = themeColor ?: ThemeColor.getThemeColor(this)
|
||||||
val backgroundColor = ContextCompat.getColor(this, R.color.backgroundColor)
|
val backgroundColor = ContextCompat.getColor(this, R.color.backgroundColor)
|
||||||
|
isRecreating = true
|
||||||
cyanea.edit{
|
cyanea.edit{
|
||||||
accent(accentColor)
|
accent(accentColor)
|
||||||
//accentDark(themeColor)
|
//accentDark(themeColor)
|
||||||
|
@ -11,36 +11,24 @@ import android.widget.AdapterView.OnItemClickListener
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlinx.android.synthetic.main.activity_change_password.*
|
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.checkboxes_section.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
|
||||||
import kotlinx.android.synthetic.main.volume_path_section.*
|
import kotlinx.android.synthetic.main.volume_path_section.*
|
||||||
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
||||||
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
|
|
||||||
import sushi.hardcore.droidfs.util.*
|
import sushi.hardcore.droidfs.util.*
|
||||||
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ChangePasswordActivity : BaseActivity() {
|
class ChangePasswordActivity : VolumeActionActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PICK_DIRECTORY_REQUEST_CODE = 1
|
private const val PICK_DIRECTORY_REQUEST_CODE = 1
|
||||||
}
|
}
|
||||||
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_change_password)
|
setContentView(R.layout.activity_change_password)
|
||||||
setSupportActionBar(toolbar)
|
setupActionBar()
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
setupFingerprintStuff()
|
||||||
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)
|
|
||||||
}
|
|
||||||
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs)
|
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs)
|
||||||
if (savedVolumesAdapter.count > 0){
|
if (savedVolumesAdapter.count > 0){
|
||||||
saved_path_listview.adapter = savedVolumesAdapter
|
saved_path_listview.adapter = savedVolumesAdapter
|
||||||
@ -147,15 +135,15 @@ class ChangePasswordActivity : BaseActivity() {
|
|||||||
override fun doTask(activity: AppCompatActivity) {
|
override fun doTask(activity: AppCompatActivity) {
|
||||||
val oldPassword = edit_old_password.text.toString().toCharArray()
|
val oldPassword = edit_old_password.text.toString().toCharArray()
|
||||||
var returnedHash: ByteArray? = null
|
var returnedHash: ByteArray? = null
|
||||||
if (usf_fingerprint && checkbox_save_password.isChecked) {
|
if (checkbox_save_password.isChecked) {
|
||||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||||
}
|
}
|
||||||
var changePasswordImmediately = true
|
var changePasswordImmediately = true
|
||||||
if (givenHash == null) {
|
if (givenHash == null) {
|
||||||
val cipherText = sharedPrefs.getString(rootCipherDir, 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 {
|
stopTask {
|
||||||
fingerprintPasswordHashSaver.decrypt(cipherText, rootCipherDir, ::changePassword)
|
loadPasswordHash(cipherText, ::changePassword)
|
||||||
}
|
}
|
||||||
changePasswordImmediately = false
|
changePasswordImmediately = false
|
||||||
}
|
}
|
||||||
@ -177,9 +165,11 @@ class ChangePasswordActivity : BaseActivity() {
|
|||||||
if (checkbox_remember_path.isChecked) {
|
if (checkbox_remember_path.isChecked) {
|
||||||
savedVolumesAdapter.addVolumePath(rootCipherDir)
|
savedVolumesAdapter.addVolumePath(rootCipherDir)
|
||||||
}
|
}
|
||||||
if (checkbox_save_password.isChecked && returnedHash != null) {
|
if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||||
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ ->
|
stopTask {
|
||||||
stopTask { onPasswordChanged() }
|
savePasswordHash(returnedHash) {
|
||||||
|
onPasswordChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stopTask { onPasswordChanged() }
|
stopTask { onPasswordChanged() }
|
||||||
@ -213,32 +203,12 @@ class ChangePasswordActivity : BaseActivity() {
|
|||||||
.show()
|
.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) {
|
fun onClickRememberPath(view: View) {
|
||||||
if (!checkbox_remember_path.isChecked){
|
if (!checkbox_remember_path.isChecked){
|
||||||
checkbox_save_password.isChecked = false
|
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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Wiper.wipeEditText(edit_old_password)
|
Wiper.wipeEditText(edit_old_password)
|
||||||
|
@ -9,34 +9,26 @@ import android.widget.Toast
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlinx.android.synthetic.main.activity_create.*
|
import kotlinx.android.synthetic.main.activity_create.*
|
||||||
import kotlinx.android.synthetic.main.checkboxes_section.*
|
import kotlinx.android.synthetic.main.checkboxes_section.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
|
||||||
import kotlinx.android.synthetic.main.volume_path_section.*
|
import kotlinx.android.synthetic.main.volume_path_section.*
|
||||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||||
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
|
import sushi.hardcore.droidfs.util.GocryptfsVolume
|
||||||
import sushi.hardcore.droidfs.util.*
|
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 sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CreateActivity : BaseActivity() {
|
class CreateActivity : VolumeActionActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PICK_DIRECTORY_REQUEST_CODE = 1
|
private const val PICK_DIRECTORY_REQUEST_CODE = 1
|
||||||
}
|
}
|
||||||
private lateinit var fingerprintPasswordHashSaver: FingerprintPasswordHashSaver
|
|
||||||
private lateinit var rootCipherDir: String
|
|
||||||
private var sessionID = -1
|
private var sessionID = -1
|
||||||
private var usf_fingerprint = false
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_create)
|
setContentView(R.layout.activity_create)
|
||||||
setSupportActionBar(toolbar)
|
setupActionBar()
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
setupFingerprintStuff()
|
||||||
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)
|
|
||||||
}
|
|
||||||
edit_password_confirm.setOnEditorActionListener { v, _, _ ->
|
edit_password_confirm.setOnEditorActionListener { v, _, _ ->
|
||||||
onClickCreate(v)
|
onClickCreate(v)
|
||||||
true
|
true
|
||||||
@ -127,7 +119,7 @@ class CreateActivity : BaseActivity() {
|
|||||||
if (goodDirectory) {
|
if (goodDirectory) {
|
||||||
if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
|
if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
|
||||||
var returnedHash: ByteArray? = null
|
var returnedHash: ByteArray? = null
|
||||||
if (usf_fingerprint && checkbox_save_password.isChecked){
|
if (checkbox_save_password.isChecked){
|
||||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||||
}
|
}
|
||||||
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
|
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
|
||||||
@ -146,9 +138,11 @@ class CreateActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
editor.apply()
|
editor.apply()
|
||||||
}
|
}
|
||||||
if (checkbox_save_password.isChecked && returnedHash != null){
|
if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||||
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir){ _ ->
|
stopTask {
|
||||||
stopTask { startExplorer() }
|
savePasswordHash(returnedHash) {
|
||||||
|
startExplorer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stopTask { startExplorer() }
|
stopTask { startExplorer() }
|
||||||
@ -191,32 +185,12 @@ class CreateActivity : BaseActivity() {
|
|||||||
.show()
|
.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) {
|
fun onClickRememberPath(view: View) {
|
||||||
if (!checkbox_remember_path.isChecked){
|
if (!checkbox_remember_path.isChecked){
|
||||||
checkbox_save_password.isChecked = false
|
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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Wiper.wipeEditText(edit_password)
|
Wiper.wipeEditText(edit_password)
|
||||||
|
@ -22,22 +22,30 @@ class MainActivity : BaseActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) +
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) +
|
||||||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSIONS_REQUEST)
|
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSIONS_REQUEST)
|
||||||
} else {
|
} else {
|
||||||
|
onStoragePermissionGranted()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onStoragePermissionGranted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStoragePermissionGranted(){
|
||||||
if (checkStorageAvailability()){
|
if (checkStorageAvailability()){
|
||||||
checkFirstOpening()
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkStorageAvailability(): Boolean {
|
private fun checkStorageAvailability(): Boolean {
|
||||||
val state = Environment.getExternalStorageState()
|
val state = Environment.getExternalStorageState()
|
||||||
@ -81,8 +89,7 @@ class MainActivity : BaseActivity() {
|
|||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }.show()
|
.setPositiveButton(R.string.ok) { _, _ -> finish() }.show()
|
||||||
} else {
|
} else {
|
||||||
checkStorageAvailability()
|
onStoragePermissionGranted()
|
||||||
checkFirstOpening()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,43 +12,31 @@ import android.widget.AdapterView.OnItemClickListener
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlinx.android.synthetic.main.activity_open.*
|
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.checkboxes_section.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
|
||||||
import kotlinx.android.synthetic.main.volume_path_section.*
|
import kotlinx.android.synthetic.main.volume_path_section.*
|
||||||
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
||||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
|
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
|
||||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
|
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
|
||||||
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
|
|
||||||
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
|
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
|
||||||
import sushi.hardcore.droidfs.util.*
|
import sushi.hardcore.droidfs.util.*
|
||||||
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class OpenActivity : BaseActivity() {
|
class OpenActivity : VolumeActionActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PICK_DIRECTORY_REQUEST_CODE = 1
|
private const val PICK_DIRECTORY_REQUEST_CODE = 1
|
||||||
}
|
}
|
||||||
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
|
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
|
||||||
private lateinit var fingerprintPasswordHashSaver: FingerprintPasswordHashSaver
|
|
||||||
private lateinit var rootCipherDir: String
|
|
||||||
private var sessionID = -1
|
private var sessionID = -1
|
||||||
private var isStartingActivity = false
|
private var isStartingActivity = false
|
||||||
private var isFinishingIntentionally = false
|
private var isFinishingIntentionally = false
|
||||||
private var usf_fingerprint = false
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_open)
|
setContentView(R.layout.activity_open)
|
||||||
setSupportActionBar(toolbar)
|
setupActionBar()
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
setupFingerprintStuff()
|
||||||
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)
|
|
||||||
}
|
|
||||||
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs)
|
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs)
|
||||||
if (savedVolumesAdapter.count > 0){
|
if (savedVolumesAdapter.count > 0){
|
||||||
saved_path_listview.adapter = savedVolumesAdapter
|
saved_path_listview.adapter = savedVolumesAdapter
|
||||||
@ -56,8 +44,8 @@ class OpenActivity : BaseActivity() {
|
|||||||
rootCipherDir = savedVolumesAdapter.getItem(position)
|
rootCipherDir = savedVolumesAdapter.getItem(position)
|
||||||
edit_volume_path.setText(rootCipherDir)
|
edit_volume_path.setText(rootCipherDir)
|
||||||
val cipherText = sharedPrefs.getString(rootCipherDir, 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
|
||||||
fingerprintPasswordHashSaver.decrypt(cipherText, rootCipherDir, ::openUsingPasswordHash)
|
loadPasswordHash(cipherText, ::openUsingPasswordHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -165,7 +153,7 @@ class OpenActivity : BaseActivity() {
|
|||||||
override fun doTask(activity: AppCompatActivity) {
|
override fun doTask(activity: AppCompatActivity) {
|
||||||
val password = edit_password.text.toString().toCharArray()
|
val password = edit_password.text.toString().toCharArray()
|
||||||
var returnedHash: ByteArray? = null
|
var returnedHash: ByteArray? = null
|
||||||
if (usf_fingerprint && checkbox_save_password.isChecked){
|
if (checkbox_save_password.isChecked){
|
||||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||||
}
|
}
|
||||||
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
|
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
|
||||||
@ -173,9 +161,11 @@ class OpenActivity : BaseActivity() {
|
|||||||
if (checkbox_remember_path.isChecked) {
|
if (checkbox_remember_path.isChecked) {
|
||||||
savedVolumesAdapter.addVolumePath(rootCipherDir)
|
savedVolumesAdapter.addVolumePath(rootCipherDir)
|
||||||
}
|
}
|
||||||
if (checkbox_save_password.isChecked && returnedHash != null){
|
if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||||
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ ->
|
stopTask {
|
||||||
stopTask { startExplorer() }
|
savePasswordHash(returnedHash) {
|
||||||
|
startExplorer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stopTask { startExplorer() }
|
stopTask { startExplorer() }
|
||||||
@ -238,16 +228,6 @@ class OpenActivity : BaseActivity() {
|
|||||||
finish()
|
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) {
|
fun onClickRememberPath(view: View) {
|
||||||
if (!checkbox_remember_path.isChecked){
|
if (!checkbox_remember_path.isChecked){
|
||||||
checkbox_save_password.isChecked = false
|
checkbox_save_password.isChecked = false
|
||||||
@ -268,12 +248,6 @@ class OpenActivity : BaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (::fingerprintPasswordHashSaver.isInitialized && fingerprintPasswordHashSaver.isListening){
|
|
||||||
fingerprintPasswordHashSaver.stopListening()
|
|
||||||
if (fingerprintPasswordHashSaver.fingerprintFragment.isAdded){
|
|
||||||
fingerprintPasswordHashSaver.fingerprintFragment.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
253
app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt
Normal file
253
app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import sushi.hardcore.droidfs.R
|
import sushi.hardcore.droidfs.R
|
||||||
|
|
||||||
class OpenAsDialogAdapter(context: Context, showOpenWithExternalApp: Boolean) : IconTextDialogAdapter(context) {
|
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("image", R.string.image, R.drawable.icon_file_image),
|
||||||
listOf("video", R.string.video, R.drawable.icon_file_video),
|
listOf("video", R.string.video, R.drawable.icon_file_video),
|
||||||
listOf("audio", R.string.audio, R.drawable.icon_file_audio),
|
listOf("audio", R.string.audio, R.drawable.icon_file_audio),
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -75,19 +75,15 @@
|
|||||||
<string name="fingerprint_save_checkbox_text">Save password hash using fingerprint</string>
|
<string name="fingerprint_save_checkbox_text">Save password hash using fingerprint</string>
|
||||||
<string name="fingerprint_instruction">Please touch the fingerprint sensor</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="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="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_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_all">Delete all</string>
|
||||||
<string name="delete_hash">Delete password hash</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="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="illegal_block_size_exception">IllegalBlockSizeException</string>
|
||||||
<string name="no_fingerprint_sensor">No fingerprint sensor detected</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="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="reset_hash_storage">Reset hash storage</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="hash_storage_reset">Hash storage successfully reset</string>
|
||||||
<string name="encrypt_action_description">Encrypting and saving password hash.</string>
|
<string name="encrypt_action_description">Encrypting and saving password hash.</string>
|
||||||
<string name="decrypt_action_description">Decrypting 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="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="error_not_a_volume">Gocryptfs volume not recognized. Please check the selected path.</string>
|
||||||
<string name="version">Version</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>
|
</resources>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.3.72"
|
ext.kotlin_version = "1.4.10"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user