libcryfs genesis

This commit is contained in:
Matéo Duparc 2022-06-18 21:13:16 +02:00
parent 83dd759f36
commit dbd04848bd
38 changed files with 973 additions and 499 deletions

View File

@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 3.10)
add_subdirectory(${PROJECT_SOURCE_DIR}/libcryfs/)
add_library(
gocryptfs
SHARED
@ -23,6 +25,10 @@ target_link_libraries(
gocryptfs
)
add_library(cryfs_jni SHARED src/main/native/libcryfs.c)
#file(GLOB CRYFS_STATIC_LIBRARIES ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/lib*.a)
target_link_libraries(cryfs_jni fspp-fuse)#${CRYFS_STATIC_LIBRARIES})
add_library(
avformat
SHARED

View File

@ -4,7 +4,7 @@ apply plugin: 'kotlin-android'
android {
compileSdkVersion 31
buildToolsVersion "31"
ndkVersion "23.1.7779620"
ndkVersion "24.0.8215888"
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8

View File

@ -30,6 +30,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.video_recording.SeekableWriter
import sushi.hardcore.droidfs.video_recording.VideoCapture
@ -64,7 +65,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
private lateinit var sensorOrientationListener: SensorOrientationListener
private var previousOrientation: Float = 0f
private lateinit var orientedIcons: List<ImageView>
private lateinit var gocryptfsVolume: GocryptfsVolume
private lateinit var encryptedVolume: EncryptedVolume
private lateinit var outputDirectory: String
private var isFinishingIntentionally = false
private var isAskingPermissions = false
@ -93,7 +94,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1))
encryptedVolume = intent.getParcelableExtra("volume")!!
outputDirectory = intent.getStringExtra("path")!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -381,7 +382,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
var fileName: String
do {
fileName = baseName+(random.nextInt(fileNameRandomMax-fileNameRandomMin)+fileNameRandomMin)+'.'+ if (isVideo) {"mp4"} else {"jpg"}
} while (gocryptfsVolume.pathExists(fileName))
} while (encryptedVolume.pathExists(fileName))
return PathUtils.pathJoin(outputDirectory, fileName)
}
@ -415,7 +416,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
imageCapture.takePicture(outputOptions, executor, object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
binding.takePhotoButton.onPhotoTaken()
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), outputPath)) {
if (encryptedVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), outputPath)) {
Toast.makeText(applicationContext, getString(R.string.picture_save_success, outputPath), Toast.LENGTH_SHORT).show()
} else {
CustomAlertDialogBuilder(this@CameraActivity, themeValue)
@ -446,18 +447,18 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
isRecording = false
} else if (!isWaitingForTimer) {
val path = getOutputPath(true)
startTimerThen {
val handleId = gocryptfsVolume.openWriteMode(path)
/*startTimerThen {
val handleId = encryptedVolume.openWriteMode(path)
videoCapture?.startRecording(VideoCapture.OutputFileOptions(object : SeekableWriter {
var offset = 0L
override fun write(byteArray: ByteArray) {
offset += gocryptfsVolume.writeFile(handleId, offset, byteArray, byteArray.size)
offset += encryptedVolume.writeFile(handleId, offset, byteArray, byteArray.size)
}
override fun seek(offset: Long) {
this.offset = offset
}
override fun close() {
gocryptfsVolume.closeFile(handleId)
encryptedVolume.closeFile(handleId)
}
}), executor, object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved() {
@ -472,14 +473,14 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
})
binding.recordVideoButton.setImageResource(R.drawable.stop_recording_video_button)
isRecording = true
}
}*/
}
}
override fun onDestroy() {
super.onDestroy()
if (!isFinishingIntentionally) {
gocryptfsVolume.close()
encryptedVolume.close()
RestrictedFileProvider.wipeAll(this)
}
}

View File

