diff --git a/app/build.gradle b/app/build.gradle index 1f2a19e..13b0736 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,6 @@ dependencies { 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 "com.otaliastudios:cameraview:2.6.4" implementation "androidx.biometric:biometric:1.0.1" } diff --git a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt index f45c2b9..6711ec6 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt @@ -29,25 +29,34 @@ class ChangePasswordActivity : VolumeActionActivity() { setContentView(R.layout.activity_change_password) setupActionBar() setupFingerprintStuff() - savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs) + savedVolumesAdapter = SavedVolumesAdapter(this, volumeDatabase) if (savedVolumesAdapter.count > 0){ saved_path_listview.adapter = savedVolumesAdapter saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ -> - edit_volume_path.setText(savedVolumesAdapter.getItem(position)) + val volume = savedVolumesAdapter.getItem(position) + currentVolumeName = volume.name + if (volume.isHidden){ + switch_hidden_volume.isChecked = true + edit_volume_name.setText(currentVolumeName) + } else { + switch_hidden_volume.isChecked = false + edit_volume_path.setText(currentVolumeName) + } + onClickSwitchHiddenVolume(switch_hidden_volume) } } else { - WidgetUtil.hide(saved_path_listview) + WidgetUtil.hideWithPadding(saved_path_listview) } - edit_volume_path.addTextChangedListener(object: TextWatcher{ + val textWatcher = object: TextWatcher{ override fun afterTextChanged(s: Editable?) { } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (savedVolumesAdapter.isPathSaved(s.toString())){ + if (volumeDatabase.isVolumeSaved(s.toString())){ checkbox_remember_path.isEnabled = false checkbox_remember_path.isChecked = false - if (sharedPrefs.getString(s.toString(), null) != null){ + if (volumeDatabase.isHashSaved(s.toString())){ edit_old_password.text = null edit_old_password.hint = getString(R.string.hash_saved_hint) edit_old_password.isEnabled = false @@ -61,7 +70,9 @@ class ChangePasswordActivity : VolumeActionActivity() { edit_old_password.isEnabled = true } } - }) + } + edit_volume_path.addTextChangedListener(textWatcher) + edit_volume_name.addTextChangedListener(textWatcher) edit_new_password_confirm.setOnEditorActionListener { v, _, _ -> onClickChangePassword(v) true @@ -102,30 +113,27 @@ class ChangePasswordActivity : VolumeActionActivity() { } fun onClickChangePassword(view: View?) { - rootCipherDir = edit_volume_path.text.toString() - if (rootCipherDir.isEmpty()) { - Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show() - } else { - val rootCipherDirFile = File(rootCipherDir) - if (!GocryptfsVolume.isGocryptfsVolume(rootCipherDirFile)){ + loadVolumePath { + val volumeFile = File(currentVolumePath) + if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){ ColoredAlertDialogBuilder(this) .setTitle(R.string.error) .setMessage(R.string.error_not_a_volume) .setPositiveButton(R.string.ok, null) .show() - } else if (!rootCipherDirFile.canWrite()){ + } else if (!volumeFile.canWrite()){ ColoredAlertDialogBuilder(this) .setTitle(R.string.warning) .setMessage(R.string.change_pwd_cant_write_error_msg) .setPositiveButton(R.string.ok, null) .show() } else { - changePassword(null) + changePassword() } } } - private fun changePassword(givenHash: ByteArray?){ + private fun changePassword(givenHash: ByteArray? = null){ val newPassword = edit_new_password.text.toString().toCharArray() val newPasswordConfirm = edit_new_password_confirm.text.toString().toCharArray() if (!newPassword.contentEquals(newPasswordConfirm)) { @@ -140,30 +148,38 @@ class ChangePasswordActivity : VolumeActionActivity() { } var changePasswordImmediately = true if (givenHash == null) { - val cipherText = sharedPrefs.getString(rootCipherDir, null) - if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //password hash saved - stopTask { - loadPasswordHash(cipherText, ::changePassword) + var volume: Volume? = null + volumeDatabase.getVolumes().forEach { testVolume -> + if (testVolume.name == currentVolumeName){ + volume = testVolume + } + } + volume?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ + it.hash?.let { hash -> + it.iv?.let { iv -> + currentVolumePath = if (it.isHidden){ + PathUtils.pathJoin(filesDir.path, it.name) + } else { + it.name + } + stopTask { + loadPasswordHash(hash, iv, ::changePassword) + } + changePasswordImmediately = false + } + } } - changePasswordImmediately = false } } if (changePasswordImmediately) { - if (GocryptfsVolume.changePassword( - rootCipherDir, - oldPassword, - givenHash, - newPassword, - returnedHash - ) - ) { - if (sharedPrefs.getString(rootCipherDir, null) != null) { - val editor = sharedPrefs.edit() - editor.remove(rootCipherDir) - editor.apply() + if (GocryptfsVolume.changePassword(currentVolumePath, oldPassword, givenHash, newPassword, returnedHash)) { + val volume = Volume(currentVolumeName, switch_hidden_volume.isChecked) + if (volumeDatabase.isHashSaved(currentVolumeName)) { + volumeDatabase.removeHash(volume) } if (checkbox_remember_path.isChecked) { - savedVolumesAdapter.addVolumePath(rootCipherDir) + volumeDatabase.saveVolume(volume) } if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ stopTask { diff --git a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt index c7cc0a6..9e8ba28 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt @@ -7,7 +7,7 @@ class ConstValues { companion object { const val creator = "DroidFS" const val gocryptfsConfFilename = "gocryptfs.conf" - const val saved_volumes_key = "saved_volumes" + const val volumeDatabaseName = "SavedVolumes" const val sort_order_key = "sort_order" val fakeUri: Uri = Uri.parse("fakeuri://droidfs") const val MAX_KERNEL_WRITE = 128*1024 diff --git a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt index 7e18f82..3cdb270 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt @@ -11,10 +11,7 @@ import kotlinx.android.synthetic.main.activity_create.* import kotlinx.android.synthetic.main.checkboxes_section.* import kotlinx.android.synthetic.main.volume_path_section.* import sushi.hardcore.droidfs.explorers.ExplorerActivity -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.util.* import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import java.io.File import java.util.* @@ -69,10 +66,7 @@ class CreateActivity : VolumeActionActivity() { } fun onClickCreate(view: View?) { - rootCipherDir = edit_volume_path.text.toString() - if (rootCipherDir.isEmpty()) { - Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show() - } else { + loadVolumePath { val password = edit_password.text.toString().toCharArray() val passwordConfirm = edit_password_confirm.text.toString().toCharArray() if (!password.contentEquals(passwordConfirm)) { @@ -80,10 +74,10 @@ class CreateActivity : VolumeActionActivity() { } else { object: LoadingTask(this, R.string.loading_msg_create){ override fun doTask(activity: AppCompatActivity) { - val volumePathFile = File(rootCipherDir) + val volumeFile = File(currentVolumePath) var goodDirectory = false - if (!volumePathFile.isDirectory) { - if (volumePathFile.mkdirs()) { + if (!volumeFile.isDirectory) { + if (volumeFile.mkdirs()) { goodDirectory = true } else { stopTask { @@ -95,10 +89,10 @@ class CreateActivity : VolumeActionActivity() { } } } else { - val dirContent = volumePathFile.list() + val dirContent = volumeFile.list() if (dirContent != null){ if (dirContent.isEmpty()) { - if (volumePathFile.canWrite()){ + if (volumeFile.canWrite()){ goodDirectory = true } else { stopTask { @@ -117,26 +111,18 @@ class CreateActivity : VolumeActionActivity() { } } if (goodDirectory) { - if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) { + if (GocryptfsVolume.createVolume(currentVolumePath, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) { var returnedHash: ByteArray? = null if (checkbox_save_password.isChecked){ returnedHash = ByteArray(GocryptfsVolume.KeyLen) } - sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash) + sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash) if (sessionID != -1) { if (checkbox_remember_path.isChecked) { - val oldSavedVolumesPaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set - val editor = sharedPrefs.edit() - val newSavedVolumesPaths = oldSavedVolumesPaths.toMutableList() - if (oldSavedVolumesPaths.contains(rootCipherDir)) { - if (sharedPrefs.getString(rootCipherDir, null) != null){ - editor.remove(rootCipherDir) - } - } else { - newSavedVolumesPaths.add(rootCipherDir) - editor.putStringSet(ConstValues.saved_volumes_key, newSavedVolumesPaths.toSet()) + if (volumeDatabase.isVolumeSaved(currentVolumeName)) { + volumeDatabase.removeVolume(Volume(currentVolumeName)) } - editor.apply() + volumeDatabase.saveVolume(Volume(currentVolumeName, switch_hidden_volume.isChecked)) } if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ stopTask { @@ -178,7 +164,7 @@ class CreateActivity : VolumeActionActivity() { .setPositiveButton(R.string.ok) { _, _ -> val intent = Intent(applicationContext, ExplorerActivity::class.java) intent.putExtra("sessionID", sessionID) - intent.putExtra("volume_name", File(rootCipherDir).name) + intent.putExtra("volume_name", File(currentVolumeName).name) startActivity(intent) finish() } diff --git a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt index c09fdc4..8fa720b 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt @@ -9,7 +9,6 @@ import android.text.TextWatcher import android.view.MenuItem import android.view.View 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.checkboxes_section.* @@ -37,30 +36,46 @@ class OpenActivity : VolumeActionActivity() { setContentView(R.layout.activity_open) setupActionBar() setupFingerprintStuff() - savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs) + savedVolumesAdapter = SavedVolumesAdapter(this, volumeDatabase) if (savedVolumesAdapter.count > 0){ saved_path_listview.adapter = savedVolumesAdapter saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ -> - rootCipherDir = savedVolumesAdapter.getItem(position) - edit_volume_path.setText(rootCipherDir) - val cipherText = sharedPrefs.getString(rootCipherDir, null) - if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ //password hash saved - loadPasswordHash(cipherText, ::openUsingPasswordHash) + val volume = savedVolumesAdapter.getItem(position) + currentVolumeName = volume.name + if (volume.isHidden){ + switch_hidden_volume.isChecked = true + edit_volume_name.setText(currentVolumeName) + } else { + switch_hidden_volume.isChecked = false + edit_volume_path.setText(currentVolumeName) + } + onClickSwitchHiddenVolume(switch_hidden_volume) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ + volume.hash?.let { hash -> + volume.iv?.let { iv -> + currentVolumePath = if (volume.isHidden){ + PathUtils.pathJoin(filesDir.path, volume.name) + } else { + volume.name + } + loadPasswordHash(hash, iv, ::openUsingPasswordHash) + } + } } } } else { - WidgetUtil.hide(saved_path_listview) + WidgetUtil.hideWithPadding(saved_path_listview) } - edit_volume_path.addTextChangedListener(object: TextWatcher { + val textWatcher = object: TextWatcher { override fun afterTextChanged(s: Editable?) { } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (savedVolumesAdapter.isPathSaved(s.toString())){ + if (volumeDatabase.isVolumeSaved(s.toString())){ checkbox_remember_path.isEnabled = false checkbox_remember_path.isChecked = false - if (sharedPrefs.getString(s.toString(), null) != null){ + if (volumeDatabase.isHashSaved(s.toString())){ checkbox_save_password.isEnabled = false checkbox_save_password.isChecked = false } else { @@ -71,7 +86,9 @@ class OpenActivity : VolumeActionActivity() { checkbox_save_password.isEnabled = true } } - }) + } + edit_volume_path.addTextChangedListener(textWatcher) + edit_volume_name.addTextChangedListener(textWatcher) edit_password.setOnEditorActionListener { v, _, _ -> onClickOpen(v) true @@ -116,24 +133,15 @@ class OpenActivity : VolumeActionActivity() { } fun onClickOpen(view: View?) { - rootCipherDir = edit_volume_path.text.toString() - if (rootCipherDir.isEmpty()) { - Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show() - } else { - val rootCipherDirFile = File(rootCipherDir) - if (!rootCipherDirFile.canRead()) { - ColoredAlertDialogBuilder(this) - .setTitle(R.string.error) - .setMessage(R.string.open_cant_read_error) - .setPositiveButton(R.string.ok, null) - .show() - } else if (!GocryptfsVolume.isGocryptfsVolume(rootCipherDirFile)){ + loadVolumePath { + val volumeFile = File(currentVolumePath) + if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){ ColoredAlertDialogBuilder(this) .setTitle(R.string.error) .setMessage(R.string.error_not_a_volume) .setPositiveButton(R.string.ok, null) .show() - } else if (!rootCipherDirFile.canWrite()) { + } else if (!volumeFile.canWrite()) { if ((intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null) { //import via android share menu ColoredAlertDialogBuilder(this) .setTitle(R.string.error) @@ -145,7 +153,7 @@ class OpenActivity : VolumeActionActivity() { .setTitle(R.string.warning) .setCancelable(false) .setPositiveButton(R.string.ok) { _, _ -> openVolume() } - if (PathUtils.isPathOnExternalStorage(rootCipherDir, this)){ + if (PathUtils.isPathOnExternalStorage(currentVolumeName, this)){ dialog.setMessage(R.string.open_on_sdcard_warning) } else { dialog.setMessage(R.string.open_cant_write_warning) @@ -166,10 +174,10 @@ class OpenActivity : VolumeActionActivity() { if (checkbox_save_password.isChecked){ returnedHash = ByteArray(GocryptfsVolume.KeyLen) } - sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash) + sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash) if (sessionID != -1) { if (checkbox_remember_path.isChecked) { - savedVolumesAdapter.addVolumePath(rootCipherDir) + volumeDatabase.saveVolume(Volume(currentVolumeName, switch_hidden_volume.isChecked)) } if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ stopTask { @@ -201,7 +209,7 @@ class OpenActivity : VolumeActionActivity() { private fun openUsingPasswordHash(passwordHash: ByteArray){ object : LoadingTask(this, R.string.loading_msg_open){ override fun doTask(activity: AppCompatActivity) { - sessionID = GocryptfsVolume.init(rootCipherDir, null, passwordHash, null) + sessionID = GocryptfsVolume.init(currentVolumePath, null, passwordHash, null) if (sessionID != -1){ stopTask { startExplorer() } } else { @@ -236,7 +244,7 @@ class OpenActivity : VolumeActionActivity() { explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening } explorerIntent.putExtra("sessionID", sessionID) - explorerIntent.putExtra("volume_name", File(rootCipherDir).name) + explorerIntent.putExtra("volume_name", File(currentVolumeName).name) startActivity(explorerIntent) isFinishingIntentionally = true finish() diff --git a/app/src/main/java/sushi/hardcore/droidfs/Volume.kt b/app/src/main/java/sushi/hardcore/droidfs/Volume.kt new file mode 100644 index 0000000..d829e35 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/Volume.kt @@ -0,0 +1,3 @@ +package sushi.hardcore.droidfs + +class Volume(val name: String, val isHidden: Boolean = false, var hash: ByteArray? = null, var iv: ByteArray? = null) \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt index be6a893..1eaf85b 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt @@ -6,8 +6,8 @@ import android.os.Build 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.LinearLayout import android.widget.Toast import androidx.annotation.RequiresApi import androidx.biometric.BiometricManager @@ -15,6 +15,8 @@ import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat 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.util.PathUtils import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import java.security.KeyStore @@ -22,7 +24,9 @@ import javax.crypto.* import javax.crypto.spec.GCMParameterSpec open class VolumeActionActivity : BaseActivity() { - protected lateinit var rootCipherDir: String + protected lateinit var currentVolumeName: String + protected lateinit var currentVolumePath: String + protected lateinit var volumeDatabase: VolumeDatabase private var usf_fingerprint = false private var biometricCanAuthenticateCode: Int = -1 private lateinit var biometricManager: BiometricManager @@ -30,10 +34,13 @@ open class VolumeActionActivity : BaseActivity() { private lateinit var keyStore: KeyStore private lateinit var key: SecretKey private lateinit var cipher: Cipher + private var isCipherReady = false private var actionMode: Int? = null private lateinit var onAuthenticationResult: (success: Boolean) -> Unit private lateinit var onPasswordDecrypted: (password: ByteArray) -> Unit private lateinit var dataToProcess: ByteArray + private lateinit var originalHiddenVolumeSectionLayoutParams: LinearLayout.LayoutParams + private lateinit var originalNormalVolumeSectionLayoutParams: LinearLayout.LayoutParams companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" private const val KEY_ALIAS = "Hash Key" @@ -42,6 +49,10 @@ open class VolumeActionActivity : BaseActivity() { } protected fun setupFingerprintStuff(){ + originalHiddenVolumeSectionLayoutParams = hidden_volume_section.layoutParams as LinearLayout.LayoutParams + originalNormalVolumeSectionLayoutParams = normal_volume_section.layoutParams as LinearLayout.LayoutParams + WidgetUtil.hide(hidden_volume_section) + volumeDatabase = VolumeDatabase(this) usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) { biometricManager = BiometricManager.from(this) @@ -73,12 +84,7 @@ open class VolumeActionActivity : BaseActivity() { 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() - success = true + success = volumeDatabase.addHash(Volume(currentVolumeName, switch_hidden_volume.isChecked, cipherText, cipherObject.iv)) } Cipher.DECRYPT_MODE -> { try { @@ -117,7 +123,7 @@ open class VolumeActionActivity : BaseActivity() { biometricPrompt = BiometricPrompt(this, executor, callback) } } else { - WidgetUtil.hide(checkbox_save_password) + WidgetUtil.hideWithPadding(checkbox_save_password) } } @@ -182,6 +188,7 @@ open class VolumeActionActivity : BaseActivity() { keyGenerator.generateKey() } cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES+"/"+KeyProperties.BLOCK_MODE_GCM+"/"+KeyProperties.ENCRYPTION_PADDING_NONE) + isCipherReady = true } private fun alertKeyPermanentlyInvalidatedException(){ @@ -198,14 +205,14 @@ open class VolumeActionActivity : BaseActivity() { @RequiresApi(Build.VERSION_CODES.M) protected fun savePasswordHash(plainText: ByteArray, onAuthenticationResult: (success: Boolean) -> Unit){ val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(rootCipherDir) + .setTitle(currentVolumeName) .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){ + if (!isCipherReady){ prepareCipher() } actionMode = Cipher.ENCRYPT_MODE @@ -220,9 +227,9 @@ open class VolumeActionActivity : BaseActivity() { } @RequiresApi(Build.VERSION_CODES.M) - protected fun loadPasswordHash(cipherText: String, onPasswordDecrypted: (password: ByteArray) -> Unit){ + protected fun loadPasswordHash(cipherText: ByteArray, iv: ByteArray, onPasswordDecrypted: (password: ByteArray) -> Unit){ val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(rootCipherDir) + .setTitle(currentVolumeName) .setSubtitle(getString(R.string.decrypt_action_description)) .setDescription(getString(R.string.fingerprint_instruction)) .setNegativeButtonText(getString(R.string.cancel)) @@ -231,12 +238,10 @@ open class VolumeActionActivity : BaseActivity() { .build() this.onPasswordDecrypted = onPasswordDecrypted actionMode = Cipher.DECRYPT_MODE - if (!::cipher.isInitialized){ + if (!isCipherReady){ prepareCipher() } - val encodedElements = cipherText.split(":") - dataToProcess = Base64.decode(encodedElements[1], 0) - val iv = Base64.decode(encodedElements[0], 0) + dataToProcess = cipherText val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv) try { cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec) @@ -248,15 +253,40 @@ open class VolumeActionActivity : BaseActivity() { private fun resetHashStorage() { keyStore.deleteEntry(KEY_ALIAS) - val savedVolumePaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set - val sharedPrefsEditor = sharedPrefs.edit() - for (path in savedVolumePaths){ - val savedHash = sharedPrefs.getString(path, null) - if (savedHash != null){ - sharedPrefsEditor.remove(path) - } + volumeDatabase.getVolumes().forEach { volume -> + volumeDatabase.removeHash(volume) } - sharedPrefsEditor.apply() + isCipherReady = false Toast.makeText(this, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show() } + + protected fun loadVolumePath(callback: () -> Unit){ + currentVolumeName = if (switch_hidden_volume.isChecked){ + edit_volume_name.text.toString() + } else { + edit_volume_path.text.toString() + } + if (currentVolumeName.isEmpty()) { + Toast.makeText(this, if (switch_hidden_volume.isChecked) {R.string.enter_volume_name} else {R.string.enter_volume_path}, Toast.LENGTH_SHORT).show() + } else if (switch_hidden_volume.isChecked && currentVolumeName.contains("/")){ + Toast.makeText(this, R.string.error_slash_in_name, Toast.LENGTH_SHORT).show() + } else { + currentVolumePath = if (switch_hidden_volume.isChecked) { + PathUtils.pathJoin(filesDir.path, currentVolumeName) + } else { + currentVolumeName + } + callback() + } + } + + fun onClickSwitchHiddenVolume(view: View){ + if (switch_hidden_volume.isChecked){ + WidgetUtil.show(hidden_volume_section, originalHiddenVolumeSectionLayoutParams) + WidgetUtil.hide(normal_volume_section) + } else { + WidgetUtil.show(normal_volume_section, originalNormalVolumeSectionLayoutParams) + WidgetUtil.hide(hidden_volume_section) + } + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt new file mode 100644 index 0000000..7976ec5 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt @@ -0,0 +1,96 @@ +package sushi.hardcore.droidfs + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class VolumeDatabase(context: Context): SQLiteOpenHelper(context, + ConstValues.volumeDatabaseName, null, 3) { + companion object { + const val TABLE_NAME = "Volumes" + const val COLUMN_NAME = "name" + const val COLUMN_HIDDEN = "hidden" + const val COLUMN_HASH = "hash" + const val COLUMN_IV = "iv" + + private fun contentValuesFromVolume(volume: Volume): ContentValues { + val contentValues = ContentValues() + contentValues.put(COLUMN_NAME, volume.name) + contentValues.put(COLUMN_HIDDEN, volume.isHidden) + contentValues.put(COLUMN_HASH, volume.hash) + contentValues.put(COLUMN_IV, volume.iv) + return contentValues + } + } + override fun onCreate(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS $TABLE_NAME ($COLUMN_NAME TEXT PRIMARY KEY, $COLUMN_HIDDEN SHORT, $COLUMN_HASH BLOB, $COLUMN_IV BLOB);" + ) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {} + + fun isVolumeSaved(volumeName: String): Boolean { + val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null) + val result = cursor.count > 0 + cursor.close() + return result + } + + fun saveVolume(volume: Volume): Boolean { + if (!isVolumeSaved(volume.name)){ + return (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) == 0.toLong()) + } + return false + } + + fun getVolumes(): List { + val list: MutableList = ArrayList() + val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null) + while (cursor.moveToNext()){ + list.add( + Volume( + cursor.getString(cursor.getColumnIndex(COLUMN_NAME)), + cursor.getShort(cursor.getColumnIndex(COLUMN_HIDDEN)) == 1.toShort(), + cursor.getBlob(cursor.getColumnIndex(COLUMN_HASH)), + cursor.getBlob(cursor.getColumnIndex(COLUMN_IV)) + ) + ) + } + cursor.close() + return list + } + + fun isHashSaved(volumeName: String): Boolean { + val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME, COLUMN_HASH), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null) + var isHashSaved = false + if (cursor.moveToNext()){ + if (cursor.getBlob(cursor.getColumnIndex(COLUMN_HASH)) != null){ + isHashSaved = true + } + } + cursor.close() + return isHashSaved + } + + fun addHash(volume: Volume): Boolean { + return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0 + } + + fun removeHash(volume: Volume): Boolean { + return writableDatabase.update( + TABLE_NAME, contentValuesFromVolume( + Volume( + volume.name, + volume.isHidden, + null, + null + ) + ), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0 + } + + fun removeVolume(volume: Volume): Boolean { + return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volume.name)) > 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt index 77a0831..cf65a48 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt @@ -1,86 +1,99 @@ package sushi.hardcore.droidfs.adapters import android.content.Context -import android.content.SharedPreferences -import android.content.SharedPreferences.Editor import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView import android.widget.TextView -import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.R +import sushi.hardcore.droidfs.util.PathUtils +import sushi.hardcore.droidfs.Volume +import sushi.hardcore.droidfs.VolumeDatabase import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import sushi.hardcore.droidfs.widgets.NonScrollableColoredBorderListView -import java.util.* +import java.io.File -class SavedVolumesAdapter(val context: Context, private val sharedPrefs: SharedPreferences) : BaseAdapter() { +class SavedVolumesAdapter(private val context: Context, private val volumeDatabase: VolumeDatabase) : BaseAdapter() { private val inflater: LayoutInflater = LayoutInflater.from(context) private lateinit var nonScrollableColoredBorderListView: NonScrollableColoredBorderListView - private val savedVolumesPaths: MutableList = ArrayList() - private val sharedPrefsEditor: Editor = sharedPrefs.edit() - - init { - val savedVolumesPathsSet = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set - for (volume_path in savedVolumesPathsSet) { - savedVolumesPaths.add(volume_path) - } - } - - private fun updateSharedPrefs() { - val savedVolumesPathsSet = savedVolumesPaths.toSet() - sharedPrefsEditor.remove(ConstValues.saved_volumes_key) - sharedPrefsEditor.putStringSet(ConstValues.saved_volumes_key, savedVolumesPathsSet) - sharedPrefsEditor.apply() - } override fun getCount(): Int { - return savedVolumesPaths.size + return volumeDatabase.getVolumes().size } - override fun getItem(position: Int): String { - return savedVolumesPaths[position] + override fun getItem(position: Int): Volume { + return volumeDatabase.getVolumes()[position] } override fun getItemId(position: Int): Long { return 0 } + private fun deletePasswordHash(volume: Volume){ + volumeDatabase.removeHash(volume) + volume.hash = null + volume.iv = null + } + + private fun deleteVolumeData(volume: Volume, parent: ViewGroup){ + volumeDatabase.removeVolume(volume) + refresh(parent) + } + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { if (!::nonScrollableColoredBorderListView.isInitialized){ nonScrollableColoredBorderListView = parent as NonScrollableColoredBorderListView } val view: View = convertView ?: inflater.inflate(R.layout.adapter_saved_volume, parent, false) - val volumeNameTextview = view.findViewById(R.id.volume_name_textview) + val volumeNameTextView = view.findViewById(R.id.volume_name_textview) val currentVolume = getItem(position) - volumeNameTextview.text = currentVolume - val deleteImageview = view.findViewById(R.id.delete_imageview) - deleteImageview.setOnClickListener { - val volumePath = savedVolumesPaths[position] + volumeNameTextView.text = currentVolume.name + val deleteImageView = view.findViewById(R.id.delete_imageview) + deleteImageView.setOnClickListener { val dialog = ColoredAlertDialogBuilder(context) dialog.setTitle(R.string.warning) - if (sharedPrefs.getString(volumePath, null) != null){ - dialog.setMessage(context.getString(R.string.delete_hash_or_all)) - dialog.setPositiveButton(context.getString(R.string.delete_all)) { _, _ -> - savedVolumesPaths.removeAt(position) - sharedPrefsEditor.remove(volumePath) - updateSharedPrefs() - refresh(parent) - } - dialog.setNegativeButton(context.getString(R.string.delete_hash)) { _, _ -> - sharedPrefsEditor.remove(volumePath) - sharedPrefsEditor.apply() + if (currentVolume.isHidden){ + if (currentVolume.hash != null) { + dialog.setMessage(R.string.hidden_volume_delete_question_hash) + dialog.setPositiveButton(R.string.password_hash){ _, _ -> + deletePasswordHash(currentVolume) + } + dialog.setNegativeButton(R.string.password_hash_and_path){ _, _ -> + deleteVolumeData(currentVolume, parent) + } + dialog.setNeutralButton(R.string.whole_volume){ _, _ -> + PathUtils.recursiveRemoveDirectory(File(PathUtils.pathJoin(context.filesDir.path, currentVolume.name))) + deleteVolumeData(currentVolume, parent) + } + } else { + dialog.setMessage(R.string.hidden_volume_delete_question) + dialog.setPositiveButton(R.string.path_only){ _, _ -> + deleteVolumeData(currentVolume, parent) + } + dialog.setNegativeButton(R.string.whole_volume){ _, _ -> + PathUtils.recursiveRemoveDirectory(File(PathUtils.pathJoin(context.filesDir.path, currentVolume.name))) + deleteVolumeData(currentVolume, parent) + } } } else { - dialog.setMessage(context.getString(R.string.ask_delete_volume_path)) - dialog.setPositiveButton(R.string.ok) {_, _ -> - savedVolumesPaths.removeAt(position) - updateSharedPrefs() - refresh(parent) + if (currentVolume.hash != null) { + dialog.setMessage(R.string.delete_hash_or_all) + dialog.setNegativeButton(R.string.password_hash_and_path) { _, _ -> + deleteVolumeData(currentVolume, parent) + } + dialog.setPositiveButton(R.string.password_hash) { _, _ -> + deletePasswordHash(currentVolume) + } + } else { + dialog.setMessage(R.string.ask_delete_volume_path) + dialog.setPositiveButton(R.string.ok) {_, _ -> + deleteVolumeData(currentVolume, parent) + } + dialog.setNegativeButton(R.string.cancel, null) } - dialog.setNegativeButton(R.string.cancel, null) } dialog.show() } @@ -90,20 +103,9 @@ class SavedVolumesAdapter(val context: Context, private val sharedPrefs: SharedP private fun refresh(parent: ViewGroup) { notifyDataSetChanged() if (count == 0){ - WidgetUtil.hide(parent) + WidgetUtil.hideWithPadding(parent) } else { nonScrollableColoredBorderListView.layoutParams.height = nonScrollableColoredBorderListView.computeHeight() } } - - fun isPathSaved(volume_path: String): Boolean { - return savedVolumesPaths.contains(volume_path) - } - - fun addVolumePath(volume_path: String) { - if (!isPathSaved(volume_path)) { - savedVolumesPaths.add(volume_path) - updateSharedPrefs() - } - } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt index d2a6384..949c979 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt @@ -160,4 +160,19 @@ object PathUtils { val split: Array = docId.split(":").toTypedArray() return if (split.size >= 2 && split[1] != null) split[1] else File.separator } + + fun recursiveRemoveDirectory(rootDirectory: File): Boolean { + rootDirectory.listFiles()?.forEach { item -> + if (item.isDirectory) { + if (!recursiveRemoveDirectory(item)){ + return false + } + } else { + if (!item.delete()) { + return false + } + } + } + return rootDirectory.delete() + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt b/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt index 29105b4..fff01ff 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt @@ -1,13 +1,20 @@ package sushi.hardcore.droidfs.util import android.view.View -import android.view.ViewGroup import android.widget.LinearLayout object WidgetUtil { - fun hide(view: View){ + fun hideWithPadding(view: View){ view.visibility = View.INVISIBLE view.setPadding(0, 0, 0, 0) view.layoutParams = LinearLayout.LayoutParams(0, 0) } + fun hide(view: View){ + view.visibility = View.INVISIBLE + view.layoutParams = LinearLayout.LayoutParams(0, 0) + } + fun show(view: View, layoutParams: LinearLayout.LayoutParams){ + view.visibility = View.VISIBLE + view.layoutParams = layoutParams + } } \ No newline at end of file diff --git a/app/src/main/res/layout/volume_path_section.xml b/app/src/main/res/layout/volume_path_section.xml index eaee4bb..a5de4fd 100644 --- a/app/src/main/res/layout/volume_path_section.xml +++ b/app/src/main/res/layout/volume_path_section.xml @@ -1,29 +1,69 @@ - + - - - + android:text="@string/hidden_volume" + app:switchPadding="10dp" + android:layout_gravity="center_horizontal" + android:onClick="onClickSwitchHiddenVolume"/> - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d82890c..6737fa5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Password: Password (confirmation): Volume Path: + Volume Name: Import/Encrypt files Create folder Directory Empty @@ -42,6 +43,7 @@ Failed to retrieve file size. Parent Folder Please enter the volume path + Please enter the volume name Open with external app Are you sure you want to delete %s ? Are you sure you want to delete these %s items ? @@ -76,10 +78,14 @@ Please touch the fingerprint sensor Failed to open the volume. The password may have changed. Authentication failed - Delete only password hash or all saved volume data ? (This won\'t delete the volume itself) - Delete all - Delete password hash + Delete only the password hash or the password hash and the saved path ? (This won\'t delete the volume itself) + Password hash and path + Password hash Are you sure you want to forget this volume path ? (This won\'t delete the volume itself) + Delete only the password hash, the password hash and the saved path, or the WHOLE volume (including its content) ? + Whole volume + Delete only the path or the whole volume ITSELF (including its content) ? + Path only IllegalBlockSizeException This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem. Reset hash storage @@ -167,7 +173,6 @@ DroidFS can\'t write on removable SD cards. Opening volume with read-only access. DroidFS doesn\'t have write access to this path. Opening volume with read-only access. DroidFS doesn\'t have write access to this path. Please try another volume. - DroidFS doesn\'t have read access to this path. You can try to move the volume to a readable location. DroidFS doesn\'t have write access to this path. You can try to move the volume to a writable location. DroidFS can\'t write on removable SD cards, please move the volume to internal storage. Slideshow stopped @@ -185,4 +190,6 @@ You should read it carefully before enabling any of these options. Unsafe features documentation Unable to retrieve file name for URI: %s + Hidden Volume + Volume name cannot contain slashes diff --git a/app/src/main/res/xml/unsafe_features_preferences.xml b/app/src/main/res/xml/unsafe_features_preferences.xml index 13ee3f3..ab4bc36 100644 --- a/app/src/main/res/xml/unsafe_features_preferences.xml +++ b/app/src/main/res/xml/unsafe_features_preferences.xml @@ -1,63 +1,61 @@ - + + android:title="@string/usf_screenshot" /> - + - + android:title="@string/usf_decrypt" /> + + android:title="@string/usf_share" /> + android:title="@string/usf_keep_open" /> - + + android:title="@string/usf_fingerprint" /> - + - + android:title="@string/usf_doc"> +