SavedVolume database & Hidden volume feature

This commit is contained in:
Matéo Duparc 2020-11-04 18:42:04 +01:00
parent 0b509c2f98
commit a4df9d3ffa
14 changed files with 439 additions and 231 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Volume> {
val list: MutableList<Volume> = 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
}
}

View File

@ -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<String> = ArrayList()
private val sharedPrefsEditor: Editor = sharedPrefs.edit()
init {
val savedVolumesPathsSet = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set<String>
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<TextView>(R.id.volume_name_textview)
val volumeNameTextView = view.findViewById<TextView>(R.id.volume_name_textview)
val currentVolume = getItem(position)
volumeNameTextview.text = currentVolume
val deleteImageview = view.findViewById<ImageView>(R.id.delete_imageview)
deleteImageview.setOnClickListener {
val volumePath = savedVolumesPaths[position]
volumeNameTextView.text = currentVolume.name
val deleteImageView = view.findViewById<ImageView>(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()
}
}
}

View File

@ -160,4 +160,19 @@ object PathUtils {
val split: Array<String?> = 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()
}
}

View File

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

View File

@ -1,29 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
android:layout_width="@dimen/create_activity_label_width"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/volume_path"
android:textSize="@dimen/edit_text_label_text_size" />
<sushi.hardcore.droidfs.widgets.ColoredEditText
android:id="@+id/edit_volume_path"
android:layout_width="0dp"
android:layout_weight="0.5"
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_hidden_volume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"/>
android:text="@string/hidden_volume"
app:switchPadding="10dp"
android:layout_gravity="center_horizontal"
android:onClick="onClickSwitchHiddenVolume"/>
<sushi.hardcore.droidfs.widgets.ColoredImageButton
android:layout_width="@dimen/image_button_size"
android:layout_height="@dimen/image_button_size"
android:onClick="pickDirectory"
android:scaleType="fitCenter"
android:background="#00000000"
android:src="@drawable/icon_folder" />
<LinearLayout
android:id="@+id/normal_volume_section"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="@dimen/create_activity_label_width"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/volume_path"
android:textSize="@dimen/edit_text_label_text_size" />
<sushi.hardcore.droidfs.widgets.ColoredEditText
android:id="@+id/edit_volume_path"
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"/>
<sushi.hardcore.droidfs.widgets.ColoredImageButton
android:layout_width="@dimen/image_button_size"
android:layout_height="@dimen/image_button_size"
android:onClick="pickDirectory"
android:scaleType="fitCenter"
android:background="#00000000"
android:src="@drawable/icon_folder" />
</LinearLayout>
<LinearLayout
android:id="@+id/hidden_volume_section"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="@dimen/create_activity_label_width"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/volume_name"
android:textSize="@dimen/edit_text_label_text_size" />
<sushi.hardcore.droidfs.widgets.ColoredEditText
android:id="@+id/edit_volume_name"
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout>

View File