@ -15,7 +15,7 @@ import java.util.*
class ChangePasswordActivity: BaseActivity() {
private lateinit var binding: ActivityChangePasswordBinding
private lateinit var volume: Volume
private lateinit var volume: SavedVolume
private lateinit var volumeDatabase: VolumeDatabase
private var fingerprintProtector: FingerprintProtector? = null
private var usfFingerprint: Boolean = false

View File

@ -5,8 +5,6 @@ import java.io.File
object ConstValues {
const val CREATOR = "DroidFS"
const val FILE_MODE = 384 //0600
const val DIRECTORY_MODE = 448 //0700
const val VOLUME_DATABASE_NAME = "SavedVolumes"
const val SORT_ORDER_KEY = "sort_order"
val FAKE_URI: Uri = Uri.parse("fakeuri://droidfs")

View File

@ -133,7 +133,7 @@ class FingerprintProtector private constructor(
private lateinit var cipher: Cipher
private var isCipherReady = false
private var cipherActionMode: Int? = null
private lateinit var volume: Volume
private lateinit var volume: SavedVolume
private lateinit var dataToProcess: ByteArray
private fun resetHashStorage() {
@ -207,7 +207,7 @@ class FingerprintProtector private constructor(
.show()
}
fun savePasswordHash(volume: Volume, plainText: ByteArray) {
fun savePasswordHash(volume: SavedVolume, plainText: ByteArray) {
this.volume = volume
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(R.string.encrypt_action_description))

View File

@ -1,15 +1,11 @@
package sushi.hardcore.droidfs
import android.content.Context
import android.net.Uri
import android.os.Parcel
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
private external fun native_close(sessionID: Int)
private external fun native_is_closed(sessionID: Int): Boolean
private external fun native_list_dir(sessionID: Int, dir_path: String): MutableList<ExplorerElement>
@ -17,13 +13,12 @@ class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
private external fun native_open_write_mode(sessionID: Int, file_path: String, mode: Int): Int
private external fun native_read_file(sessionID: Int, handleID: Int, offset: Long, buff: ByteArray): Int
private external fun native_write_file(sessionID: Int, handleID: Int, offset: Long, buff: ByteArray, buff_size: Int): Int
private external fun native_truncate(sessionID: Int, handleID: Int, offset: Long): Boolean
private external fun native_path_exists(sessionID: Int, file_path: String): Boolean
private external fun native_get_size(sessionID: Int, file_path: String): Long
private external fun native_truncate(sessionID: Int, path: String, offset: Long): Boolean
private external fun native_close_file(sessionID: Int, handleID: Int)
private external fun native_remove_file(sessionID: Int, file_path: String): Boolean
private external fun native_mkdir(sessionID: Int, dir_path: String, mode: Int): Boolean
private external fun native_rmdir(sessionID: Int, dir_path: String): Boolean
private external fun native_get_attr(sessionID: Int, file_path: String): Stat?
private external fun native_rename(sessionID: Int, old_path: String, new_path: String): Boolean
companion object {
@ -32,14 +27,16 @@ class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
const val DefaultBS = 4096
const val CONFIG_FILE_NAME = "gocryptfs.conf"
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String, returnedHash: ByteArray?): Boolean
external fun init(root_cipher_dir: String, password: CharArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
external fun nativeInit(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
external fun changePassword(root_cipher_dir: String, old_password: CharArray?, givenHash: ByteArray?, new_password: CharArray, returnedHash: ByteArray?): Boolean
fun isGocryptfsVolume(path: File): Boolean {
if (path.isDirectory){
return File(path, CONFIG_FILE_NAME).isFile
fun init(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): GocryptfsVolume? {
val sessionId = nativeInit(root_cipher_dir, password, givenHash, returnedHash)
return if (sessionId == -1) {
null
} else {
GocryptfsVolume(sessionId)
}
return false
}
init {
@ -47,197 +44,63 @@ class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
}
}
fun close() {
constructor(parcel: Parcel) : this(parcel.readInt())
override fun openFile(path: String): Long {
return native_open_write_mode(sessionID, path, 0).toLong()
}
override fun read(fileHandle: Long, buffer: ByteArray, offset: Long): Int {
return native_read_file(sessionID, fileHandle.toInt(), offset, buffer)
}
override fun readDir(path: String): MutableList<ExplorerElement>? {
return native_list_dir(sessionID, path)
}
override fun getAttr(path: String): Stat? {
return native_get_attr(sessionID, path)
}
override fun writeToParcel(parcel: Parcel, flags: Int) = with(parcel) {
writeByte(GOCRYPTFS_VOLUME_TYPE)
writeInt(sessionID)
}
override fun close() {
native_close(sessionID)
}
fun isClosed(): Boolean {
override fun isClosed(): Boolean {
return native_is_closed(sessionID)
}
fun listDir(dir_path: String): MutableList<ExplorerElement> {
return native_list_dir(sessionID, dir_path)
override fun mkdir(path: String): Boolean {
return native_mkdir(sessionID, path, 0)
}
fun mkdir(dir_path: String): Boolean {
return native_mkdir(sessionID, dir_path, ConstValues.DIRECTORY_MODE)
override fun rmdir(path: String): Boolean {
return native_rmdir(sessionID, path)
}
fun rmdir(dir_path: String): Boolean {
return native_rmdir(sessionID, dir_path)
}
fun removeFile(file_path: String): Boolean {
return native_remove_file(sessionID, file_path)
}
fun pathExists(file_path: String): Boolean {
return native_path_exists(sessionID, file_path)
}
fun getSize(file_path: String): Long {
return native_get_size(sessionID, file_path)
}
fun closeFile(handleID: Int) {
native_close_file(sessionID, handleID)
}
fun openReadMode(file_path: String): Int {
return native_open_read_mode(sessionID, file_path)
}
fun openWriteMode(file_path: String): Int {
return native_open_write_mode(sessionID, file_path, ConstValues.FILE_MODE)
}
fun readFile(handleID: Int, offset: Long, buff: ByteArray): Int {
return native_read_file(sessionID, handleID, offset, buff)
}
fun writeFile(handleID: Int, offset: Long, buff: ByteArray, buff_size: Int): Int {
return native_write_file(sessionID, handleID, offset, buff, buff_size)
}
fun truncate(handleID: Int, offset: Long): Boolean {
return native_truncate(sessionID, handleID, offset)
}
fun rename(old_path: String, new_path: String): Boolean {
return native_rename(sessionID, old_path, new_path)
}
fun exportFile(handleID: Int, os: OutputStream): Boolean {
var offset: Long = 0
val ioBuffer = ByteArray(DefaultBS)
var length: Int
while (readFile(handleID, offset, ioBuffer).also { length = it } > 0){
os.write(ioBuffer, 0, length)
offset += length.toLong()
}
os.close()
override fun closeFile(fileHandle: Long): Boolean {
native_close_file(sessionID, fileHandle.toInt())
return true
}
fun exportFile(src_path: String, os: OutputStream): Boolean {
var success = false
val srcHandleId = openReadMode(src_path)
if (srcHandleId != -1) {
success = exportFile(srcHandleId, os)
closeFile(srcHandleId)
}
return success
override fun write(fileHandle: Long, offset: Long, buffer: ByteArray, size: Int): Int {
return native_write_file(sessionID, fileHandle.toInt(), offset, buffer, size)
}
fun exportFile(src_path: String, dst_path: String): Boolean {
return exportFile(src_path, FileOutputStream(dst_path))
override fun truncate(path: String, size: Long): Boolean {
return native_truncate(sessionID, path, size)
}
fun exportFile(context: Context, src_path: String, output_path: Uri): Boolean {
val os = context.contentResolver.openOutputStream(output_path)
if (os != null){
return exportFile(src_path, os)
}
return false
override fun deleteFile(path: String): Boolean {
return native_remove_file(sessionID, path)
}
fun importFile(inputStream: InputStream, dst_path: String): Boolean {
val dstHandleId = openWriteMode(dst_path)
if (dstHandleId != -1) {
var success = true
var offset: Long = 0
val ioBuffer = ByteArray(DefaultBS)
var length: Int
while (inputStream.read(ioBuffer).also { length = it } > 0) {
val written = writeFile(dstHandleId, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += written
} else {
inputStream.close()
success = false
break
}
}
closeFile(dstHandleId)
inputStream.close()
return success
}
return false
}
fun importFile(context: Context, src_uri: Uri, dst_path: String): Boolean {
val inputStream = context.contentResolver.openInputStream(src_uri)
if (inputStream != null){
return importFile(inputStream, dst_path)
}
return false
}
fun recursiveMapFiles(rootPath: String): MutableList<ExplorerElement> {
val result = mutableListOf<ExplorerElement>()
val explorerElements = listDir(rootPath)
result.addAll(explorerElements)
for (e in explorerElements){
if (e.isDirectory){
result.addAll(recursiveMapFiles(e.fullPath))
}
}
return result
}
fun recursiveRemoveDirectory(plain_directory_path: String): String? {
val explorerElements = listDir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
val result = recursiveRemoveDirectory(fullPath)
result?.let { return it }
} else {
if (!removeFile(fullPath)) {
return fullPath
}
}
}
return if (!rmdir(plain_directory_path)) {
plain_directory_path
} else {
null
}
}
fun loadWholeFile(fullPath: String, size: Long? = null, maxSize: Long? = null): Pair<ByteArray?, Int> {
val fileSize = size ?: getSize(fullPath)
return if (fileSize >= 0) {
maxSize?.let {
if (fileSize > it) {
return Pair(null, 0)
}
}
try {
val fileBuff = ByteArray(fileSize.toInt())
val handleID = openReadMode(fullPath)
if (handleID == -1) {
Pair(null, 3)
} else {
var offset: Long = 0
val ioBuffer = ByteArray(DefaultBS)
var length: Int
while (readFile(handleID, offset, ioBuffer).also { length = it } > 0) {
System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length)
offset += length.toLong()
}
closeFile(handleID)
if (offset == fileBuff.size.toLong()) {
Pair(fileBuff, 0)
} else {
Pair(null, 4)
}
}
} catch (e: OutOfMemoryError) {
Pair(null, 2)
}
} else {
Pair(null, 1)
}
override fun rename(srcPath: String, dstPath: String): Boolean {
return native_rename(sessionID, srcPath, dstPath)
}
}

View File

@ -30,10 +30,13 @@ import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.File
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.util.*
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
@ -155,7 +158,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
}
override fun onVolumeItemClick(volume: Volume, position: Int) {
override fun onVolumeItemClick(volume: SavedVolume, position: Int) {
if (volumeAdapter.selectedItems.isEmpty())
openVolume(volume, position)
else
@ -186,7 +189,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
invalidateOptionsMenu()
}
private fun removeVolumes(volumes: List<Volume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
private fun removeVolumes(volumes: List<SavedVolume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
if (i < volumes.size) {
if (volumes[i].isHidden) {
if (doDeleteVolumeContent == null) {
@ -306,7 +309,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(File(volume.name)),
DocumentFile.fromFile(filesDir),
) {
Volume(volume.shortName, true, volume.encryptedHash, volume.iv)
SavedVolume(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
}
}
}
@ -377,9 +380,10 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
dstRootDirectory.name?.let { name ->
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
if (path == null) null
else Volume(
else SavedVolume(
PathUtils.pathJoin(path, name),
false,
volume.type,
volume.encryptedHash,
volume.iv
)
@ -388,7 +392,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
}
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> Volume?) {
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> SavedVolume?) {
lifecycleScope.launch {
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
when {
@ -415,7 +419,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
}
private fun renameVolume(volume: Volume, position: Int) {
private fun renameVolume(volume: SavedVolume, position: Int) {
with (EditTextDialog(this, R.string.new_volume_name) { newName ->
val srcPath = File(volume.getFullPath(filesDir.path))
val dstPath = File(srcPath.parent, newName).canonicalFile
@ -452,7 +456,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
private fun openVolume(volume: Volume, position: Int) {
private fun openVolume(volume: SavedVolume, position: Int) {
var askForPassword = true
fingerprintProtector?.let { fingerprintProtector ->
volume.encryptedHash?.let { encryptedHash ->
@ -463,21 +467,21 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
volumeAdapter.refresh()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask<Int>(this@MainActivity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): Int {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
object : LoadingTask<EncryptedVolume?>(this@MainActivity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, filesDir.path, null, hash, null)
Arrays.fill(hash, 0)
return sessionId
return encryptedVolume
}
}.startTask(lifecycleScope) { sessionId ->
if (sessionId != -1) {
startExplorer(sessionId, volume.shortName)
} else {
}.startTask(lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(this@MainActivity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
startExplorer(encryptedVolume, volume.shortName)
}
}
}
@ -496,7 +500,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
askForPassword(volume, position)
}
private fun onPasswordSubmitted(volume: Volume, position: Int, dialogBinding: DialogOpenVolumeBinding) {
private fun onPasswordSubmitted(volume: SavedVolume, position: Int, dialogBinding: DialogOpenVolumeBinding) {
if (dialogBinding.checkboxDefaultOpen.isChecked xor (defaultVolumeName == volume.name)) {
with (sharedPrefs.edit()) {
defaultVolumeName = if (dialogBinding.checkboxDefaultOpen.isChecked) {
@ -515,12 +519,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
openVolumeWithPassword(
volume,
position,
password,
StandardCharsets.UTF_8.encode(CharBuffer.wrap(password)).array(),
dialogBinding.checkboxSavePassword.isChecked,
)
}
private fun askForPassword(volume: Volume, position: Int, savePasswordHash: Boolean = false) {
private fun askForPassword(volume: SavedVolume, position: Int, savePasswordHash: Boolean = false) {
val dialogBinding = DialogOpenVolumeBinding.inflate(layoutInflater)
if (!usfFingerprint || fingerprintProtector == null || volume.encryptedHash != null) {
dialogBinding.checkboxSavePassword.visibility = View.GONE
@ -550,20 +554,28 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
dialog.show()
}
private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) {
private fun openVolumeWithPassword(volume: SavedVolume, position: Int, password: ByteArray, savePasswordHash: Boolean) {
val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
var returnedHash: ByteArray? = null
if (savePasswordHash && usfFingerprint) {
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
object : LoadingTask<Int>(this, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): Int {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
Arrays.fill(password, 0.toChar())
return sessionId
object : LoadingTask<EncryptedVolume?>(this, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, filesDir.path, password, null, returnedHash)
Arrays.fill(password, 0)
return encryptedVolume
}
}.startTask(lifecycleScope) { sessionId ->
if (sessionId != -1) {
}.startTask(lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok) { _, _ ->
askForPassword(volume, position, savePasswordHash)
}
.show()
} else {
val fingerprintProtector = fingerprintProtector
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
@ -575,12 +587,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
volumeAdapter.onVolumeChanged(position)
startExplorer(sessionId, volume.shortName)
startExplorer(encryptedVolume, volume.shortName)
}
private var isClosed = false
override fun onFailed(pending: Boolean) {
if (!isClosed) {
GocryptfsVolume(this@MainActivity, sessionId).close()
encryptedVolume.close()
isClosed = true
}
Arrays.fill(returnedHash, 0)
@ -588,21 +600,13 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
fingerprintProtector.savePasswordHash(volume, returnedHash)
} else {
startExplorer(sessionId, volume.shortName)
startExplorer(encryptedVolume, volume.shortName)
}
} else {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok) { _, _ ->
askForPassword(volume, position, savePasswordHash)
}
.show()
}
}
}
private fun startExplorer(sessionId: Int, volumeShortName: String) {
private fun startExplorer(encryptedVolume: EncryptedVolume, volumeShortName: String) {
var explorerIntent: Intent? = null
if (dropMode) { //import via android share menu
explorerIntent = Intent(this, ExplorerActivityDrop::class.java)
@ -610,13 +614,13 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
explorerIntent.putExtras(intent.extras!!) //forward extras
} else if (pickMode) {
explorerIntent = Intent(this, ExplorerActivityPick::class.java)
explorerIntent.putExtra("originalSessionID", intent.getIntExtra("sessionID", -1))
explorerIntent.putExtra("destinationVolume", intent.getParcelableExtra<EncryptedVolume>("volume")!!)
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
}
if (explorerIntent == null) {
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
}
explorerIntent.putExtra("sessionID", sessionId)
explorerIntent.putExtra("volume", encryptedVolume)
explorerIntent.putExtra("volume_name", volumeShortName)
startActivity(explorerIntent)
if (pickMode)
@ -642,7 +646,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (shouldCloseVolume) {
val sessionID = intent.getIntExtra("sessionID", -1)
if (sessionID != -1) {
GocryptfsVolume(this, sessionID).close()
GocryptfsVolume(sessionID).close()
RestrictedFileProvider.wipeAll(this)
}
}

View File

@ -5,11 +5,12 @@ import android.os.Parcelable
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
class Volume(val name: String, val isHidden: Boolean = false, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable {
class SavedVolume(val name: String, val isHidden: Boolean = false, val type: Byte, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readByte() != 0.toByte(),
parcel.readByte(),
parcel.createByteArray(),
parcel.createByteArray()
)
@ -20,7 +21,7 @@ class Volume(val name: String, val isHidden: Boolean = false, var encryptedHash:
fun getFullPath(filesDir: String): String {
return if (isHidden)
PathUtils.pathJoin(filesDir, name)
getHiddenVolumeFullPath(filesDir, name)
else
name
}
@ -37,18 +38,23 @@ class Volume(val name: String, val isHidden: Boolean = false, var encryptedHash:
with (dest) {
writeString(name)
writeByte(if (isHidden) 1 else 0)
writeByte(type)
writeByteArray(encryptedHash)
writeByteArray(iv)
}
}
companion object CREATOR : Parcelable.Creator<Volume> {
override fun createFromParcel(parcel: Parcel): Volume {
return Volume(parcel)
companion object {
const val VOLUMES_DIRECTORY = "volumes"
@JvmField
val CREATOR = object : Parcelable.Creator<SavedVolume> {
override fun createFromParcel(parcel: Parcel) = SavedVolume(parcel)
override fun newArray(size: Int) = arrayOfNulls<SavedVolume>(size)
}
override fun newArray(size: Int): Array<Volume?> {
return arrayOfNulls(size)
fun getHiddenVolumeFullPath(filesDir: String, name: String): String {
return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name)
}
}
}

View File

@ -4,20 +4,25 @@ import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
ConstValues.VOLUME_DATABASE_NAME, null, 3) {
class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, ConstValues.VOLUME_DATABASE_NAME, null, 4) {
companion object {
const val TABLE_NAME = "Volumes"
const val COLUMN_NAME = "name"
const val COLUMN_HIDDEN = "hidden"
const val COLUMN_TYPE = "type"
const val COLUMN_HASH = "hash"
const val COLUMN_IV = "iv"
private fun contentValuesFromVolume(volume: Volume): ContentValues {
private fun contentValuesFromVolume(volume: SavedVolume): ContentValues {
val contentValues = ContentValues()
contentValues.put(COLUMN_NAME, volume.name)
contentValues.put(COLUMN_HIDDEN, volume.isHidden)
contentValues.put(COLUMN_TYPE, byteArrayOf(volume.type))
contentValues.put(COLUMN_HASH, volume.encryptedHash)
contentValues.put(COLUMN_IV, volume.iv)
return contentValues
@ -25,11 +30,57 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
}
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);"
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"$COLUMN_NAME TEXT PRIMARY KEY," +
"$COLUMN_HIDDEN SHORT," +
"$COLUMN_TYPE BLOB," +
"$COLUMN_HASH BLOB," +
"$COLUMN_IV BLOB" +
");"
)
File(context.filesDir, SavedVolume.VOLUMES_DIRECTORY).mkdir()
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// Adding type column and set it to GOCRYPTFS_VOLUME_TYPE for all existing volumes
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN $COLUMN_TYPE BLOB;")
db.update(TABLE_NAME, ContentValues().apply {
put(COLUMN_TYPE, byteArrayOf(EncryptedVolume.GOCRYPTFS_VOLUME_TYPE))
}, null, null)
// Moving hidden volumes to the "volumes" directory
if (File(context.filesDir, SavedVolume.VOLUMES_DIRECTORY).mkdir()) {
val cursor = db.query(
TABLE_NAME,
arrayOf(COLUMN_NAME),
"$COLUMN_HIDDEN=?",
arrayOf("1"),
null,
null,
null
)
while (cursor.moveToNext()) {
val volumeName = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME))
File(
PathUtils.pathJoin(
context.filesDir.path,
volumeName
)
).renameTo(
File(
SavedVolume(
volumeName,
true,
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
).getFullPath(context.filesDir.path)
).canonicalFile
)
}
cursor.close()
} else {
Log.e("VolumeDatabase", "Volumes directory creation failed while upgrading")
}
}
fun isVolumeSaved(volumeName: String, isHidden: Boolean): Boolean {
val cursor = readableDatabase.query(TABLE_NAME,
@ -42,23 +93,30 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
return result
}
fun saveVolume(volume: Volume): Boolean {
fun saveVolume(volume: SavedVolume): Boolean {
if (!isVolumeSaved(volume.name, volume.isHidden)) {
return (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) == 0.toLong())
}
return false
}
fun getVolumes(): List<Volume> {
val list: MutableList<Volume> = ArrayList()
fun getVolumes(): List<SavedVolume> {
val list: MutableList<SavedVolume> = ArrayList()
val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null)
while (cursor.moveToNext()){
val typeColumnIndex = cursor.getColumnIndex(COLUMN_TYPE)
val volumeType = if (typeColumnIndex == -1) {
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
} else {
cursor.getBlob(typeColumnIndex)[0]
}
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))
SavedVolume(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
volumeType,
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)),
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_IV))
)
)
}
@ -70,7 +128,7 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
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) {
if (cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)) != null) {
isHashSaved = true
}
}
@ -78,16 +136,17 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
return isHashSaved
}
fun addHash(volume: Volume): Boolean {
fun addHash(volume: SavedVolume): Boolean {
return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
}
fun removeHash(volume: Volume): Boolean {
fun removeHash(volume: SavedVolume): Boolean {
return writableDatabase.update(
TABLE_NAME, contentValuesFromVolume(
Volume(
SavedVolume(
volume.name,
volume.isHidden,
volume.type,
null,
null
)

View File

@ -17,16 +17,17 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.*
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import java.text.DateFormat
import java.util.*
class ExplorerElementAdapter(
val activity: AppCompatActivity,
val gocryptfsVolume: GocryptfsVolume?,
val encryptedVolume: EncryptedVolume?,
private val listener: Listener,
val thumbnailMaxSize: Long,
) : SelectableAdapter<ExplorerElement>(listener::onSelectionChanged) {
@ -42,7 +43,7 @@ class ExplorerElementAdapter(
private var thumbnailsCache: LruCache<String, Bitmap>? = null
init {
if (gocryptfsVolume != null) {
if (encryptedVolume != null) {
thumbnailsCache = LruCache((Runtime.getRuntime().maxMemory() / 1024 / 8).toInt())
}
}
@ -105,9 +106,9 @@ class ExplorerElementAdapter(
open class RegularElementViewHolder(itemView: View) : ExplorerElementViewHolder(itemView) {
open fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position)
textElementSize.text = PathUtils.formatSize(explorerElement.size)
textElementSize.text = PathUtils.formatSize(explorerElement.stat.size)
(bindingAdapter as ExplorerElementAdapter?)?.let {
textElementMtime.text = it.dateFormat.format(explorerElement.mTime)
textElementMtime.text = it.dateFormat.format(explorerElement.stat.mTime)
}
}
}
@ -118,7 +119,7 @@ class ExplorerElementAdapter(
private val scope = CoroutineScope(Dispatchers.IO)
private fun loadThumbnail(fullPath: String, adapter: ExplorerElementAdapter) {
adapter.gocryptfsVolume?.let { volume ->
adapter.encryptedVolume?.let { volume ->
job = scope.launch {
volume.loadWholeFile(fullPath, maxSize = adapter.thumbnailMaxSize).first?.let {
if (isActive) {
@ -220,9 +221,9 @@ class ExplorerElementAdapter(
}, parent, false
)
return when (viewType) {
ExplorerElement.REGULAR_FILE_TYPE -> FileViewHolder(view)
ExplorerElement.DIRECTORY_TYPE -> DirectoryViewHolder(view)
ExplorerElement.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view)
Stat.S_IFREG -> FileViewHolder(view)
Stat.S_IFDIR -> DirectoryViewHolder(view)
Stat.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view)
else -> throw IllegalArgumentException()
}
}
@ -237,6 +238,6 @@ class ExplorerElementAdapter(
}
override fun getItemViewType(position: Int): Int {
return explorerElements[position].elementType.toInt()
return explorerElements[position].stat.type
}
}

View File

@ -10,7 +10,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.Volume
import sushi.hardcore.droidfs.SavedVolume
import sushi.hardcore.droidfs.VolumeDatabase
class VolumeAdapter(
@ -19,9 +19,9 @@ class VolumeAdapter(
private val allowSelection: Boolean,
private val showReadOnly: Boolean,
private val listener: Listener,
) : SelectableAdapter<Volume>(listener::onSelectionChanged) {
) : SelectableAdapter<SavedVolume>(listener::onSelectionChanged) {
private val inflater: LayoutInflater = LayoutInflater.from(context)
lateinit var volumes: List<Volume>
lateinit var volumes: List<SavedVolume>
init {
reloadVolumes()
@ -29,11 +29,11 @@ class VolumeAdapter(
interface Listener {
fun onSelectionChanged(size: Int)
fun onVolumeItemClick(volume: Volume, position: Int)
fun onVolumeItemClick(volume: SavedVolume, position: Int)
fun onVolumeItemLongClick()
}
override fun getItems(): List<Volume> {
override fun getItems(): List<SavedVolume> {
return volumes
}

View File

@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
import java.util.*
@ -130,8 +131,8 @@ class CreateVolumeFragment: Fragment() {
var returnedHash: ByteArray? = null
if (binding.checkboxSavePassword.isChecked)
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
object: LoadingTask<Volume?>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
override suspend fun doTask(): Volume? {
object: LoadingTask<SavedVolume?>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
override suspend fun doTask(): SavedVolume? {
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
0 -> 0
1 -> 1
@ -151,7 +152,7 @@ class CreateVolumeFragment: Fragment() {
)
) {
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
val volume = Volume(volumeName, isHiddenVolume)
val volume = SavedVolume(volumeName, isHiddenVolume, EncryptedVolume.GOCRYPTFS_VOLUME_TYPE)
volumeDatabase.apply {
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
removeVolume(volumeName)

View File

@ -11,7 +11,6 @@ import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
@ -19,6 +18,7 @@ import androidx.fragment.app.Fragment
import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.databinding.DialogSdcardErrorBinding
import sushi.hardcore.droidfs.databinding.FragmentSelectPathBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
@ -158,7 +158,7 @@ class SelectPathFragment: Fragment() {
private fun getCurrentVolumePath(): String {
return if (binding.switchHiddenVolume.isChecked)
PathUtils.pathJoin(requireContext().filesDir.path, binding.editVolumeName.text.toString())
SavedVolume.getHiddenVolumeFullPath(requireContext().filesDir.path, binding.editVolumeName.text.toString())
else
binding.editVolumeName.text.toString()
}
@ -222,7 +222,8 @@ class SelectPathFragment: Fragment() {
(activity as AddVolumeActivity).createVolume(volumePath, isHidden)
}
Action.ADD -> {
if (!GocryptfsVolume.isGocryptfsVolume(File(volumePath))) {
val volumeType = EncryptedVolume.getVolumeType(volumePath)
if (volumeType < 0) {
CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.error)
.setMessage(R.string.error_not_a_volume)
@ -232,7 +233,7 @@ class SelectPathFragment: Fragment() {
val dialog = CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.warning)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden) }
.setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType) }
if (PathUtils.isPathOnExternalStorage(volumePath, requireContext()))
dialog.setView(
DialogSdcardErrorBinding.inflate(layoutInflater).apply {
@ -244,7 +245,7 @@ class SelectPathFragment: Fragment() {
dialog.setMessage(R.string.add_cant_write_warning)
dialog.show()
} else {
addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden)
addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType)
}
}
}
@ -268,8 +269,8 @@ class SelectPathFragment: Fragment() {
dialog.show()
}
private fun addVolume(volumeName: String, isHidden: Boolean) {
volumeDatabase.saveVolume(Volume(volumeName, isHidden))
private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) {
volumeDatabase.saveVolume(SavedVolume(volumeName, isHidden, volumeType))
(activity as AddVolumeActivity).onVolumeAdded(false)
}
}

View File

@ -9,9 +9,9 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.LoadingTask
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
@ -33,25 +33,25 @@ object ExternalProvider {
return previous_content_type
}
private fun exportFile(context: Context, gocryptfsVolume: GocryptfsVolume, file_path: String, previous_content_type: String?): Pair<Uri?, String?> {
private fun exportFile(context: Context, encryptedVolume: EncryptedVolume, file_path: String, previous_content_type: String?): Pair<Uri?, String?> {
val fileName = File(file_path).name
val tmpFileUri = RestrictedFileProvider.newFile(fileName)
if (tmpFileUri != null){
storedFiles.add(tmpFileUri)
if (gocryptfsVolume.exportFile(context, file_path, tmpFileUri)) {
if (encryptedVolume.exportFile(context, file_path, tmpFileUri)) {
return Pair(tmpFileUri, getContentType(fileName, previous_content_type))
}
}
return Pair(null, null)
}
fun share(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_paths: List<String>) {
fun share(activity: AppCompatActivity, themeValue: String, encryptedVolume: EncryptedVolume, file_paths: List<String>) {
var contentType: String? = null
val uris = ArrayList<Uri>(file_paths.size)
object : LoadingTask<String?>(activity, themeValue, R.string.loading_msg_export) {
override suspend fun doTask(): String? {
for (path in file_paths) {
val result = exportFile(activity, gocryptfsVolume, path, contentType)
val result = exportFile(activity, encryptedVolume, path, contentType)
contentType = if (result.first != null) {
uris.add(result.first!!)
result.second
@ -83,10 +83,10 @@ object ExternalProvider {
}
}
fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) {
fun open(activity: AppCompatActivity, themeValue: String, encryptedVolume: EncryptedVolume, file_path: String) {
object : LoadingTask<Intent?>(activity, themeValue, R.string.loading_msg_export) {
override suspend fun doTask(): Intent? {
val result = exportFile(activity, gocryptfsVolume, file_path, null)
val result = exportFile(activity, encryptedVolume, file_path, null)
return if (result.first != null) {
Intent(Intent.ACTION_VIEW).apply {
setDataAndType(result.first, result.second)

View File

@ -7,6 +7,7 @@ import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -40,6 +41,8 @@ import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_viewers.*
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -50,7 +53,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private var foldersFirst = true
private var mapFolders = true
private var currentSortOrderIndex = 0
protected lateinit var gocryptfsVolume: GocryptfsVolume
protected lateinit var encryptedVolume: EncryptedVolume
private lateinit var volumeName: String
private lateinit var explorerViewModel: ExplorerViewModel
protected var currentDirectoryPath: String = ""
@ -82,8 +85,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
usf_open = sharedPrefs.getBoolean("usf_open", false)
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
volumeName = intent.getStringExtra("volume_name") ?: ""
val sessionID = intent.getIntExtra("sessionID", -1)
gocryptfsVolume = GocryptfsVolume(applicationContext, sessionID)
encryptedVolume = intent.getParcelableExtra("volume")!!
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@ -107,7 +109,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
explorerAdapter = ExplorerElementAdapter(
this,
if (sharedPrefs.getBoolean("thumbnails", true)) {
gocryptfsVolume
encryptedVolume
} else {
null
},
@ -139,7 +141,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
class ExplorerViewModel: ViewModel() {
var currentDirectoryPath = ""
var currentDirectoryPath = "/"
}
private fun setRecyclerViewLayout() {
@ -166,7 +168,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FileOperationService.LocalBinder
fileOperationService = binder.getService()
binder.setGocryptfsVolume(gocryptfsVolume)
binder.setEncryptedVolume(encryptedVolume)
}
override fun onServiceDisconnected(arg0: ComponentName) {
@ -178,7 +180,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private fun startFileViewer(cls: Class<*>, filePath: String){
val intent = Intent(this, cls).apply {
putExtra("path", filePath)
putExtra("sessionID", gocryptfsVolume.sessionID)
putExtra("volume", encryptedVolume)
putExtra("sortOrder", sortOrderValues[currentSortOrderIndex])
}
isStartingActivity = true
@ -187,7 +189,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private fun openWithExternalApp(fullPath: String){
isStartingActivity = true
ExternalProvider.open(this, themeValue, gocryptfsVolume, fullPath)
ExternalProvider.open(this, themeValue, encryptedVolume, fullPath)
}
private fun showOpenAsDialog(path: String) {
@ -276,11 +278,11 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
private fun recursiveSetSize(directory: ExplorerElement) {
for (child in gocryptfsVolume.listDir(directory.fullPath)) {
for (child in encryptedVolume.readDir(directory.fullPath) ?: return) {
if (child.isDirectory) {
recursiveSetSize(child)
}
directory.size += child.size
directory.stat.size += child.stat.size
}
}
@ -301,11 +303,11 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
protected fun setCurrentPath(path: String, onDisplayed: (() -> Unit)? = null) {
synchronized(this) {
explorerElements = gocryptfsVolume.listDir(path)
if (path.isNotEmpty()) { //not root
explorerElements = encryptedVolume.readDir(path) ?: return
if (path != "/") {
explorerElements.add(
0,
ExplorerElement("..", (-1).toShort(), parentPath = currentDirectoryPath)
ExplorerElement("..", Stat.parentFolderStat(), parentPath = currentDirectoryPath)
)
}
}
@ -323,7 +325,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (element.isDirectory) {
recursiveSetSize(element)
}
totalSize += element.size
totalSize += element.stat.size
}
}
}
@ -331,7 +333,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
onDisplayed?.invoke()
}
} else {
displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.size })
displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.stat.size })
onDisplayed?.invoke()
}
}
@ -362,7 +364,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (folderName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
if (!gocryptfsVolume.mkdir(PathUtils.pathJoin(currentDirectoryPath, folderName))) {
if (!encryptedVolume.mkdir(PathUtils.pathJoin(currentDirectoryPath, folderName))) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.error_mkdir)
@ -382,27 +384,27 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
protected fun checkPathOverwrite(items: ArrayList<OperationFile>, dstDirectoryPath: String, callback: (ArrayList<OperationFile>?) -> Unit) {
val srcDirectoryPath = items[0].explorerElement.parentPath
val srcDirectoryPath = items[0].parentPath
var ready = true
for (i in 0 until items.size) {
val testDstPath: String
if (items[i].dstPath == null){
testDstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.fullPath))
if (gocryptfsVolume.pathExists(testDstPath)){
testDstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].srcPath))
if (encryptedVolume.pathExists(testDstPath)) {
ready = false
} else {
items[i].dstPath = testDstPath
}
} else {
testDstPath = items[i].dstPath!!
if (gocryptfsVolume.pathExists(testDstPath) && !items[i].overwriteConfirmed){
if (encryptedVolume.pathExists(testDstPath) && !items[i].overwriteConfirmed) {
ready = false
}
}
if (!ready){
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setMessage(getString(if (items[i].explorerElement.isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, testDstPath))
.setMessage(getString(if (items[i].isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, testDstPath))
.setPositiveButton(R.string.yes) {_, _ ->
items[i].dstPath = testDstPath
items[i].overwriteConfirmed = true
@ -410,17 +412,17 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
.setNegativeButton(R.string.no) { _, _ ->
with(EditTextDialog(this, R.string.enter_new_name) {
items[i].dstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.parentPath), it)
if (items[i].explorerElement.isDirectory){
items[i].dstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].parentPath), it)
if (items[i].isDirectory) {
for (j in 0 until items.size){
if (PathUtils.isChildOf(items[j].explorerElement.fullPath, items[i].explorerElement.fullPath)){
items[j].dstPath = PathUtils.pathJoin(items[i].dstPath!!, PathUtils.getRelativePath(items[i].explorerElement.fullPath, items[j].explorerElement.fullPath))
if (PathUtils.isChildOf(items[j].srcPath, items[i].srcPath)) {
items[j].dstPath = PathUtils.pathJoin(items[i].dstPath!!, PathUtils.getRelativePath(items[i].srcPath, items[j].srcPath))
}
}
}
checkPathOverwrite(items, dstDirectoryPath, callback)
}) {
setSelectedText(items[i].explorerElement.name)
setSelectedText(items[i].name)
setOnCancelListener{
callback(null)
}
@ -452,7 +454,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
items.clear()
break
} else {
items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, parentPath = currentDirectoryPath)))
items.add(OperationFile(PathUtils.pathJoin(fileName, currentDirectoryPath), Stat.S_IFREG))
}
}
if (items.size > 0) {
@ -475,7 +477,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (new_name.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
if (!gocryptfsVolume.rename(PathUtils.pathJoin(currentDirectoryPath, old_name), PathUtils.pathJoin(currentDirectoryPath, new_name))) {
if (!encryptedVolume.rename(PathUtils.pathJoin(currentDirectoryPath, old_name), PathUtils.pathJoin(currentDirectoryPath, new_name))) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(getString(R.string.rename_failed, old_name))
@ -587,8 +589,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
protected open fun closeVolumeOnDestroy() {
if (!gocryptfsVolume.isClosed()){
gocryptfsVolume.close()
if (!encryptedVolume.isClosed()){
encryptedVolume.close()
}
RestrictedFileProvider.wipeAll(this) //additional security
}
@ -616,7 +618,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (isCreating){
isCreating = false
} else {
if (gocryptfsVolume.isClosed()){
if (encryptedVolume.isClosed()){
finish()
} else {
isStartingActivity = false

View File

@ -3,6 +3,7 @@ package sushi.hardcore.droidfs.explorers
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
@ -18,6 +19,8 @@ import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
import sushi.hardcore.droidfs.content_providers.ExternalProvider
import sushi.hardcore.droidfs.databinding.ActivityExplorerBinding
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -36,8 +39,7 @@ class ExplorerActivity : BaseExplorerActivity() {
private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { resultIntent ->
val remoteSessionID = resultIntent.getIntExtra("sessionID", -1)
val remoteGocryptfsVolume = GocryptfsVolume(applicationContext, remoteSessionID)
val remoteEncryptedVolume = resultIntent.getParcelableExtra<EncryptedVolume>("volume")!!
val path = resultIntent.getStringExtra("path")
val operationFiles = ArrayList<OperationFile>()
if (path == null){ //multiples elements
@ -46,12 +48,10 @@ class ExplorerActivity : BaseExplorerActivity() {
if (types != null && paths != null){
for (i in paths.indices) {
operationFiles.add(
OperationFile.fromExplorerElement(
ExplorerElement(File(paths[i]).name, types[i].toShort(), parentPath = PathUtils.getParentPath(paths[i]))
)
OperationFile(paths[i], types[i])
)
if (types[i] == 0){ //directory
remoteGocryptfsVolume.recursiveMapFiles(paths[i]).forEach {
if (types[i] == Stat.S_IFDIR) {
remoteEncryptedVolume.recursiveMapFiles(paths[i])?.forEach {
operationFiles.add(OperationFile.fromExplorerElement(it))
}
}
@ -59,18 +59,16 @@ class ExplorerActivity : BaseExplorerActivity() {
}
} else {
operationFiles.add(
OperationFile.fromExplorerElement(
ExplorerElement(File(path).name, 1, parentPath = PathUtils.getParentPath(path))
)
OperationFile(path, Stat.S_IFREG)
)
}
if (operationFiles.size > 0){
checkPathOverwrite(operationFiles, currentDirectoryPath) { items ->
if (items == null) {
remoteGocryptfsVolume.close()
remoteEncryptedVolume.close()
} else {
lifecycleScope.launch {
val failedItem = fileOperationService.copyElements(items, remoteGocryptfsVolume)
val failedItem = fileOperationService.copyElements(items, remoteEncryptedVolume)
if (failedItem == null) {
Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show()
} else {
@ -81,12 +79,12 @@ class ExplorerActivity : BaseExplorerActivity() {
.show()
}
setCurrentPath(currentDirectoryPath)
remoteGocryptfsVolume.close()
remoteEncryptedVolume.close()
}
}
}
} else {
remoteGocryptfsVolume.close()
remoteEncryptedVolume.close()
}
}
}
@ -120,7 +118,7 @@ class ExplorerActivity : BaseExplorerActivity() {
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
rootUri?.let {
val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
val operation = OperationFile(PathUtils.pathJoin(tree.name!!, currentDirectoryPath), Stat.S_IFDIR)
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
checkedOperation?.let {
lifecycleScope.launch {
@ -175,7 +173,7 @@ class ExplorerActivity : BaseExplorerActivity() {
setContentView(binding.root)
binding.fab.setOnClickListener {
if (currentItemAction != ItemsActions.NONE){
openDialogCreateFolder()
//openDialogCreateFolder()
} else {
val adapter = IconTextDialogAdapter(this)
adapter.items = listOf(
@ -192,7 +190,7 @@ class ExplorerActivity : BaseExplorerActivity() {
"importFromOtherVolumes" -> {
val intent = Intent(this, MainActivity::class.java)
intent.action = "pick"
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
intent.putExtra("volume", encryptedVolume)
isStartingActivity = true
pickFromOtherVolumes.launch(intent)
}
@ -215,7 +213,7 @@ class ExplorerActivity : BaseExplorerActivity() {
"camera" -> {
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("path", currentDirectoryPath)
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
intent.putExtra("volume", encryptedVolume)
isStartingActivity = true
startActivity(intent)
}
@ -241,15 +239,15 @@ class ExplorerActivity : BaseExplorerActivity() {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
val filePath = PathUtils.pathJoin(currentDirectoryPath, fileName)
val handleID = gocryptfsVolume.openWriteMode(filePath) //don't check overwrite because openWriteMode open in read-write (doesn't erase content)
if (handleID == -1) {
val handleID = encryptedVolume.openFile(filePath)
if (handleID == -1L) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
} else {
gocryptfsVolume.closeFile(handleID)
encryptedVolume.closeFile(handleID)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
@ -312,7 +310,7 @@ class ExplorerActivity : BaseExplorerActivity() {
for (i in explorerAdapter.selectedItems){
itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i]))
if (explorerElements[i].isDirectory){
gocryptfsVolume.recursiveMapFiles(explorerElements[i].fullPath).forEach {
encryptedVolume.recursiveMapFiles(explorerElements[i].fullPath)?.forEach {
itemsToProcess.add(OperationFile.fromExplorerElement(it))
}
}
@ -346,11 +344,11 @@ class ExplorerActivity : BaseExplorerActivity() {
}
} else if (currentItemAction == ItemsActions.MOVE){
itemsToProcess.forEach {
it.dstPath = PathUtils.pathJoin(currentDirectoryPath, it.explorerElement.name)
it.dstPath = PathUtils.pathJoin(currentDirectoryPath, it.name)
it.overwriteConfirmed = false // reset the field in case of a previous cancelled move
}
val toMove = ArrayList<OperationFile>(itemsToProcess.size)
val toClean = ArrayList<ExplorerElement>()
val toClean = ArrayList<String>()
prepareFilesForMove(
itemsToProcess,
toMove,
@ -398,7 +396,7 @@ class ExplorerActivity : BaseExplorerActivity() {
paths.add(explorerElements[i].fullPath)
}
isStartingActivity = true
ExternalProvider.share(this, themeValue, gocryptfsVolume, paths)
ExternalProvider.share(this, themeValue, encryptedVolume, paths)
unselectAll()
true
}
@ -418,12 +416,12 @@ class ExplorerActivity : BaseExplorerActivity() {
*/
private fun checkMoveOverwrite(items: List<OperationFile>, callback: (List<OperationFile>?) -> Unit) {
for (item in items) {
if (gocryptfsVolume.pathExists(item.dstPath!!) && !item.overwriteConfirmed) {
if (encryptedVolume.pathExists(item.dstPath!!) && !item.overwriteConfirmed) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setMessage(
getString(
if (item.explorerElement.isDirectory) {
if (item.isDirectory) {
R.string.dir_overwrite_question
} else {
R.string.file_overwrite_question
@ -440,7 +438,7 @@ class ExplorerActivity : BaseExplorerActivity() {
item.dstPath = PathUtils.pathJoin(PathUtils.getParentPath(item.dstPath!!), it)
checkMoveOverwrite(items, callback)
}) {
setSelectedText(item.explorerElement.name)
setSelectedText(item.name)
show()
}
}
@ -463,24 +461,24 @@ class ExplorerActivity : BaseExplorerActivity() {
private fun prepareFilesForMove(
items: List<OperationFile>,
toMove: ArrayList<OperationFile>,
toClean: ArrayList<ExplorerElement>,
toClean: ArrayList<String>,
onReady: () -> Unit
) {
checkMoveOverwrite(items) { checkedItems ->
checkedItems?.let {
for (item in checkedItems) {
if (!item.overwriteConfirmed || !item.explorerElement.isDirectory) {
if (!item.overwriteConfirmed || !item.isDirectory) {
toMove.add(item)
}
}
val toCheck = mutableListOf<OperationFile>()
for (item in checkedItems) {
if (item.overwriteConfirmed && item.explorerElement.isDirectory) {
val children = gocryptfsVolume.listDir(item.explorerElement.fullPath)
toCheck.addAll(children.map {
OperationFile(it, PathUtils.pathJoin(item.dstPath!!, it.name))
})
toClean.add(item.explorerElement)
if (item.overwriteConfirmed && item.isDirectory) {
val children = encryptedVolume.readDir(item.srcPath)
children?.map {
OperationFile(it.fullPath, it.stat.type, PathUtils.pathJoin(item.dstPath!!, it.name))
}?.let { toCheck.addAll(it) }
toClean.add(item.srcPath)
}
}
if (toCheck.isEmpty()) {
@ -514,10 +512,10 @@ class ExplorerActivity : BaseExplorerActivity() {
val element = explorerAdapter.explorerElements[i]
val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name)
if (element.isDirectory) {
val result = gocryptfsVolume.recursiveRemoveDirectory(fullPath)
val result = encryptedVolume.recursiveRemoveDirectory(fullPath)
result?.let{ failedItem = it }
} else {
if (!gocryptfsVolume.removeFile(fullPath)) {
if (!encryptedVolume.deleteFile(fullPath)) {
failedItem = fullPath
}
}

View File

@ -2,10 +2,12 @@ package sushi.hardcore.droidfs.explorers
import android.app.Activity
import android.content.Intent
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import java.util.*
@ -14,7 +16,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
private var isFinishingIntentionally = false
override fun init() {
super.init()
resultIntent.putExtra("sessionID", gocryptfsVolume.sessionID)
resultIntent.putExtra("volume", encryptedVolume)
}
override fun bindFileOperationService() {
@ -65,7 +67,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
for (i in explorerAdapter.selectedItems) {
val e = explorerElements[i]
paths.add(PathUtils.pathJoin(currentDirectoryPath, e.name))
types.add(e.elementType.toInt())
types.add(e.stat.type)
}
resultIntent.putStringArrayListExtra("paths", paths)
resultIntent.putIntegerArrayListExtra("types", types)
@ -84,10 +86,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun closeVolumeOnDestroy() {
if (!isFinishingIntentionally && !usf_keep_open){
val sessionID = intent.getIntExtra("originalSessionID", -1)
if (sessionID != -1){
GocryptfsVolume(applicationContext, sessionID).close()
}
intent.getParcelableExtra<EncryptedVolume>("destinationVolume")?.let { it.close() }
super.closeVolumeOnDestroy()
}
}

View File

@ -1,33 +1,31 @@
package sushi.hardcore.droidfs.explorers
import sushi.hardcore.droidfs.collation.getCollationKeyForFileName
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import java.text.Collator
import java.util.*
class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) {
val mTime = Date((mTime * 1000).toString().toLong())
class ExplorerElement(val name: String, val stat: Stat, val parentPath: String) {
val fullPath: String = PathUtils.pathJoin(parentPath, name)
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
val isDirectory: Boolean
get() = elementType.toInt() == DIRECTORY_TYPE
val isParentFolder: Boolean
get() = elementType.toInt() == PARENT_FOLDER_TYPE
get() = stat.type == Stat.S_IFDIR
val isRegularFile: Boolean
get() = elementType.toInt() == REGULAR_FILE_TYPE
get() = stat.type == Stat.S_IFREG
val isSymlink: Boolean
get() = stat.type == Stat.S_IFLNK
val isParentFolder: Boolean
get() = stat.type == Stat.PARENT_FOLDER_TYPE
companion object {
const val DIRECTORY_TYPE = 0
const val PARENT_FOLDER_TYPE = -1
const val REGULAR_FILE_TYPE = 1
@JvmStatic
//this function is needed because I had some problems calling the constructor from JNI, probably due to arguments with default values
fun new(name: String, elementType: Short, size: Long, mTime: Long, parentPath: String): ExplorerElement {
return ExplorerElement(name, elementType, size, mTime, parentPath)
fun new(name: String, elementType: Int, size: Long, mTime: Long, parentPath: String): ExplorerElement {
return ExplorerElement(name, Stat(elementType, size, mTime*1000), parentPath)
}
private fun foldersFirst(a: ExplorerElement, b: ExplorerElement, default: () -> Int): Int {
@ -61,12 +59,12 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
}
"size" -> {
explorerElements.sortWith { a, b ->
doSort(a, b, foldersFirst) { (a.size - b.size).toInt() }
doSort(a, b, foldersFirst) { (a.stat.size - b.stat.size).toInt() }
}
}
"date" -> {
explorerElements.sortWith { a, b ->
doSort(a, b, foldersFirst) { a.mTime.compareTo(b.mTime) }
doSort(a, b, foldersFirst) { a.stat.mTime.compareTo(b.stat.mTime) }
}
}
"name_desc" -> {
@ -76,12 +74,12 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
}
"size_desc" -> {
explorerElements.sortWith { a, b ->
doSort(a, b, foldersFirst) { (b.size - a.size).toInt() }
doSort(a, b, foldersFirst) { (b.stat.size - a.stat.size).toInt() }
}
}
"date_desc" -> {
explorerElements.sortWith { a, b ->
doSort(a, b, foldersFirst) { b.mTime.compareTo(a.mTime) }
doSort(a, b, foldersFirst) { b.stat.mTime.compareTo(a.stat.mTime) }
}
}
}

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.*
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
@ -29,15 +30,15 @@ class FileOperationService : Service() {
}
private val binder = LocalBinder()
private lateinit var gocryptfsVolume: GocryptfsVolume
private lateinit var encryptedVolume: EncryptedVolume
private lateinit var notificationManager: NotificationManagerCompat
private val tasks = HashMap<Int, Job>()
private var lastNotificationId = 0
inner class LocalBinder : Binder() {
fun getService(): FileOperationService = this@FileOperationService
fun setGocryptfsVolume(g: GocryptfsVolume) {
gocryptfsVolume = g
fun setEncryptedVolume(volume: EncryptedVolume) {
encryptedVolume = volume
}
}
@ -121,17 +122,17 @@ class FileOperationService : Service() {
}
}
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
private fun copyFile(srcPath: String, dstPath: String, remoteEncryptedVolume: EncryptedVolume = encryptedVolume): Boolean {
var success = true
val srcHandleId = remoteGocryptfsVolume.openReadMode(srcPath)
if (srcHandleId != -1){
val dstHandleId = gocryptfsVolume.openWriteMode(dstPath)
if (dstHandleId != -1){
val srcFileHandle = remoteEncryptedVolume.openFile(srcPath)
if (srcFileHandle != -1L) {
val dstFileHandle = encryptedVolume.openFile(dstPath)
if (dstFileHandle != -1L) {
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (remoteGocryptfsVolume.readFile(srcHandleId, offset, ioBuffer).also { length = it } > 0) {
val written = gocryptfsVolume.writeFile(dstHandleId, offset, ioBuffer, length).toLong()
while (remoteEncryptedVolume.read(srcFileHandle, ioBuffer, offset).also { length = it } > 0) {
val written = encryptedVolume.write(dstFileHandle, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += written
} else {
@ -139,11 +140,11 @@ class FileOperationService : Service() {
break
}
}
gocryptfsVolume.closeFile(dstHandleId)
encryptedVolume.closeFile(dstFileHandle)
} else {
success = false
}
remoteGocryptfsVolume.closeFile(srcHandleId)
remoteEncryptedVolume.closeFile(srcFileHandle)
} else {
success = false
}
@ -152,22 +153,22 @@ class FileOperationService : Service() {
suspend fun copyElements(
items: ArrayList<OperationFile>,
remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume
remoteEncryptedVolume: EncryptedVolume = encryptedVolume
): String? = coroutineScope {
val notification = showNotification(R.string.file_op_copy_msg, items.size)
val task = async {
var failedItem: String? = null
for (i in 0 until items.size) {
withContext(Dispatchers.IO) {
if (items[i].explorerElement.isDirectory) {
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
failedItem = items[i].explorerElement.fullPath
if (items[i].isDirectory) {
if (!encryptedVolume.pathExists(items[i].dstPath!!)) {
if (!encryptedVolume.mkdir(items[i].dstPath!!)) {
failedItem = items[i].srcPath
}
}
} else {
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)) {
failedItem = items[i].explorerElement.fullPath
if (!copyFile(items[i].srcPath, items[i].dstPath!!, remoteEncryptedVolume)) {
failedItem = items[i].srcPath
}
}
}
@ -183,23 +184,23 @@ class FileOperationService : Service() {
waitForTask(notification, task).failedItem
}
suspend fun moveElements(toMove: List<OperationFile>, toClean: List<ExplorerElement>): String? = coroutineScope {
suspend fun moveElements(toMove: List<OperationFile>, toClean: List<String>): String? = coroutineScope {
val notification = showNotification(R.string.file_op_move_msg, toMove.size)
val task = async(Dispatchers.IO) {
val total = toMove.size+toClean.size
var failedItem: String? = null
for ((i, item) in toMove.withIndex()) {
if (!gocryptfsVolume.rename(item.explorerElement.fullPath, item.dstPath!!)) {
failedItem = item.explorerElement.fullPath
if (!encryptedVolume.rename(item.srcPath, item.dstPath!!)) {
failedItem = item.srcPath
break
} else {
updateNotificationProgress(notification, i+1, total)
}
}
if (failedItem == null) {
for ((i, folder) in toClean.asReversed().withIndex()) {
if (!gocryptfsVolume.rmdir(folder.fullPath)) {
failedItem = folder.fullPath
for ((i, folderPath) in toClean.asReversed().withIndex()) {
if (!encryptedVolume.rmdir(folderPath)) {
failedItem = folderPath
break
} else {
updateNotificationProgress(notification, toMove.size+i+1, total)
@ -221,7 +222,7 @@ class FileOperationService : Service() {
for (i in dstPaths.indices) {
withContext(Dispatchers.IO) {
try {
if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
if (!encryptedVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
failedIndex = i
}
} catch (e: FileNotFoundException) {
@ -301,7 +302,7 @@ class FileOperationService : Service() {
// create destination folders so the new files can use them
for (dir in dstDirs) {
if (!gocryptfsVolume.mkdir(dir)) {
if (!encryptedVolume.mkdir(dir)) {
failedItem = dir
break
}
@ -345,7 +346,7 @@ class FileOperationService : Service() {
return if (outputStream == null) {
false
} else {
gocryptfsVolume.exportFile(srcPath, outputStream)
encryptedVolume.exportFile(srcPath, outputStream)
}
}
@ -355,7 +356,7 @@ class FileOperationService : Service() {
scope: CoroutineScope
): String? {
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
val explorerElements = encryptedVolume.readDir(plain_directory_path) ?: return null
for (e in explorerElements) {
if (!scope.isActive) {
return null

View File

@ -1,11 +1,22 @@
package sushi.hardcore.droidfs.file_operations
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
class OperationFile(val srcPath: String, val type: Int, var dstPath: String? = null, var overwriteConfirmed: Boolean = false) {
val isDirectory = type == Stat.S_IFDIR
val name: String by lazy {
File(srcPath).name
}
val parentPath by lazy {
PathUtils.getParentPath(srcPath)
}
class OperationFile(val explorerElement: ExplorerElement, var dstPath: String? = null, var overwriteConfirmed: Boolean = false) {
companion object {
fun fromExplorerElement(e: ExplorerElement): OperationFile {
return OperationFile(e, null)
return OperationFile(e.fullPath, e.stat.type)
}
}
}

View File

@ -5,18 +5,18 @@ import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DataSpec
import com.google.android.exoplayer2.upstream.TransferListener
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import kotlin.math.ceil
import kotlin.math.min
class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private val filePath: String): DataSource {
private var handleID = -1
class EncryptedVolumeDataSource(private val encryptedVolume: EncryptedVolume, private val filePath: String): DataSource {
private var fileHandle = -1L
private var fileSize: Long = -1
private var fileOffset: Long = 0
override fun open(dataSpec: DataSpec): Long {
fileOffset = dataSpec.position
handleID = gocryptfsVolume.openReadMode(filePath)
fileSize = gocryptfsVolume.getSize(filePath)
fileHandle = encryptedVolume.openFile(filePath)
fileSize = encryptedVolume.getAttr(filePath)!!.size
return fileSize
}
@ -25,7 +25,7 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
}
override fun close() {
gocryptfsVolume.closeFile(handleID)
encryptedVolume.closeFile(fileHandle)
}
override fun addTransferListener(transferListener: TransferListener) {
@ -44,7 +44,7 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
} else {
ByteArray(tmpReadLength)
}
val read = gocryptfsVolume.readFile(handleID, fileOffset, tmpBuff)
val read = encryptedVolume.read(fileHandle, tmpBuff, fileOffset)
System.arraycopy(tmpBuff, 0, buffer, offset+totalRead, read)
fileOffset += read
totalRead += read
@ -52,9 +52,9 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
return totalRead
}
class Factory(private val gocryptfsVolume: GocryptfsVolume, private val filePath: String): DataSource.Factory {
class Factory(private val encryptedVolume: EncryptedVolume, private val filePath: String): DataSource.Factory {
override fun createDataSource(): DataSource {
return GocryptfsDataSource(gocryptfsVolume, filePath)
return EncryptedVolumeDataSource(encryptedVolume, filePath)
}
}
}

View File

@ -10,11 +10,12 @@ import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
abstract class FileViewerActivity: BaseActivity() {
protected lateinit var gocryptfsVolume: GocryptfsVolume
protected lateinit var encryptedVolume: EncryptedVolume
protected lateinit var filePath: String
private lateinit var originalParentPath: String
private lateinit var windowInsetsController: WindowInsetsControllerCompat
@ -33,8 +34,7 @@ abstract class FileViewerActivity: BaseActivity() {
super.onCreate(savedInstanceState)
filePath = intent.getStringExtra("path")!!
originalParentPath = PathUtils.getParentPath(filePath)
val sessionID = intent.getIntExtra("sessionID", -1)
gocryptfsVolume = GocryptfsVolume(applicationContext, sessionID)
encryptedVolume = intent.getParcelableExtra("volume")!!
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
@ -67,7 +67,7 @@ abstract class FileViewerActivity: BaseActivity() {
}
protected fun loadWholeFile(path: String, fileSize: Long? = null): ByteArray? {
val result = gocryptfsVolume.loadWholeFile(path, size = fileSize)
val result = encryptedVolume.loadWholeFile(path, size = fileSize)
if (result.second != 0) {
val dialog = CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
@ -85,10 +85,12 @@ abstract class FileViewerActivity: BaseActivity() {
protected fun createPlaylist() {
if (!wasMapped){
for (e in gocryptfsVolume.recursiveMapFiles(originalParentPath)) {
if (e.isRegularFile) {
if (ConstValues.isExtensionType(getFileType(), e.name) || filePath == e.fullPath) {
mappedPlaylist.add(e)
encryptedVolume.recursiveMapFiles(originalParentPath)?.let { elements ->
for (e in elements) {
if (e.isRegularFile) {
if (ConstValues.isExtensionType(getFileType(), e.name) || filePath == e.fullPath) {
mappedPlaylist.add(e)
}
}
}
}
@ -133,7 +135,7 @@ abstract class FileViewerActivity: BaseActivity() {
override fun onDestroy() {
super.onDestroy()
if (!isFinishingIntentionally) {
gocryptfsVolume.close()
encryptedVolume.close()
RestrictedFileProvider.wipeAll(this)
}
}

View File

@ -99,7 +99,7 @@ class ImageViewer: FileViewerActivity() {
.setTitle(R.string.warning)
.setPositiveButton(R.string.ok) { _, _ ->
createPlaylist() //be sure the playlist is created before deleting if there is only one image
if (gocryptfsVolume.removeFile(filePath)) {
if (encryptedVolume.deleteFile(filePath)) {
playlistNext(true)
refreshPlaylist()
if (mappedPlaylist.size == 0) { //deleted all images of the playlist
@ -275,7 +275,7 @@ class ImageViewer: FileViewerActivity() {
Bitmap.CompressFormat.JPEG
}, 100, outputStream) == true
){
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputStream.toByteArray()), filePath)){
if (encryptedVolume.importFile(ByteArrayInputStream(outputStream.toByteArray()), filePath)){
Toast.makeText(this, R.string.image_saved_successfully, Toast.LENGTH_SHORT).show()
callback()
} else {

View File

@ -25,7 +25,7 @@ abstract class MediaPlayer: FileViewerActivity() {
protected open fun onVideoSizeChanged(width: Int, height: Int) {}
private fun createMediaSource(filePath: String): MediaSource {
val dataSourceFactory = GocryptfsDataSource.Factory(gocryptfsVolume, filePath)
val dataSourceFactory = EncryptedVolumeDataSource.Factory(encryptedVolume, filePath)
return ProgressiveMediaSource.Factory(dataSourceFactory, DefaultExtractorsFactory())
.createMediaSource(MediaItem.fromUri(ConstValues.FAKE_URI))
}

View File

@ -21,7 +21,7 @@ class PdfViewer: FileViewerActivity() {
pdfViewer = PdfViewer(this)
val fileName = File(filePath).name
title = fileName
val fileSize = gocryptfsVolume.getSize(filePath)
val fileSize = encryptedVolume.getAttr(filePath)?.size
loadWholeFile(filePath, fileSize)?.let {
pdfViewer.loadPdf(ByteArrayInputStream(it), fileName, fileSize)
}

View File

@ -3,6 +3,7 @@ package sushi.hardcore.droidfs.file_viewers
import android.annotation.SuppressLint
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
@ -68,14 +69,14 @@ class TextEditor: FileViewerActivity() {
private fun save(): Boolean{
var success = false
val content = editor.text.toString().toByteArray()
val handleID = gocryptfsVolume.openWriteMode(filePath)
if (handleID != -1){
val fileHandle = encryptedVolume.openFile(filePath)
if (fileHandle != -1L) {
val buff = ByteArrayInputStream(content)
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (buff.read(ioBuffer).also { length = it } > 0) {
val written = gocryptfsVolume.writeFile(handleID, offset, ioBuffer, length).toLong()
val written = encryptedVolume.write(fileHandle, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += written
} else {
@ -83,9 +84,9 @@ class TextEditor: FileViewerActivity() {
}
}
if (offset == content.size.toLong()){
success = gocryptfsVolume.truncate(handleID, offset)
success = encryptedVolume.truncate(filePath, offset)
}
gocryptfsVolume.closeFile(handleID)
encryptedVolume.closeFile(fileHandle)
buff.close()
}
if (success){

View File

@ -0,0 +1,99 @@
package sushi.hardcore.droidfs.filesystems
import android.os.Parcel
import sushi.hardcore.droidfs.explorers.ExplorerElement
class CryfsVolume(private val fusePtr: Long): EncryptedVolume() {
companion object {
init {
System.loadLibrary("cryfs_jni")
}
const val CONFIG_FILE_NAME = "cryfs.config"
private external fun nativeInit(baseDir: String, localStateDir: String, password: ByteArray): Long
private external fun nativeCreate(fusePtr: Long, path: String, mode: Int): Long
private external fun nativeOpen(fusePtr: Long, path: String, flags: Int): Long
private external fun nativeRead(fusePtr: Long, fileHandle: Long, buffer: ByteArray, offset: Long): Int
private external fun nativeWrite(fusePtr: Long, fileHandle: Long, offset: Long, buffer: ByteArray, size: Int): Int
private external fun nativeTruncate(fusePtr: Long, path: String, size: Long): Boolean
private external fun nativeDeleteFile(fusePtr: Long, path: String): Boolean
private external fun nativeCloseFile(fusePtr: Long, fileHandle: Long): Boolean
private external fun nativeReadDir(fusePtr: Long, path: String): MutableList<ExplorerElement>?
private external fun nativeMkdir(fusePtr: Long, path: String, mode: Int): Boolean
private external fun nativeRmdir(fusePtr: Long, path: String): Boolean
private external fun nativeGetAttr(fusePtr: Long, path: String): Stat?
private external fun nativeRename(fusePtr: Long, srcPath: String, dstPath: String): Boolean
private external fun nativeClose(fusePtr: Long)
private external fun nativeIsClosed(fusePtr: Long): Boolean
fun init(baseDir: String, localStateDir: String, password: ByteArray): CryfsVolume {
val fusePtr = nativeInit(baseDir, localStateDir, password)
return CryfsVolume(fusePtr)
}
}
constructor(parcel: Parcel) : this(parcel.readLong())
override fun writeToParcel(parcel: Parcel, flags: Int) = with(parcel) {
writeByte(CRYFS_VOLUME_TYPE)
writeLong(fusePtr)
}
override fun openFile(path: String): Long {
val fileHandle = nativeOpen(fusePtr, path, 0)
return if (fileHandle == -1L) {
nativeCreate(fusePtr, path, 0)
} else {
fileHandle
}
}
override fun read(fileHandle: Long, buffer: ByteArray, offset: Long): Int {
return nativeRead(fusePtr, fileHandle, buffer, offset)
}
override fun write(fileHandle: Long, offset: Long, buffer: ByteArray, size: Int): Int {
return nativeWrite(fusePtr, fileHandle, offset, buffer, size)
}
override fun truncate(path: String, size: Long): Boolean {
return nativeTruncate(fusePtr, path, size)
}
override fun closeFile(fileHandle: Long): Boolean {
return nativeCloseFile(fusePtr, fileHandle)
}
override fun deleteFile(path: String): Boolean {
return nativeDeleteFile(fusePtr, path)
}
override fun readDir(path: String): MutableList<ExplorerElement>? {
return nativeReadDir(fusePtr, path)
}
override fun mkdir(path: String): Boolean {
return nativeMkdir(fusePtr, path, Stat.S_IFDIR)
}
override fun rmdir(path: String): Boolean {
return nativeRmdir(fusePtr, path)
}
override fun getAttr(path: String): Stat? {
return nativeGetAttr(fusePtr, path)
}
override fun rename(srcPath: String, dstPath: String): Boolean {
return nativeRename(fusePtr, srcPath, dstPath)
}
override fun close() {
return nativeClose(fusePtr)
}
override fun isClosed(): Boolean {
return nativeIsClosed(fusePtr)
}
}

View File

@ -0,0 +1,214 @@
package sushi.hardcore.droidfs.filesystems
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.SavedVolume
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
abstract class EncryptedVolume: Parcelable {
companion object {
const val GOCRYPTFS_VOLUME_TYPE: Byte = 0
const val CRYFS_VOLUME_TYPE: Byte = 1
@JvmField
val CREATOR = object : Parcelable.Creator<EncryptedVolume> {
override fun createFromParcel(parcel: Parcel): EncryptedVolume {
return when (parcel.readByte()) {
GOCRYPTFS_VOLUME_TYPE -> GocryptfsVolume(parcel)
CRYFS_VOLUME_TYPE -> CryfsVolume(parcel)
else -> throw invalidVolumeType()
}
}
override fun newArray(size: Int) = arrayOfNulls<EncryptedVolume>(size)
}
fun getVolumeType(path: String): Byte {
return if (File(path, GocryptfsVolume.CONFIG_FILE_NAME).isFile) {
GOCRYPTFS_VOLUME_TYPE
} else if (File(path, CryfsVolume.CONFIG_FILE_NAME).isFile) {
CRYFS_VOLUME_TYPE
} else {
-1
}
}
fun init(volume: SavedVolume, filesDir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): EncryptedVolume? {
return when (volume.type) {
GOCRYPTFS_VOLUME_TYPE -> {
GocryptfsVolume.init(volume.getFullPath(filesDir), password, givenHash, returnedHash)
}
CRYFS_VOLUME_TYPE -> {
CryfsVolume.init(volume.getFullPath(filesDir), PathUtils.pathJoin(filesDir, "localState"), password!!)
}
else -> throw invalidVolumeType()
}
}
private fun invalidVolumeType(): java.lang.RuntimeException {
return RuntimeException("Invalid volume type")
}
}
override fun describeContents() = 0
abstract fun openFile(path: String): Long
abstract fun read(fileHandle: Long, buffer: ByteArray, offset: Long): Int
abstract fun write(fileHandle: Long, offset: Long, buffer: ByteArray, size: Int): Int
abstract fun closeFile(fileHandle: Long): Boolean
abstract fun truncate(path: String, size: Long): Boolean
abstract fun deleteFile(path: String): Boolean
abstract fun readDir(path: String): MutableList<ExplorerElement>?
abstract fun mkdir(path: String): Boolean
abstract fun rmdir(path: String): Boolean
abstract fun getAttr(path: String): Stat?
abstract fun rename(srcPath: String, dstPath: String): Boolean
abstract fun close()
abstract fun isClosed(): Boolean
fun pathExists(path: String): Boolean {
return getAttr(path) != null
}
fun exportFile(fileHandle: Long, os: OutputStream): Boolean {
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (read(fileHandle, ioBuffer, offset).also { length = it } > 0){
os.write(ioBuffer, 0, length)
offset += length.toLong()
}
os.close()
return true
}
fun exportFile(src_path: String, os: OutputStream): Boolean {
var success = false
val srcfileHandle = openFile(src_path)
if (srcfileHandle != -1L) {
success = exportFile(srcfileHandle, os)
closeFile(srcfileHandle)
}
return success
}
fun exportFile(src_path: String, dst_path: String): Boolean {
return exportFile(src_path, FileOutputStream(dst_path))
}
fun exportFile(context: Context, src_path: String, output_path: Uri): Boolean {
val os = context.contentResolver.openOutputStream(output_path)
if (os != null){
return exportFile(src_path, os)
}
return false
}
fun importFile(inputStream: InputStream, dst_path: String): Boolean {
val dstfileHandle = openFile(dst_path)
if (dstfileHandle != -1L) {
var success = true
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (inputStream.read(ioBuffer).also { length = it } > 0) {
val written = write(dstfileHandle, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += written
} else {
inputStream.close()
success = false
break
}
}
closeFile(dstfileHandle)
inputStream.close()
return success
}
return false
}
fun importFile(context: Context, src_uri: Uri, dst_path: String): Boolean {
val inputStream = context.contentResolver.openInputStream(src_uri)
if (inputStream != null){
return importFile(inputStream, dst_path)
}
return false
}
fun loadWholeFile(fullPath: String, size: Long? = null, maxSize: Long? = null): Pair<ByteArray?, Int> {
val fileSize = size ?: getAttr(fullPath)?.size ?: -1
return if (fileSize >= 0) {
maxSize?.let {
if (fileSize > it) {
return Pair(null, 0)
}
}
try {
val fileBuff = ByteArray(fileSize.toInt())
val fileHandle = openFile(fullPath)
if (fileHandle == -1L) {
Pair(null, 3)
} else {
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (read(fileHandle, ioBuffer, offset).also { length = it } > 0) {
System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length)
offset += length.toLong()
}
closeFile(fileHandle)
if (offset == fileBuff.size.toLong()) {
Pair(fileBuff, 0)
} else {
Pair(null, 4)
}
}
} catch (e: OutOfMemoryError) {
Pair(null, 2)
}
} else {
Pair(null, 1)
}
}
fun recursiveMapFiles(rootPath: String): MutableList<ExplorerElement>? {
val result = mutableListOf<ExplorerElement>()
val explorerElements = readDir(rootPath) ?: return null
result.addAll(explorerElements)
for (e in explorerElements) {
if (e.isDirectory) {
result.addAll(recursiveMapFiles(e.fullPath) ?: return null)
}
}
return result
}
fun recursiveRemoveDirectory(path: String): String? {
readDir(path)?.let { elements ->
for (e in elements) {
val fullPath = PathUtils.pathJoin(path, e.name)
if (e.isDirectory) {
val result = recursiveRemoveDirectory(fullPath)
result?.let { return it }
} else {
if (!deleteFile(fullPath)) {
return fullPath
}
}
}
}
return if (!rmdir(path)) {
path
} else {
null
}
}
}

View File

@ -0,0 +1,14 @@
package sushi.hardcore.droidfs.filesystems
class Stat(val type: Int, var size: Long, val mTime: Long) {
companion object {
const val S_IFDIR = 0x4000
const val S_IFREG = 0x8000
const val S_IFLNK = 0xA000
const val PARENT_FOLDER_TYPE = -1
fun parentFolderStat(): Stat {
return Stat(PARENT_FOLDER_TYPE, -1, -1)
}
}
}

View File

@ -6,6 +6,7 @@ import android.net.Uri
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.ContextCompat
import sushi.hardcore.droidfs.R
@ -19,16 +20,16 @@ object PathUtils {
fun getParentPath(path: String): String {
return if (path.endsWith("/")) {
val a = path.substring(0, path.length - 2)
if (a.contains("/")) {
a.substring(0, a.lastIndexOf("/"))
if (a.count { it == '/' } == 1) {
"/"
} else {
""
a.substring(0, a.lastIndexOf("/"))
}
} else {
if (path.contains("/")) {
path.substring(0, path.lastIndexOf("/"))
if (path.count { it == '/' } == 1) {
"/"
} else {
""
path.substring(0, path.lastIndexOf("/"))
}
}
}

View File

@ -78,16 +78,15 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_createVolume(JNIEnv *
}
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_init(JNIEnv *env, jobject clazz,
Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_nativeInit(JNIEnv *env, jobject clazz,
jstring jroot_cipher_dir,
jcharArray jpassword,
jbyteArray jpassword,
jbyteArray jgiven_hash,
jbyteArray jreturned_hash) {
const char* root_cipher_dir = (*env)->GetStringUTFChars(env, jroot_cipher_dir, NULL);
GoString go_root_cipher_dir = {root_cipher_dir, strlen(root_cipher_dir)};
size_t password_len;
jchar* jchar_password;
char* password;
GoSlice go_password = {NULL, 0, 0};
size_t given_hash_len;
@ -96,9 +95,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_init(JNIEnv *env, job
GoSlice go_given_hash = {NULL, 0, 0};
if ((*env)->IsSameObject(env, jgiven_hash, NULL)){
password_len = (*env)->GetArrayLength(env, jpassword);
jchar_password = (*env)->GetCharArrayElements(env, jpassword, NULL);
password = malloc(password_len);
jcharArray_to_charArray(jchar_password, password, password_len);
password = (char*)(*env)->GetByteArrayElements(env, jpassword, NULL);
go_password.data = password;
go_password.len = password_len;
go_password.cap = password_len;
@ -131,8 +128,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_init(JNIEnv *env, job
if ((*env)->IsSameObject(env, jgiven_hash, NULL)){
wipe(password, password_len);
free(password);
(*env)->ReleaseCharArrayElements(env, jpassword, jchar_password, 0);
(*env)->ReleaseByteArrayElements(env, jpassword, (jbyte*)password, 0);
} else {
wipe(given_hash, given_hash_len);
free(given_hash);
@ -256,7 +252,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1list_1dir(JNIEnv *env, jobje
jmethodID java_ArrayList_add = (*env)->GetMethodID(env, java_ArrayList, "add", "(Ljava/lang/Object;)Z");
jclass classExplorerElement = (*env)->NewLocalRef(env, (*env)->FindClass(env, "sushi/hardcore/droidfs/explorers/ExplorerElement"));
jmethodID explorerElement_new = (*env)->GetStaticMethodID(env, classExplorerElement, "new", "(Ljava/lang/String;SJJLjava/lang/String;)Lsushi/hardcore/droidfs/explorers/ExplorerElement;");
jmethodID explorerElement_new = (*env)->GetStaticMethodID(env, classExplorerElement, "new", "(Ljava/lang/String;IJJLjava/lang/String;)Lsushi/hardcore/droidfs/explorers/ExplorerElement;");
jobject element_list = (*env)->NewObject(env, java_ArrayList, java_ArrayList_init, elements.r2);
unsigned int c = 0;
for (unsigned int i=0; i<elements.r2; ++i){
@ -277,19 +273,15 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1list_1dir(JNIEnv *env, jobje
GoString go_name = {gcf_full_path, strlen(gcf_full_path)};
struct gcf_get_attrs_return attrs = gcf_get_attrs(sessionID, go_name);
short type = 0; //directory
if (S_ISREG(elements.r1[i])){
type = 1; //regular file
}
jstring jname = (*env)->NewStringUTF(env, name);
jobject explorerElement = (*env)->CallStaticObjectMethod(
env,
classExplorerElement,
explorerElement_new,
jname,
type,
(long long) attrs.r0,
attrs.r1,
elements.r1[i],
(long long) attrs.r1,
attrs.r2,
jplain_dir
);
(*env)->CallBooleanMethod(env, element_list, java_ArrayList_add, explorerElement);
@ -306,8 +298,8 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1list_1dir(JNIEnv *env, jobje
return element_list;
}
JNIEXPORT jlong JNICALL
Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1get_1size(JNIEnv *env, jobject thiz,
JNIEXPORT jobject JNICALL
Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1get_1attr(JNIEnv *env, jobject thiz,
jint sessionID, jstring jfile_path) {
const char* file_path = (*env)->GetStringUTFChars(env, jfile_path, NULL);
GoString go_file_path = {file_path, strlen(file_path)};
@ -316,21 +308,18 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1get_1size(JNIEnv *env, jobje
(*env)->ReleaseStringUTFChars(env, jfile_path, file_path);
return attrs.r0;
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1path_1exists(JNIEnv *env, jobject thiz,
jint sessionID,
jstring jfile_path) {
const char* file_path = (*env)->GetStringUTFChars(env, jfile_path, NULL);
GoString go_file_path = {file_path, strlen(file_path)};
struct gcf_get_attrs_return attrs = gcf_get_attrs(sessionID, go_file_path);
(*env)->ReleaseStringUTFChars(env, jfile_path, file_path);
return attrs.r1 != 0;
if (attrs.r3 == 1) {
jclass stat = (*env)->FindClass(env, "sushi/hardcore/droidfs/filesystems/Stat");
jmethodID statInit = (*env)->GetMethodID(env, stat, "<init>", "(IJJ)V");
return (*env)->NewObject(
env, stat, statInit,
(jint)attrs.r0,
(jlong)attrs.r1,
(jlong)attrs.r2
);
} else {
return NULL;
}
}
JNIEXPORT jint JNICALL
@ -396,8 +385,15 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1read_1file(JNIEnv *env, jobj
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1truncate(JNIEnv *env, jobject thiz,
jint sessionID,
jint handleID, jlong offset) {
return gcf_truncate(sessionID, handleID, offset);
jstring jpath,
jlong offset) {
const char* path = (*env)->GetStringUTFChars(env, jpath, NULL);
GoString go_path = {path, strlen(path)};
GoUint8 result = gcf_truncate(sessionID, go_path, offset);
(*env)->ReleaseStringUTFChars(env, jpath, path);
return result;
}
JNIEXPORT void JNICALL

View File

@ -0,0 +1,198 @@
#include <sys/stat.h>
#include <jni.h>
jlong cryfs_init(JNIEnv* env, jstring jbaseDir, jstring jlocalSateDir, jbyteArray jpassword);
jlong cryfs_create(JNIEnv* env, jlong fusePtr, jstring jpath, mode_t mode);
jlong cryfs_open(JNIEnv* env, jlong fusePtr, jstring jpath, jint flags);
jint cryfs_read(JNIEnv* env, jlong fusePtr, jlong fileHandle, jbyteArray jbuffer, jlong offset);
jint cryfs_write(JNIEnv* env, jlong fusePtr, jlong fileHandle, jlong offset, jbyteArray jbuffer, jint size);
jint cryfs_truncate(JNIEnv* env, jlong fusePtr, jstring jpath, jlong size);
jint cryfs_unlink(JNIEnv* env, jlong fusePtr, jstring jpath);
jint cryfs_release(jlong fusePtr, jlong fileHandle);
jlong cryfs_readdir(JNIEnv* env, jlong fusePtr, jstring jpath ,void* data, int(void*, const char*, const struct stat*));
jint cryfs_mkdir(JNIEnv* env, jlong fusePtr, jstring jpath, mode_t mode);
jint cryfs_rmdir(JNIEnv* env, jlong fusePtr, jstring jpath);
jint cryfs_getattr(JNIEnv* env, jlong fusePtr, jstring jpath, struct stat* stat);
jint cryfs_rename(JNIEnv* env, jlong fusePtr, jstring jsrcPath, jstring jdstPath);
void cryfs_destroy(jlong fusePtr);
jboolean cryfs_is_closed(jlong fusePtr);
JNIEXPORT jlong JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeInit(JNIEnv *env, jobject thiz,
jstring base_dir, jstring jlocalStateDir,
jbyteArray password) {
return cryfs_init(env, base_dir, jlocalStateDir, password);
}
JNIEXPORT jlong JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeCreate(JNIEnv *env, jobject thiz,
jlong fuse_ptr, jstring path,
jint mode) {
return cryfs_create(env, fuse_ptr, path, mode);
}
JNIEXPORT jlong JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeOpen(JNIEnv *env, jobject thiz,
jlong fuse_ptr, jstring path,
jint flags) {
return cryfs_open(env, fuse_ptr, path, flags);
}
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeRead(JNIEnv *env, jobject thiz,
jlong fuse_ptr, jlong file_handle,
jbyteArray buffer, jlong offset) {
return cryfs_read(env, fuse_ptr, file_handle, buffer, offset);
}
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeWrite(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jlong file_handle,
jlong offset,
jbyteArray buffer,
jint size) {
return cryfs_write(env, fuse_ptr, file_handle, offset, buffer, size);
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeTruncate(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring path,
jlong size) {
return cryfs_truncate(env, fuse_ptr, path, size) == 0;
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeDeleteFile(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring path) {
return cryfs_unlink(env, fuse_ptr, path) == 0;
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeCloseFile(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jlong file_handle) {
return cryfs_release(fuse_ptr, file_handle) == 0;
}
struct readDirHelper {
JNIEnv* env;
jclass explorerElement;
jmethodID explorerElementNew;
jmethodID arrayListAdd;
jobject elementList;
jstring jparentPath;
};
int readDir(void* data, const char* name, const struct stat* stat) {
struct readDirHelper* helper = (struct readDirHelper*)data;
jstring jname = (*helper->env)->NewStringUTF(helper->env, name);
jobject explorerElement = (*helper->env)->CallStaticObjectMethod(
helper->env,
helper->explorerElement,
helper->explorerElementNew,
jname,
stat->st_mode,
stat->st_size,
stat->st_mtim.tv_sec,
helper->jparentPath
);
(*helper->env)->CallBooleanMethod(helper->env, helper->elementList, helper->arrayListAdd, explorerElement);
(*helper->env)->DeleteLocalRef(helper->env, explorerElement);
(*helper->env)->DeleteLocalRef(helper->env, jname);
return 0;
}
JNIEXPORT jobject JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeReadDir(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring path) {
jclass arrayList = (*env)->NewLocalRef(env, (*env)->FindClass(env, "java/util/ArrayList"));
jmethodID arrayListInit = (*env)->GetMethodID(env, arrayList, "<init>", "()V");
struct readDirHelper helper;
helper.env = env;
helper.explorerElement = (*env)->NewLocalRef(env, (*env)->FindClass(env, "sushi/hardcore/droidfs/explorers/ExplorerElement"));
helper.explorerElementNew = (*env)->GetStaticMethodID(
env, helper.explorerElement, "new",
"(Ljava/lang/String;IJJLjava/lang/String;)Lsushi/hardcore/droidfs/explorers/ExplorerElement;"
);
helper.arrayListAdd = (*env)->GetMethodID(env, arrayList, "add", "(Ljava/lang/Object;)Z");
helper.elementList = (*env)->NewObject(env, arrayList, arrayListInit);
helper.jparentPath = path;
int result = cryfs_readdir(env, fuse_ptr, path, &helper, readDir);
if (result == 0) {
return helper.elementList;
} else {
return NULL;
}
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeMkdir(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring path,
jint mode) {
return cryfs_mkdir(env, fuse_ptr, path, mode) == 0;
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeRmdir(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring path) {
return cryfs_rmdir(env, fuse_ptr, path) == 0;
}
JNIEXPORT jobject JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeGetAttr(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring path) {
struct stat stbuf;
int result = cryfs_getattr(env, fuse_ptr, path, &stbuf);
if (result == 0) {
jclass stat = (*env)->FindClass(env, "sushi/hardcore/droidfs/filesystems/Stat");
jmethodID statInit = (*env)->GetMethodID(env, stat, "<init>", "(IJJ)V");
return (*env)->NewObject(
env, stat, statInit,
(jint)stbuf.st_mode,
(jlong)stbuf.st_size,
(jlong)stbuf.st_mtim.tv_sec
);
} else {
return NULL;
}
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeRename(JNIEnv *env,
jobject thiz,
jlong fuse_ptr,
jstring src_path,
jstring dst_path) {
return cryfs_rename(env, fuse_ptr, src_path, dst_path) == 0;
}
JNIEXPORT void JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeClose(JNIEnv *env,
jobject thiz,
jlong fuse_ptr) {
cryfs_destroy(fuse_ptr);
}
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeIsClosed(JNIEnv *env,
jobject thiz,
jlong fuse_ptr) {
return cryfs_is_closed(fuse_ptr);
}

View File

@ -39,7 +39,7 @@
<string name="external_open">فتح بتطبيق خارجي</string>
<string name="single_delete_confirm">هل أنت متأكد من حذف %s ?</string>
<string name="multiple_delete_confirm">هل أنت متأكد من حذف هذه %s العناصر ?</string>
<string name="location">موقع: /%s</string>
<string name="location">موقع: %s</string>
<string name="total_size">الحجم الكلي: %s</string>
<string name="import_from_other_volume">إستيراد من مجلد مشفر آخر</string>
<string name="read_file_failed">لقد فشل فتح هذا الملف.</string>

View File

@ -39,7 +39,7 @@
<string name="external_open">Abrir con una aplicación externa</string>
<string name="single_delete_confirm">¿Estás seguro de que quieres borrar %s ?</string>
<string name="multiple_delete_confirm">¿Estás seguro de que quiere borrar %s objetos?</string>
<string name="location">Ubicación: /%s</string>
<string name="location">Ubicación: %s</string>
<string name="total_size">Tamaño total: %s</string>
<string name="import_from_other_volume">Importar desde otro volumen</string>
<string name="read_file_failed">No se ha podido abrir este archivo.</string>

View File

@ -39,7 +39,7 @@
<string name="external_open">Abrir com app externo</string>
<string name="single_delete_confirm">Você tem certeza que quer excluir %s?</string>
<string name="multiple_delete_confirm">Você realmente deseja excluir estos %s itens?</string>
<string name="location">Localização: /%s</string>
<string name="location">Localização: %s</string>
<string name="total_size">Tamanho total: %s</string>
<string name="import_from_other_volume">Importar do outro volume</string>
<string name="read_file_failed">Falha ao abrir este arquivo.</string>

View File

@ -37,7 +37,7 @@
<string name="external_open">Открыть внешним приложением</string>
<string name="single_delete_confirm">Удалить %s?</string>
<string name="multiple_delete_confirm">Удалить %s элементов?</string>
<string name="location">Путь: /%s</string>
<string name="location">Путь: %s</string>
<string name="total_size">Общий размер: %s</string>
<string name="import_from_other_volume">Импорт из другого тома</string>
<string name="read_file_failed">Невозможно открыть файл.</string>

View File

@ -39,7 +39,7 @@
<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>
<string name="location">Location: /%s</string>
<string name="location">Location: %s</string>
<string name="total_size">Total size: %s</string>
<string name="import_from_other_volume">Import from another volume</string>
<string name="read_file_failed">Failed to open this file.</string>