@ -9,6 +9,7 @@
<string name="password">Password:</string>
<string name="password_confirm">Password (confirmation):</string>
<string name="volume_path">Volume Path:</string>
<string name="volume_name">Volume Name:</string>
<string name="import_files">Import/Encrypt files</string>
<string name="mkdir">Create folder</string>
<string name="dir_empty">Directory Empty</string>
@ -42,6 +43,7 @@
<string name="get_size_failed">Failed to retrieve file size.</string>
<string name="parent_folder">Parent Folder</string>
<string name="enter_volume_path">Please enter the volume path</string>
<string name="enter_volume_name">Please enter the volume name</string>
<string name="external_open">Open with external app</string>
<string name="single_delete_confirm">Are you sure you want to delete %s ?</string>
<string name="multiple_delete_confirm">Are you sure you want to delete these %s items ?</string>
@ -76,10 +78,14 @@
<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="authentication_failed">Authentication failed</string>
<string name="delete_hash_or_all">Delete only password hash or all saved volume data ? (This won\'t delete the volume itself)</string>
<string name="delete_all">Delete all</string>
<string name="delete_hash">Delete password hash</string>
<string name="delete_hash_or_all">Delete only the password hash or the password hash and the saved path ? (This won\'t delete the volume itself)</string>
<string name="password_hash_and_path">Password hash and path</string>
<string name="password_hash">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="hidden_volume_delete_question_hash">Delete only the password hash, the password hash and the saved path, or the WHOLE volume (including its content) ?</string>
<string name="whole_volume">Whole volume</string>
<string name="hidden_volume_delete_question">Delete only the path or the whole volume ITSELF (including its content) ?</string>
<string name="path_only">Path only</string>
<string name="illegal_block_size_exception">IllegalBlockSizeException</string>
<string name="illegal_block_size_exception_msg">This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem.</string>
<string name="reset_hash_storage">Reset hash storage</string>
@ -167,7 +173,6 @@
<string name="open_on_sdcard_warning">DroidFS can\'t write on removable SD cards. Opening volume with read-only access.</string>
<string name="open_cant_write_warning">DroidFS doesn\'t have write access to this path. Opening volume with read-only access.</string>
<string name="open_cant_write_error_msg">DroidFS doesn\'t have write access to this path. Please try another volume.</string>
<string name="open_cant_read_error">DroidFS doesn\'t have read access to this path. You can try to move the volume to a readable location.</string>
<string name="change_pwd_cant_write_error_msg">DroidFS doesn\'t have write access to this path. You can try to move the volume to a writable location.</string>
<string name="change_pwd_on_sdcard_error_msg">DroidFS can\'t write on removable SD cards, please move the volume to internal storage.</string>
<string name="slideshow_stopped">Slideshow stopped</string>
@ -185,4 +190,6 @@
<string name="usf_read_doc">You should read it carefully before enabling any of these options.</string>
<string name="usf_doc">Unsafe features documentation</string>
<string name="error_retrieving_filename">Unable to retrieve file name for URI: %s</string>
<string name="hidden_volume">Hidden Volume</string>
<string name="error_slash_in_name">Volume name cannot contain slashes</string>
</resources>

View File

@ -1,63 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/ui">
<PreferenceCategory android:title="@string/ui">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_screenshot"
android:key="usf_screenshot"
android:title="@string/usf_screenshot"
android:defaultValue="false"/>
android:title="@string/usf_screenshot" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/explorer">
<PreferenceCategory android:title="@string/explorer">
<SwitchPreference
android:icon="@drawable/icon_open_in_new"
android:key="usf_open"
android:title="@string/usf_open"
android:defaultValue="false"/>
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_decrypt"
android:key="usf_decrypt"
android:title="@string/usf_decrypt"
android:defaultValue="false"/>
android:title="@string/usf_decrypt" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_open_in_new"
android:key="usf_open"
android:title="@string/usf_open" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_share"
android:key="usf_share"
android:title="@string/usf_share"
android:defaultValue="false"/>
android:title="@string/usf_share" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_decrypt"
android:key="usf_keep_open"
android:title="@string/usf_keep_open"
android:defaultValue="false"/>
android:title="@string/usf_keep_open" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/usf_volume_management">
<PreferenceCategory android:title="@string/usf_volume_management">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_fingerprint"
android:key="usf_fingerprint"
android:title="@string/usf_fingerprint"
android:defaultValue="false"/>
android:title="@string/usf_fingerprint" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/about">
<PreferenceCategory android:title="@string/about">
<Preference
android:title="@string/usf_doc"
android:icon="@drawable/icon_notes"
android:summary="@string/usf_read_doc"
android:icon="@drawable/icon_notes">
<intent android:action="android.intent.action.VIEW" android:data="https://github.com/hardcore-sushi/DroidFS#unsafe-features"/>
android:title="@string/usf_doc">
<intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/hardcore-sushi/DroidFS#unsafe-features" />
</Preference>
</PreferenceCategory>