From cf4927a90b40d99256ec3dcf9fe9c427f8edae64 Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Sat, 18 Jun 2022 21:13:16 +0200 Subject: [PATCH] CryFS --- .gitmodules | 3 + BUILD.md | 108 ++++++++ README.md | 79 +----- app/CMakeLists.txt | 45 ++-- app/build.gradle | 37 ++- app/libcryfs | 1 + app/libgocryptfs | 2 +- .../sushi/hardcore/droidfs/CameraActivity.kt | 24 +- .../droidfs/ChangePasswordActivity.kt | 3 +- .../sushi/hardcore/droidfs/ConstValues.kt | 4 +- .../hardcore/droidfs/FingerprintProtector.kt | 4 +- .../sushi/hardcore/droidfs/GocryptfsVolume.kt | 243 ------------------ .../sushi/hardcore/droidfs/MainActivity.kt | 110 ++++---- .../droidfs/{Volume.kt => SavedVolume.kt} | 20 +- .../sushi/hardcore/droidfs/VolumeDatabase.kt | 87 +++++-- .../adapters/ExplorerElementAdapter.kt | 21 +- .../droidfs/adapters/VolumeAdapter.kt | 10 +- .../add_volume/CreateVolumeFragment.kt | 127 +++++---- .../droidfs/add_volume/SelectPathFragment.kt | 17 +- .../content_providers/ExternalProvider.kt | 14 +- .../droidfs/explorers/BaseExplorerActivity.kt | 70 ++--- .../droidfs/explorers/ExplorerActivity.kt | 73 +++--- .../droidfs/explorers/ExplorerActivityPick.kt | 12 +- .../droidfs/explorers/ExplorerElement.kt | 34 ++- .../file_operations/FileOperationService.kt | 63 ++--- .../droidfs/file_operations/OperationFile.kt | 15 +- ...Source.kt => EncryptedVolumeDataSource.kt} | 18 +- .../file_viewers/FileViewerActivity.kt | 21 +- .../droidfs/file_viewers/ImageViewer.kt | 4 +- .../droidfs/file_viewers/MediaPlayer.kt | 2 +- .../droidfs/file_viewers/PdfViewer.kt | 2 +- .../droidfs/file_viewers/TextEditor.kt | 14 +- .../droidfs/filesystems/CryfsVolume.kt | 123 +++++++++ .../droidfs/filesystems/EncryptedVolume.kt | 214 +++++++++++++++ .../droidfs/filesystems/GocryptfsVolume.kt | 103 ++++++++ .../hardcore/droidfs/filesystems/Stat.kt | 14 + .../sushi/hardcore/droidfs/util/PathUtils.kt | 49 ++-- .../sushi/hardcore/droidfs/util/WidgetUtil.kt | 18 ++ app/src/main/native/gocryptfs_jni.c | 184 +++++++------ app/src/main/native/libcryfs.c | 184 +++++++++++++ .../res/layout/fragment_create_volume.xml | 16 +- app/src/main/res/values-ar/arrays.xml | 6 - app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values-pt-rBR/strings.xml | 4 +- app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values/arrays.xml | 16 +- app/src/main/res/values/strings.xml | 10 +- build.gradle | 2 +- .../android/en-US/full_description.txt | 14 +- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 52 files changed, 1439 insertions(+), 823 deletions(-) create mode 100644 BUILD.md create mode 160000 app/libcryfs delete mode 100644 app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt rename app/src/main/java/sushi/hardcore/droidfs/{Volume.kt => SavedVolume.kt} (56%) rename app/src/main/java/sushi/hardcore/droidfs/file_viewers/{GocryptfsDataSource.kt => EncryptedVolumeDataSource.kt} (73%) create mode 100644 app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt create mode 100644 app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt create mode 100644 app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt create mode 100644 app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt create mode 100644 app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt create mode 100644 app/src/main/native/libcryfs.c diff --git a/.gitmodules b/.gitmodules index 0e2fb5e..7d464bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "libpdfviewer"] path = libpdfviewer url = https://forge.chapril.org/hardcoresushi/libpdfviewer.git +[submodule "app/libcryfs"] + path = app/libcryfs + url = https://forge.chapril.org/hardcoresushi/libcryfs.git diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..dec1916 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,108 @@ +# Introduction +DroidFS relies on modified versions of the original encrypted filesystems programs to open volumes. [CryFS](https://github.com/cryfs/cryfs) is written in C++ while [gocryptfs](https://github.com/rfjakob/gocryptfs) is written in [Go](https://golang.org). Thus, building DroidFS requires the compilation of native code. However, for the sake of simplicity, the application has been designed in a modular way: you can build a version of DroidFS that supports both Gocryptfs and CryFS, or only one of the two. + +Moreover, DroidFS aims to be accessible to as many people as possible. If you encounter any problems or need help with the build, feel free to open an issue, a discussion, or contact me by [email](mailto:hardcore.sushi@disroot.org) or on [Matrix](https://matrix.org): @hardcoresushi:matrix.underworld.fr + +# Setup +Install required packages: +``` +$ sudo apt-get install openjdk-11-jdk-headless build-essential pkg-config git gnupg2 wget apksigner +``` +You also need to manually install the [Android SDK](https://developer.android.com/studio/index.html#command-tools) and the [Android Native Development Kit (NDK)](https://developer.android.com/ndk/downloads) (r23 versions are recommended). + +If you want a support for Gocryptfs volumes, you must install [Go](https://golang.org/doc/install) and libssl: +``` +$ sudo apt-get install golang-go libssl-dev +``` +The code should be authenticated before being built. To verify the signatures, you will need my PGP key: +``` +$ gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys AFE384344A45E13A +``` +Fingerprint: `B64E FE86 CEE1 D054 F082 1711 AFE3 8434 4A45 E13A` \ +Email: `Hardcore Sushi ` + +# Download sources +Download DroidFS source code: +``` +$ git clone --depth=1 https://github.com/hardcore-sushi/DroidFS.git +``` +Verify sources: +``` +$ cd DroidFS +$ git verify-commit HEAD +``` +__Don't continue if the verification fails!__ + +Initialize submodules: +``` +$ git submodule update --depth=1 --init +``` +[FFmpeg](https://ffmpeg.org) is needed to record encrypted video: +``` +$ cd app/ffmpeg +$ git clone --depth=1 https://git.ffmpeg.org/ffmpeg.git +``` +If you want Gocryptfs support, you need to download OpenSSL: +``` +$ cd ../libgocryptfs +$ wget https://www.openssl.org/source/openssl-1.1.1p.tar.gz +``` +Verify OpenSSL signature: +``` +$ wget https://www.openssl.org/source/openssl-1.1.1p.tar.gz.asc +$ gpg --verify openssl-1.1.1p.tar.gz.asc openssl-1.1.1p.tar.gz +``` +Continue **ONLY** if the signature is **VALID**. +``` +$ tar -xzf openssl-1.1.1p.tar.gz +``` +If you want CryFS support, initialize libcryfs: +``` +$ cd app/libcryfs +$ git submodule update --depth=1 --init +``` + +# Build +Retrieve your Android NDK installation path, usually something like `/home/\/Android/SDK/ndk/\`. Then, make it available in your shell: +``` +$ export ANDROID_NDK_HOME="" +``` +Start by compiling FFmpeg: +``` +$ cd app/ffmpeg +$ ./build.sh ffmpeg +``` +## libgocryptfs +This step is only required if you want Gocryptfs support. +``` +$ cd app/libgocryptfs +$ OPENSSL_PATH="./openssl-1.1.1p" ./build.sh + ``` +## Compile APKs +Gradle build libgocryptfs and libcryfs by default. + +To build DroidFS without Gocryptfs support, run: +``` +$ ./gradlew assembleRelease -PdisableGocryptfs=true +``` +To build DroidFS without CryFS support, run: +``` +$ ./gradlew assembleRelease -PdisableCryFS=true +``` +If you want to build DroidFS with support for both Gocryptfs and CryFS, just run: +``` +$ ./gradlew assembleRelease +``` + +# Sign APKs +If the build succeeds, you will find the unsigned APKs in `app/build/outputs/apk/release/`. These APKs need to be signed in order to be installed on an Android device. + +If you don't already have a keystore, you can create a new one by running: +``` +$ keytool -genkey -keystore -alias -keyalg EC -validity 10000 +``` +Then, sign the APK with: +``` +$ apksigner sign --out droidfs.apk -v --ks app/build/outputs/apk/release/ +``` +Now you can install `droidfs.apk` on your device. diff --git a/README.md b/README.md index 8d7554e..d762062 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DroidFS DroidFS is an alternative way to use encrypted overlay filesystems on Android that uses its own internal file explorer instead of mounting virtual volumes. -It currently only works with [gocryptfs](https://github.com/rfjakob/gocryptfs) but support for [CryFS](https://github.com/cryfs/cryfs) could be added in the future. +It currently supports [gocryptfs](https://github.com/rfjakob/gocryptfs) and [CryFS](https://github.com/cryfs/cryfs) (alpha).

@@ -46,7 +46,7 @@ It is strongly recommended to read the documentation of a feature before enablin -You can download DroidFS from [F-Droid](https://f-droid.org/packages/sushi.hardcore.droidfs) or from the "Releases" section in the repo. +You can download DroidFS from [F-Droid](https://f-droid.org/packages/sushi.hardcore.droidfs) or from the "Releases" section in this repository. APKs available here are signed with my PGP key available on keyservers: @@ -83,83 +83,24 @@ DroidFS need some permissions to work properly. Here is why: # Limitations -DroidFS use some parts of the original gocryptfs code, which is designed to run on Linux x86 systems: it accesses the underlying file system with file paths and syscalls. However in Android, you can't access other apps files with file paths. Instead, you must use the [ContentProvider](https://developer.android.com/guide/topics/providers/content-providers) API. And obviously, the original gocryptfs code doesn't work with this API. This is why DroidFS can't open volumes provided by other applications, such as cloud storage clients. You can only use DroidFS with volumes located on shared storage or in the app's internal storage (hidden volumes). External storage such as SD cards are only supported in read-only access for now. +DroidFS works as a wrapper around modified versions of the original encrypted container implementations ([libgocryptfs](https://forge.chapril.org/hardcoresushi/libgocryptfs) and [libcryfs](https://forge.chapril.org/hardcoresushi/libcryfs)). These programs were designed to run on standard x86 Linux systems: they access the underlying file system with file paths and syscalls. However, on Android, you can't access files from other applications using file paths. Instead, one has to use the [ContentProvider](https://developer.android.com/guide/topics/providers/content-providers) API. Obviously, neither Gocryptfs nor CryFS support this API. As a result, DroidFS cannot open volumes provided by other applications (such as cloud storage clients), nor can it allow other applications to access encrypted volumes once opened. -# Build -Most of the original gocryptfs code was used as is (written in Go) and compiled to native code. That's why you need [Go](https://golang.org) and the [Android Native Development Kit (NDK)](https://developer.android.com/ndk/) to build DroidFS from source. +Due to Android's storage restrictions, encrypted volumes located on SD cards must be placed under `/Android/data/sushi.hardcore.droidfs/` if you want DroidFS to be able to modify them. -#### Install dependencies -On debian: -``` -$ sudo apt-get install build-essential pkg-config libssl-dev -``` -Install [Go](https://golang.org/doc/install): -``` -$ sudo apt-get install golang-go -``` -You also need to install the Android SDK build tools and the [Android NDK](https://developer.android.com/studio/projects/install-ndk). - -#### Download Sources -``` -$ git clone --recurse-submodules https://github.com/hardcore-sushi/DroidFS.git -$ cd DroidFS -``` -[libgocryptfs](https://forge.chapril.org/hardcoresushi/libgocryptfs) needs OpenSSL: -``` -$ cd app/libgocryptfs -$ wget https://www.openssl.org/source/openssl-1.1.1n.tar.gz -``` -Verify OpenSSL signature: -``` -$ wget https://www.openssl.org/source/openssl-1.1.1n.tar.gz.asc -$ gpg --verify openssl-1.1.1n.tar.gz.asc openssl-1.1.1n.tar.gz -``` -Continue **ONLY** if the signature is **VALID**. -``` -$ tar -xvzf openssl-1.1.1n.tar.gz -``` -DroidFS also need [FFmpeg](https://ffmpeg.org) to record encrypted video: -``` -$ cd app/ffmpeg -$ git clone --depth=1 https://git.ffmpeg.org/ffmpeg.git -``` - -#### Generate a keystore -APKs must be signed to be installed on an Android device. If you don't already have a keystore, you can generate one by running: -``` -$ keytool -genkey -keystore -alias -keyalg EC -validity 10000 -``` - -#### Build -Retrieve your Android NDK installation path, usually something like "/home/\/Android/SDK/ndk/\". Now you can build libgocryptfs: -``` -$ cd DroidFS/app/libgocryptfs -$ env ANDROID_NDK_HOME="" OPENSSL_PATH="./openssl-1.1.1n" ./build.sh - ``` -Then FFmpeg: -``` -$ cd app/ffmpeg -$ env ANDROID_NDK_HOME="" ./build.sh ffmpeg -``` -Finally, compile the app: -``` -$ ./gradlew assembleRelease -``` -If the build succeeds, you will find the unsigned APKs in `app/build/outputs/apk/release/`. You need to sign them in order to install the app: -``` -$ apksigner sign --out droidfs.apk -v --ks app/build/outputs/apk/release/ -``` -Now you can install `droidfs.apk` on your device. +# Building from source +You can follow the instructions in [BUILD.md](BUILD.md) to build DroidFS from source. # Third party code Thanks to these open source projects that DroidFS uses: ### Modified code: -- [libgocryptfs](https://forge.chapril.org/hardcoresushi/libgocryptfs) (forked from [gocryptfs](https://github.com/rfjakob/gocryptfs)) to encrypt your data +- Encrypted filesystems (to protect your data): + - [libgocryptfs](https://forge.chapril.org/hardcoresushi/libgocryptfs) (forked from [gocryptfs](https://github.com/rfjakob/gocryptfs)) + - [libcryfs](https://forge.chapril.org/hardcoresushi/libcryfs) (forked from [CryFS](https://github.com/cryfs/cryfs)) - [libpdfviewer](https://forge.chapril.org/hardcoresushi/libpdfviewer) (forked from [PdfViewer](https://github.com/GrapheneOS/PdfViewer)) to open PDF files - [DoubleTapPlayerView](https://github.com/vkay94/DoubleTapPlayerView) to add double-click controls to the video player ### Borrowed code: -- [MaterialFiles](https://github.com/zhanghai/MaterialFiles) for kotlin natural sorting implementation +- [MaterialFiles](https://github.com/zhanghai/MaterialFiles) for Kotlin natural sorting implementation ### Libraries: - [Glide](https://github.com/bumptech/glide/) to display pictures - [ExoPlayer](https://github.com/google/ExoPlayer) to play media files diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d9ea853..2d522e6 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,27 +1,27 @@ cmake_minimum_required(VERSION 3.10) -add_library( - gocryptfs - SHARED - IMPORTED -) +project(DroidFS) -set_target_properties( - gocryptfs - PROPERTIES IMPORTED_LOCATION - ${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI}/libgocryptfs.so -) +option(GOCRYPTFS "build libgocryptfs" ON) +option(CRYFS "build libcryfs" ON) -add_library( - gocryptfs_jni - SHARED - src/main/native/gocryptfs_jni.c -) +if (GOCRYPTFS) + add_library(gocryptfs SHARED IMPORTED) + set_target_properties( + gocryptfs + PROPERTIES IMPORTED_LOCATION + ${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI}/libgocryptfs.so + ) + add_library(gocryptfs_jni SHARED src/main/native/gocryptfs_jni.c) + target_include_directories(gocryptfs_jni PRIVATE ${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI}) + target_link_libraries(gocryptfs_jni gocryptfs) +endif() -target_link_libraries( - gocryptfs_jni - gocryptfs -) +if (CRYFS) + add_subdirectory(${PROJECT_SOURCE_DIR}/libcryfs) + add_library(cryfs_jni SHARED src/main/native/libcryfs.c) + target_link_libraries(cryfs_jni libcryfs-jni) +endif() add_library( avformat @@ -65,14 +65,11 @@ add_library( src/main/native/libmux.c ) +target_include_directories(mux PRIVATE ${PROJECT_SOURCE_DIR}/ffmpeg/build/${ANDROID_ABI}) + target_link_libraries( mux avformat avcodec avutil -) - -include_directories( - ${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI} - ${PROJECT_SOURCE_DIR}/ffmpeg/build/${ANDROID_ABI} ) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7d098aa..084badc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,18 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +if (hasProperty("disableCryFS")) { + ext.disableCryFS = getProperty("disableCryFS") +} else { + ext.disableCryFS = false +} + +if (hasProperty("disableGocryptfs")) { + ext.disableGocryptfs = getProperty("disableGocryptfs") +} else { + ext.disableGocryptfs = false +} + android { compileSdkVersion 31 buildToolsVersion "31" @@ -15,12 +27,21 @@ android { minSdkVersion 21 //noinspection ExpiredTargetSdkVersion targetSdkVersion 29 - versionCode 27 - versionName "1.10.1" + versionCode 28 + versionName "2.0.0-alpha1" ndk { abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a" } + + externalNativeBuild.cmake { + if (project.ext.disableGocryptfs) { + arguments "-DGOCRYPTFS=OFF" + } + if (project.ext.disableCryFS) { + arguments "-DCRYFS=OFF" + } + } } if (!file("fdroid").exists()) { @@ -34,6 +55,8 @@ android { applicationVariants.all { variant -> variant.resValue "string", "versionName", variant.versionName + buildConfigField "boolean", "CRYFS_DISABLED", "${project.ext.disableCryFS}" + buildConfigField "boolean", "GOCRYPTFS_DISABLED", "${project.ext.disableGocryptfs}" } buildFeatures { @@ -42,9 +65,13 @@ android { buildTypes { release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + postprocessing { + removeUnusedCode true + removeUnusedResources true + obfuscate false + optimizeCode true + proguardFiles 'proguard-rules.pro' + } } } diff --git a/app/libcryfs b/app/libcryfs new file mode 160000 index 0000000..356cf8a --- /dev/null +++ b/app/libcryfs @@ -0,0 +1 @@ +Subproject commit 356cf8a1604776cb2cc4f4ff873936f7b396bd49 diff --git a/app/libgocryptfs b/app/libgocryptfs index 9e98192..e6e4c20 160000 --- a/app/libgocryptfs +++ b/app/libgocryptfs @@ -1 +1 @@ -Subproject commit 9e98192442b08362660b45f4e2e50221ba7bc65b +Subproject commit e6e4c201dbf3834de1a49a8b67b4b54239d24249 diff --git a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt index 3a77d4e..5352fe7 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt @@ -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 - 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) { @@ -378,11 +379,12 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { private fun getOutputPath(isVideo: Boolean): String { val baseName = if (isVideo) {"VID"} else {"IMG"}+'_'+dateFormat.format(Date())+'_' - var fileName: String + var outputPath: String do { - fileName = baseName+(random.nextInt(fileNameRandomMax-fileNameRandomMin)+fileNameRandomMin)+'.'+ if (isVideo) {"mp4"} else {"jpg"} - } while (gocryptfsVolume.pathExists(fileName)) - return PathUtils.pathJoin(outputDirectory, fileName) + val fileName = baseName+(random.nextInt(fileNameRandomMax-fileNameRandomMin)+fileNameRandomMin)+'.'+ if (isVideo) {"mp4"} else {"jpg"} + outputPath = PathUtils.pathJoin(outputDirectory, fileName) + } while (encryptedVolume.pathExists(outputPath)) + return outputPath } private fun startTimerThen(action: () -> Unit) { @@ -415,7 +417,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) @@ -447,17 +449,17 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { } else if (!isWaitingForTimer) { val path = getOutputPath(true) startTimerThen { - val handleId = gocryptfsVolume.openWriteMode(path) + val fileHandle = encryptedVolume.openFile(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.write(fileHandle, offset, byteArray, byteArray.size) } override fun seek(offset: Long) { this.offset = offset } override fun close() { - gocryptfsVolume.closeFile(handleId) + encryptedVolume.closeFile(fileHandle) } }), executor, object : VideoCapture.OnVideoSavedCallback { override fun onVideoSaved() { @@ -479,7 +481,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { override fun onDestroy() { super.onDestroy() if (!isFinishingIntentionally) { - gocryptfsVolume.close() + encryptedVolume.close() RestrictedFileProvider.wipeAll(this) } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt index 5837e33..498090b 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt @@ -9,13 +9,14 @@ import android.view.View import android.widget.Toast import androidx.lifecycle.lifecycleScope import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding +import sushi.hardcore.droidfs.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder 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 diff --git a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt index fb15ceb..ece664b 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt @@ -5,13 +5,13 @@ 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 CRYFS_LOCAL_STATE_DIR = "cryfsLocalState" const val SORT_ORDER_KEY = "sort_order" val FAKE_URI: Uri = Uri.parse("fakeuri://droidfs") const val MAX_KERNEL_WRITE = 128*1024 const val WIPE_PASSES = 2 + const val IO_BUFF_SIZE = 16384 const val SLIDESHOW_DELAY: Long = 4000 const val DEFAULT_THEME_VALUE = "dark_green" const val THUMBNAIL_MAX_SIZE_KEY = "thumbnail_max_size" diff --git a/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt b/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt index 1665471..05bb457 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt @@ -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)) diff --git a/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt deleted file mode 100644 index e85555e..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt +++ /dev/null @@ -1,243 +0,0 @@ -package sushi.hardcore.droidfs - -import android.content.Context -import android.net.Uri -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 - -class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) { - 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 - private external fun native_open_read_mode(sessionID: Int, file_path: String): 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_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_rename(sessionID: Int, old_path: String, new_path: String): Boolean - - companion object { - const val KeyLen = 32 - const val ScryptDefaultLogN = 16 - 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 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 - } - return false - } - - init { - System.loadLibrary("gocryptfs_jni") - } - } - - fun close() { - native_close(sessionID) - } - - fun isClosed(): Boolean { - return native_is_closed(sessionID) - } - - fun listDir(dir_path: String): MutableList { - return native_list_dir(sessionID, dir_path) - } - - fun mkdir(dir_path: String): Boolean { - return native_mkdir(sessionID, dir_path, ConstValues.DIRECTORY_MODE) - } - - 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() - 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 - } - - 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 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 { - val result = mutableListOf() - 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 { - 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt index 770228a..05f91ca 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt @@ -30,7 +30,10 @@ 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.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.util.PathUtils +import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.EditTextDialog import java.io.File @@ -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, i: Int = 0, doDeleteVolumeContent: Boolean? = null) { + private fun removeVolumes(volumes: List, 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) } } } @@ -339,7 +342,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { val onlyOneAndWriteable = onlyOneSelected && volumeAdapter.volumes[volumeAdapter.selectedItems.first()].canWrite(filesDir.path) - menu.findItem(R.id.change_password).isVisible = onlyOneAndWriteable + menu.findItem(R.id.change_password).isVisible = + onlyOneAndWriteable && + // Only gocryptfs volumes support password change + !BuildConfig.GOCRYPTFS_DISABLED && + volumeAdapter.volumes[volumeAdapter.selectedItems.first()].type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE menu.findItem(R.id.remove_default_open).isVisible = onlyOneSelected && volumeAdapter.volumes[volumeAdapter.selectedItems.first()].name == defaultVolumeName @@ -377,9 +384,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 +396,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,13 +423,13 @@ 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 val newDBName: String val success = if (volume.isHidden) { - if (newName.contains("/")) { + if (newName.contains(PathUtils.SEPARATOR)) { Toast.makeText(this, R.string.error_slash_in_name, Toast.LENGTH_SHORT).show() renameVolume(volume, position) return@EditTextDialog @@ -452,7 +460,14 @@ 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) { + if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) { + Toast.makeText(this, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show() + return + } else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) { + Toast.makeText(this, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show() + return + } var askForPassword = true fingerprintProtector?.let { fingerprintProtector -> volume.encryptedHash?.let { encryptedHash -> @@ -463,21 +478,21 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { volumeAdapter.refresh() } override fun onPasswordHashDecrypted(hash: ByteArray) { - object : LoadingTask(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(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 +511,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) { @@ -509,20 +524,18 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { apply() } } - val password = CharArray(dialogBinding.editPassword.text.length) - dialogBinding.editPassword.text.getChars(0, password.size, password, 0) // openVolumeWithPassword is responsible for wiping the password openVolumeWithPassword( volume, position, - password, + WidgetUtil.encodeEditTextContent(dialogBinding.editPassword), 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) { + if (!usfFingerprint || fingerprintProtector == null || volume.encryptedHash != null || volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE) { dialogBinding.checkboxSavePassword.visibility = View.GONE } else { dialogBinding.checkboxSavePassword.isChecked = savePasswordHash @@ -550,20 +563,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(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(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 +596,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 +609,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 +623,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("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) @@ -640,11 +653,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { if (pickMode && !usfKeepOpen) { finish() if (shouldCloseVolume) { - val sessionID = intent.getIntExtra("sessionID", -1) - if (sessionID != -1) { - GocryptfsVolume(this, sessionID).close() - RestrictedFileProvider.wipeAll(this) - } + intent.getParcelableExtra("volume")?.close() + RestrictedFileProvider.wipeAll(this) } } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/Volume.kt b/app/src/main/java/sushi/hardcore/droidfs/SavedVolume.kt similarity index 56% rename from app/src/main/java/sushi/hardcore/droidfs/Volume.kt rename to app/src/main/java/sushi/hardcore/droidfs/SavedVolume.kt index a6739c8..a0c35d4 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/Volume.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/SavedVolume.kt @@ -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 { - override fun createFromParcel(parcel: Parcel): Volume { - return Volume(parcel) + companion object { + const val VOLUMES_DIRECTORY = "volumes" + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = SavedVolume(parcel) + override fun newArray(size: Int) = arrayOfNulls(size) } - override fun newArray(size: Int): Array { - return arrayOfNulls(size) + fun getHiddenVolumeFullPath(filesDir: String, name: String): String { + return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name) } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt index 14bf791..1c57388 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt @@ -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,24 @@ 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 { - val list: MutableList = ArrayList() + fun getVolumes(): List { + val list: MutableList = ArrayList() val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null) while (cursor.moveToNext()){ list.add( - Volume( - cursor.getString(cursor.getColumnIndex(COLUMN_NAME)), - cursor.getShort(cursor.getColumnIndex(COLUMN_HIDDEN)) == 1.toShort(), - cursor.getBlob(cursor.getColumnIndex(COLUMN_HASH)), - cursor.getBlob(cursor.getColumnIndex(COLUMN_IV)) + SavedVolume( + cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)), + cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(), + cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0], + cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)), + cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_IV)) ) ) } @@ -70,7 +122,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 +130,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 ) diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt index d36f82c..6a3505b 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt @@ -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(listener::onSelectionChanged) { @@ -42,7 +43,7 @@ class ExplorerElementAdapter( private var thumbnailsCache: LruCache? = 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 } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt index 1726e39..1d4ba41 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt @@ -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(listener::onSelectionChanged) { +) : SelectableAdapter(listener::onSelectionChanged) { private val inflater: LayoutInflater = LayoutInflater.from(context) - lateinit var volumes: List + lateinit var volumes: List 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 { + override fun getItems(): List { return volumes } diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt index c0cfd1e..4c53870 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt @@ -15,9 +15,14 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import sushi.hardcore.droidfs.* import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding +import sushi.hardcore.droidfs.filesystems.CryfsVolume +import sushi.hardcore.droidfs.filesystems.EncryptedVolume +import sushi.hardcore.droidfs.filesystems.GocryptfsVolume +import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import java.io.File import java.util.* +import kotlin.collections.ArrayList class CreateVolumeFragment: Fragment() { companion object { @@ -48,6 +53,7 @@ class CreateVolumeFragment: Fragment() { private lateinit var binding: FragmentCreateVolumeBinding private var themeValue = ConstValues.DEFAULT_THEME_VALUE + private val volumeTypes = ArrayList(2) private lateinit var volumePath: String private var isHiddenVolume: Boolean = false private var usfFingerprint: Boolean = false @@ -79,6 +85,45 @@ class CreateVolumeFragment: Fragment() { if (!usfFingerprint || fingerprintProtector == null) { binding.checkboxSavePassword.visibility = View.GONE } + if (!BuildConfig.GOCRYPTFS_DISABLED) { + volumeTypes.add(resources.getString(R.string.gocryptfs)) + } + if (!BuildConfig.CRYFS_DISABLED) { + volumeTypes.add(resources.getString(R.string.cryfs)) + } + binding.spinnerVolumeType.adapter = ArrayAdapter( + requireContext(), + android.R.layout.simple_spinner_item, + volumeTypes + ).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + val encryptionCipherAdapter = ArrayAdapter( + requireContext(), + android.R.layout.simple_spinner_item, + resources.getStringArray(R.array.gocryptfs_encryption_ciphers).toMutableList() + ).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + binding.spinnerVolumeType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val ciphersArray = if (volumeTypes[position] == resources.getString(R.string.gocryptfs)) { + if (usfFingerprint && fingerprintProtector != null) { + binding.checkboxSavePassword.visibility = View.VISIBLE + } + R.array.gocryptfs_encryption_ciphers + } else { + binding.checkboxSavePassword.visibility = View.GONE + R.array.cryfs_encryption_ciphers + } + with(encryptionCipherAdapter) { + clear() + addAll(resources.getStringArray(ciphersArray).asList()) + } + } + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + binding.spinnerCipher.adapter = encryptionCipherAdapter if (pinPasswords) { arrayOf(binding.editPassword, binding.editPasswordConfirm).forEach { it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD @@ -88,24 +133,6 @@ class CreateVolumeFragment: Fragment() { createVolume() true } - binding.spinnerXchacha.adapter = ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, - resources.getStringArray(R.array.encryption_cipher) - ).apply { - setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - } - binding.spinnerXchacha.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - if (position == 1) - CustomAlertDialogBuilder(requireContext(), themeValue) - .setTitle(R.string.warning) - .setMessage(R.string.xchacha_warning) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun onNothingSelected(parent: AdapterView<*>?) {} - } binding.buttonCreate.setOnClickListener { createVolume() } @@ -116,31 +143,45 @@ class CreateVolumeFragment: Fragment() { (activity as AddVolumeActivity).onFragmentLoaded(false) } + private fun saveVolume(success: Boolean, volumeType: Byte): SavedVolume? { + return if (success) { + val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath + val volume = SavedVolume(volumeName, isHiddenVolume, volumeType) + volumeDatabase.apply { + if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path + removeVolume(volumeName) + saveVolume(volume) + } + volume + } else { + null + } + } + private fun createVolume() { - val password = CharArray(binding.editPassword.text.length) - binding.editPassword.text.getChars(0, password.size, password, 0) - val passwordConfirm = CharArray(binding.editPasswordConfirm.text.length) - binding.editPasswordConfirm.text.getChars(0, passwordConfirm.size, passwordConfirm, 0) + val password = WidgetUtil.encodeEditTextContent(binding.editPassword) + val passwordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm) if (!password.contentEquals(passwordConfirm)) { Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show() - Arrays.fill(password, 0.toChar()) - Arrays.fill(passwordConfirm, 0.toChar()) + Arrays.fill(password, 0) + Arrays.fill(passwordConfirm, 0) } else { - Arrays.fill(passwordConfirm, 0.toChar()) + Arrays.fill(passwordConfirm, 0) var returnedHash: ByteArray? = null if (binding.checkboxSavePassword.isChecked) returnedHash = ByteArray(GocryptfsVolume.KeyLen) - object: LoadingTask(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) { - override suspend fun doTask(): Volume? { - val xchacha = when (binding.spinnerXchacha.selectedItemPosition) { - 0 -> 0 - 1 -> 1 - else -> -1 - } + object: LoadingTask(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) { + override suspend fun doTask(): SavedVolume? { val volumeFile = File(volumePath) if (!volumeFile.exists()) volumeFile.mkdirs() - val volume = if (GocryptfsVolume.createVolume( + val volume = if (volumeTypes[binding.spinnerVolumeType.selectedItemPosition] == resources.getString(R.string.gocryptfs)) { + val xchacha = when (binding.spinnerCipher.selectedItemPosition) { + 0 -> 0 + 1 -> 1 + else -> -1 + } + saveVolume(GocryptfsVolume.createVolume( volumePath, password, false, @@ -148,20 +189,16 @@ class CreateVolumeFragment: Fragment() { GocryptfsVolume.ScryptDefaultLogN, ConstValues.CREATOR, returnedHash - ) - ) { - val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath - val volume = Volume(volumeName, isHiddenVolume) - volumeDatabase.apply { - if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path - removeVolume(volumeName) - saveVolume(volume) - } - volume + ), EncryptedVolume.GOCRYPTFS_VOLUME_TYPE) } else { - null + saveVolume(CryfsVolume.create( + volumePath, + CryfsVolume.getLocalStateDir(activity.filesDir.path), + password, + resources.getStringArray(R.array.cryfs_encryption_ciphers)[binding.spinnerCipher.selectedItemPosition] + ), EncryptedVolume.CRYFS_VOLUME_TYPE) } - Arrays.fill(password, 0.toChar()) + Arrays.fill(password, 0) return volume } }.startTask(lifecycleScope) { volume -> diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt index 4bde96d..f27ef24 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt @@ -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() } @@ -190,7 +190,7 @@ class SelectPathFragment: Fragment() { if (isHidden) R.string.enter_volume_name else R.string.enter_volume_path, Toast.LENGTH_SHORT ).show() - } else if (isHidden && currentVolumeValue.contains("/")) { + } else if (isHidden && currentVolumeValue.contains(PathUtils.SEPARATOR)) { Toast.makeText(requireContext(), R.string.error_slash_in_name, Toast.LENGTH_SHORT).show() } else { val volumePath = getCurrentVolumePath() @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt index cee4e32..5717abe 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt @@ -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 { + private fun exportFile(context: Context, encryptedVolume: EncryptedVolume, file_path: String, previous_content_type: String?): Pair { 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) { + fun share(activity: AppCompatActivity, themeValue: String, encryptedVolume: EncryptedVolume, file_paths: List) { var contentType: String? = null val uris = ArrayList(file_paths.size) object : LoadingTask(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(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) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt index eba643e..ad5e057 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -31,7 +31,6 @@ import sushi.hardcore.droidfs.ConstValues.isImage import sushi.hardcore.droidfs.ConstValues.isPDF import sushi.hardcore.droidfs.ConstValues.isText import sushi.hardcore.droidfs.ConstValues.isVideo -import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter @@ -40,6 +39,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 +51,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 +83,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 +107,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene explorerAdapter = ExplorerElementAdapter( this, if (sharedPrefs.getBoolean("thumbnails", true)) { - gocryptfsVolume + encryptedVolume } else { null }, @@ -139,7 +139,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } class ExplorerViewModel: ViewModel() { - var currentDirectoryPath = "" + var currentDirectoryPath = "/" } private fun setRecyclerViewLayout() { @@ -166,7 +166,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 +178,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 +187,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 +276,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 +301,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 +323,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene if (element.isDirectory) { recursiveSetSize(element) } - totalSize += element.size + totalSize += element.stat.size } } } @@ -331,7 +331,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 +362,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 +382,33 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } protected fun checkPathOverwrite(items: ArrayList, dstDirectoryPath: String, callback: (ArrayList?) -> 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 +416,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 +458,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(currentDirectoryPath, fileName), Stat.S_IFREG)) } } if (items.size > 0) { @@ -475,7 +481,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 +593,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 +622,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene if (isCreating){ isCreating = false } else { - if (gocryptfsVolume.isClosed()){ + if (encryptedVolume.isClosed()) { finish() } else { isStartingActivity = false diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt index 465f299..4175798 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -11,17 +11,17 @@ import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import sushi.hardcore.droidfs.CameraActivity -import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.MainActivity import sushi.hardcore.droidfs.R 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 -import java.io.File class ExplorerActivity : BaseExplorerActivity() { companion object { @@ -36,8 +36,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("volume")!! val path = resultIntent.getStringExtra("path") val operationFiles = ArrayList() if (path == null){ //multiples elements @@ -46,12 +45,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 +56,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 +76,12 @@ class ExplorerActivity : BaseExplorerActivity() { .show() } setCurrentPath(currentDirectoryPath) - remoteGocryptfsVolume.close() + remoteEncryptedVolume.close() } } } } else { - remoteGocryptfsVolume.close() + remoteEncryptedVolume.close() } } } @@ -120,7 +115,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(currentDirectoryPath, tree.name!!), Stat.S_IFDIR) checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation -> checkedOperation?.let { lifecycleScope.launch { @@ -192,7 +187,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 +210,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 +236,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 +307,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 +341,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(itemsToProcess.size) - val toClean = ArrayList() + val toClean = ArrayList() prepareFilesForMove( itemsToProcess, toMove, @@ -398,7 +393,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 +413,12 @@ class ExplorerActivity : BaseExplorerActivity() { */ private fun checkMoveOverwrite(items: List, callback: (List?) -> 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 +435,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 +458,24 @@ class ExplorerActivity : BaseExplorerActivity() { private fun prepareFilesForMove( items: List, toMove: ArrayList, - toClean: ArrayList, + toClean: ArrayList, 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() 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 +509,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 } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt index 3f332bd..7327db8 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt @@ -5,16 +5,15 @@ import android.content.Intent 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.* class ExplorerActivityPick : BaseExplorerActivity() { private var resultIntent = Intent() private var isFinishingIntentionally = false override fun init() { super.init() - resultIntent.putExtra("sessionID", gocryptfsVolume.sessionID) + resultIntent.putExtra("volume", encryptedVolume) } override fun bindFileOperationService() { @@ -65,7 +64,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 +83,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("destinationVolume")?.let { it.close() } super.closeVolumeOnDestroy() } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt index 8b1fc91..e128d31 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt @@ -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) } } } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt index e8a72a8..22201ed 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt @@ -14,9 +14,10 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.* -import sushi.hardcore.droidfs.GocryptfsVolume +import sushi.hardcore.droidfs.ConstValues 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() 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) + val ioBuffer = ByteArray(ConstValues.IO_BUFF_SIZE) 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, - 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, toClean: List): String? = coroutineScope { + suspend fun moveElements(toMove: List, toClean: List): 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 diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt index cec8481..76f5e57 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/GocryptfsDataSource.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/EncryptedVolumeDataSource.kt similarity index 73% rename from app/src/main/java/sushi/hardcore/droidfs/file_viewers/GocryptfsDataSource.kt rename to app/src/main/java/sushi/hardcore/droidfs/file_viewers/EncryptedVolumeDataSource.kt index 507fe67..0827f2a 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/GocryptfsDataSource.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/EncryptedVolumeDataSource.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt index e5dedda..96c798f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt @@ -6,15 +6,15 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.ConstValues -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 +33,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 +66,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 +84,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 +134,7 @@ abstract class FileViewerActivity: BaseActivity() { override fun onDestroy() { super.onDestroy() if (!isFinishingIntentionally) { - gocryptfsVolume.close() + encryptedVolume.close() RestrictedFileProvider.wipeAll(this) } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt index c8c1e8f..b71c18c 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt @@ -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 { diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt index 7dc4a1f..352929c 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt @@ -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)) } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt index 9da796a..8dd4b62 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt @@ -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) } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt index 8085923..a875ba0 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt @@ -7,7 +7,7 @@ import android.view.Menu import android.view.MenuItem import android.widget.EditText import android.widget.Toast -import sushi.hardcore.droidfs.GocryptfsVolume +import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import java.io.ByteArrayInputStream @@ -68,14 +68,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) + val ioBuffer = ByteArray(ConstValues.IO_BUFF_SIZE) 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 +83,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){ diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt new file mode 100644 index 0000000..3b44686 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt @@ -0,0 +1,123 @@ +package sushi.hardcore.droidfs.filesystems + +import android.os.Parcel +import sushi.hardcore.droidfs.ConstValues +import sushi.hardcore.droidfs.explorers.ExplorerElement +import sushi.hardcore.droidfs.util.PathUtils + +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, + createBaseDir: Boolean, + cipher: String? + ): 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? + 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 getLocalStateDir(filesDir: String): String { + return PathUtils.pathJoin(filesDir, ConstValues.CRYFS_LOCAL_STATE_DIR) + } + + private fun init(baseDir: String, localStateDir: String, password: ByteArray, createBaseDir: Boolean, cipher: String?): CryfsVolume? { + val fusePtr = nativeInit(baseDir, localStateDir, password, createBaseDir, cipher) + return if (fusePtr == 0L) { + null + } else { + CryfsVolume(fusePtr) + } + } + + fun create(baseDir: String, localStateDir: String, password: ByteArray, cipher: String?): Boolean { + return init(baseDir, localStateDir, password, true, cipher)?.also { it.close() } != null + } + + fun init(baseDir: String, localStateDir: String, password: ByteArray): CryfsVolume? { + return init(baseDir, localStateDir, password, false, null) + } + } + + 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? { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt new file mode 100644 index 0000000..9895fef --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt @@ -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.ConstValues +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 { + 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(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), CryfsVolume.getLocalStateDir(filesDir), 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? + 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(ConstValues.IO_BUFF_SIZE) + 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(ConstValues.IO_BUFF_SIZE) + 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 { + 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(ConstValues.IO_BUFF_SIZE) + 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? { + val result = mutableListOf() + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt new file mode 100644 index 0000000..22d60a8 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt @@ -0,0 +1,103 @@ +package sushi.hardcore.droidfs.filesystems + +import android.os.Parcel +import sushi.hardcore.droidfs.explorers.ExplorerElement + +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? + private external fun native_open_read_mode(sessionID: Int, file_path: String): 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, 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 { + const val KeyLen = 32 + const val ScryptDefaultLogN = 16 + const val CONFIG_FILE_NAME = "gocryptfs.conf" + external fun createVolume(root_cipher_dir: String, password: ByteArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String, returnedHash: ByteArray?): Boolean + private 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 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) + } + } + + init { + System.loadLibrary("gocryptfs_jni") + } + } + + 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? { + 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) + } + + override fun isClosed(): Boolean { + return native_is_closed(sessionID) + } + + override fun mkdir(path: String): Boolean { + return native_mkdir(sessionID, path, 0) + } + + override fun rmdir(path: String): Boolean { + return native_rmdir(sessionID, path) + } + + override fun closeFile(fileHandle: Long): Boolean { + native_close_file(sessionID, fileHandle.toInt()) + return true + } + + override fun write(fileHandle: Long, offset: Long, buffer: ByteArray, size: Int): Int { + return native_write_file(sessionID, fileHandle.toInt(), offset, buffer, size) + } + + override fun truncate(path: String, size: Long): Boolean { + return native_truncate(sessionID, path, size) + } + + override fun deleteFile(path: String): Boolean { + return native_remove_file(sessionID, path) + } + + override fun rename(srcPath: String, dstPath: String): Boolean { + return native_rename(sessionID, srcPath, dstPath) + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt new file mode 100644 index 0000000..e08545f --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt index 89646b8..b3d82a0 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt @@ -13,23 +13,22 @@ import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import java.io.File import java.text.DecimalFormat import kotlin.math.log10 +import kotlin.math.max import kotlin.math.pow object PathUtils { + const val SEPARATOR = '/' + 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("/")) - } else { - "" - } + val strippedPath = if (path.endsWith(SEPARATOR)) { + path.substring(0, max(1, path.length - 1)) } else { - if (path.contains("/")) { - path.substring(0, path.lastIndexOf("/")) - } else { - "" - } + path + } + return if (strippedPath.count { it == SEPARATOR } <= 1) { + SEPARATOR.toString() + } else { + strippedPath.substring(0, strippedPath.lastIndexOf(SEPARATOR)) } } @@ -37,27 +36,21 @@ object PathUtils { val result = StringBuilder() for (element in strings) { if (element.isNotEmpty()) { - result.append(element) - if (!element.endsWith("/")) { - result.append("/") + if (!element.startsWith(SEPARATOR) && result.last() != SEPARATOR) { + result.append(SEPARATOR) } + result.append(element) } } - return result.substring(0, result.length - 1) + return result.toString() } fun getRelativePath(parentPath: String, childPath: String): String { - return when { - parentPath.isEmpty() -> { - childPath - } - parentPath.length == childPath.length -> { - "" - } - else -> { - childPath.substring(parentPath.length + 1) - } - } + return childPath.substring(parentPath.length + if (parentPath.endsWith(SEPARATOR) || childPath.length == parentPath.length) { + 0 + } else { + 1 + }) } fun isChildOf(childPath: String, parentPath: String): Boolean { @@ -79,7 +72,7 @@ object PathUtils { if (result == null) { result = uri.path result?.let { - val cut = it.lastIndexOf('/') + val cut = it.lastIndexOf(SEPARATOR) if (cut != -1) { result = it.substring(cut + 1) } diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt b/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt new file mode 100644 index 0000000..910ff58 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt @@ -0,0 +1,18 @@ +package sushi.hardcore.droidfs.util + +import android.widget.EditText +import java.nio.CharBuffer +import java.nio.charset.StandardCharsets +import java.util.* + +object WidgetUtil { + fun encodeEditTextContent(editText: EditText): ByteArray { + val charArray = CharArray(editText.text.length) + editText.text.getChars(0, editText.text.length, charArray, 0) + val byteArray = StandardCharsets.UTF_8.encode( + CharBuffer.wrap(charArray) + ).array() + Arrays.fill(charArray, Char.MIN_VALUE) + return byteArray + } +} \ No newline at end of file diff --git a/app/src/main/native/gocryptfs_jni.c b/app/src/main/native/gocryptfs_jni.c index a1e1244..e5dfd3e 100644 --- a/app/src/main/native/gocryptfs_jni.c +++ b/app/src/main/native/gocryptfs_jni.c @@ -30,8 +30,9 @@ void jbyteArray_to_unsignedCharArray(const jbyte* src, unsigned char* dst, const } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_createVolume(JNIEnv *env, jclass clazz, - jstring jroot_cipher_dir, jcharArray jpassword, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_createVolume(JNIEnv *env, jclass clazz, + jstring jroot_cipher_dir, + jbyteArray jpassword, jboolean plainTextNames, jint xchacha, jint logN, @@ -42,9 +43,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_createVolume(JNIEnv * GoString gofilename = {root_cipher_dir, strlen(root_cipher_dir)}, gocreator = {creator, strlen(creator)}; const size_t password_len = (*env)->GetArrayLength(env, jpassword); - jchar* jchar_password = (*env)->GetCharArrayElements(env, jpassword, NULL); - char password[password_len]; - jcharArray_to_charArray(jchar_password, password, password_len); + char* password = (char*)(*env)->GetByteArrayElements(env, jpassword, NULL); GoSlice go_password = {password, password_len, password_len}; size_t returned_hash_len; @@ -65,7 +64,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_createVolume(JNIEnv * (*env)->ReleaseStringUTFChars(env, jroot_cipher_dir, root_cipher_dir); (*env)->ReleaseStringUTFChars(env, jcreator, creator); wipe(password, password_len); - (*env)->ReleaseCharArrayElements(env, jpassword, jchar_password, 0); + (*env)->ReleaseByteArrayElements(env, jpassword, (jbyte*)password, 0); if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) { unsignedCharArray_to_jbyteArray(returned_hash, jbyte_returned_hash, returned_hash_len); @@ -78,16 +77,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_filesystems_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 +94,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 +127,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); @@ -150,13 +145,13 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_init(JNIEnv *env, job } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1is_1closed(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1is_1closed(JNIEnv *env, jobject thiz, jint sessionID) { return gcf_is_closed(sessionID); } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_changePassword(JNIEnv *env, jclass clazz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_changePassword(JNIEnv *env, jclass clazz, jstring jroot_cipher_dir, jcharArray jold_password, jbyteArray jgiven_hash, @@ -225,6 +220,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_changePassword(JNIEnv } wipe(new_password, new_password_len); + free(new_password); (*env)->ReleaseCharArrayElements(env, jnew_password, jchar_new_password, 0); if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) { @@ -238,12 +234,12 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_changePassword(JNIEnv } JNIEXPORT void JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1close(JNIEnv *env, jobject thiz, jint sessionID) { +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1close(JNIEnv *env, jobject thiz, jint sessionID) { gcf_close(sessionID); } JNIEXPORT jobject JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1list_1dir(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1list_1dir(JNIEnv *env, jobject thiz, jint sessionID, jstring jplain_dir) { const char* plain_dir = (*env)->GetStringUTFChars(env, jplain_dir, NULL); const size_t plain_dir_len = strlen(plain_dir); @@ -251,63 +247,59 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1list_1dir(JNIEnv *env, jobje struct gcf_list_dir_return elements = gcf_list_dir(sessionID, go_plain_dir); - jclass java_ArrayList = (*env)->NewLocalRef(env, (*env)->FindClass(env, "java/util/ArrayList")); - jmethodID java_ArrayList_init = (*env)->GetMethodID(env, java_ArrayList, "", "(I)V"); - jmethodID java_ArrayList_add = (*env)->GetMethodID(env, java_ArrayList, "add", "(Ljava/lang/Object;)Z"); + jobject elementList = NULL; + if (elements.r0 != NULL) { + jclass arrayList = (*env)->NewLocalRef(env, (*env)->FindClass(env, "java/util/ArrayList")); + jmethodID arrayListInit = (*env)->GetMethodID(env, arrayList, "", "(I)V"); + jmethodID arrayListAdd = (*env)->GetMethodID(env, arrayList, "add", "(Ljava/lang/Object;)Z"); + jclass classExplorerElement = (*env)->NewLocalRef(env, (*env)->FindClass(env, "sushi/hardcore/droidfs/explorers/ExplorerElement")); + jmethodID explorerElementNew = (*env)->GetStaticMethodID(env, classExplorerElement, "new", "(Ljava/lang/String;IJJLjava/lang/String;)Lsushi/hardcore/droidfs/explorers/ExplorerElement;"); + elementList = (*env)->NewObject(env, arrayList, arrayListInit, elements.r2); - 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;"); - jobject element_list = (*env)->NewObject(env, java_ArrayList, java_ArrayList_init, elements.r2); - unsigned int c = 0; - for (unsigned int i=0; i 0){ - strcpy(gcf_full_path, plain_dir); + char* fullPath = malloc(sizeof(char) * (plain_dir_len + nameLen + 2)); + strcpy(fullPath, plain_dir); if (plain_dir[-2] != '/') { - strcat(gcf_full_path, "/"); + strcat(fullPath, "/"); } - strcat(gcf_full_path, name); - } else { - strcpy(gcf_full_path, name); + strcat(fullPath, name); + + GoString go_name = {fullPath, strlen(fullPath)}; + struct gcf_get_attrs_return attrs = gcf_get_attrs(sessionID, go_name); + free(fullPath); + + jstring jname = (*env)->NewStringUTF(env, name); + jobject explorerElement = (*env)->CallStaticObjectMethod( + env, + classExplorerElement, + explorerElementNew, + jname, + elements.r1[i], + (long long) attrs.r1, + (long long) attrs.r2, + jplain_dir + ); + (*env)->CallBooleanMethod(env, elementList, arrayListAdd, explorerElement); + (*env)->DeleteLocalRef(env, explorerElement); + (*env)->DeleteLocalRef(env, jname); + c += nameLen + 1; } - 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, - jplain_dir - ); - (*env)->CallBooleanMethod(env, element_list, java_ArrayList_add, explorerElement); - (*env)->DeleteLocalRef(env, explorerElement); - (*env)->DeleteLocalRef(env, jname); - c += name_len+1; + free(elements.r0); + free(elements.r1); } - free(elements.r0); - free(elements.r1); - (*env)->ReleaseStringUTFChars(env, jplain_dir, plain_dir); - return element_list; + return elementList; } -JNIEXPORT jlong JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1get_1size(JNIEnv *env, jobject thiz, +JNIEXPORT jobject JNICALL +Java_sushi_hardcore_droidfs_filesystems_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,25 +308,22 @@ 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, "", "(IJJ)V"); + return (*env)->NewObject( + env, stat, statInit, + (jint)attrs.r0, + (jlong)attrs.r1, + (jlong)attrs.r2 + ); + } else { + return NULL; + } } JNIEXPORT jint JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1open_1read_1mode(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1open_1read_1mode(JNIEnv *env, jobject thiz, jint sessionID, jstring jfile_path) { const char* file_path = (*env)->GetStringUTFChars(env, jfile_path, NULL); @@ -348,7 +337,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1open_1read_1mode(JNIEnv *env } JNIEXPORT jint JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1open_1write_1mode(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1open_1write_1mode(JNIEnv *env, jobject thiz, jint sessionID, jstring jfile_path, jint mode) { @@ -363,7 +352,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1open_1write_1mode(JNIEnv *en } JNIEXPORT jint JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1write_1file(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1write_1file(JNIEnv *env, jobject thiz, jint sessionID, jint handleID, jlong offset, jbyteArray jbuff, jint buff_size) { jbyte* buff = (*env)->GetByteArrayElements(env, jbuff, NULL); @@ -377,38 +366,45 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1write_1file(JNIEnv *env, job } JNIEXPORT jint JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1read_1file(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1read_1file(JNIEnv *env, jobject thiz, jint sessionID, jint handleID, jlong offset, jbyteArray jbuff) { const size_t buff_size = (*env)->GetArrayLength(env, jbuff); - unsigned char buff[buff_size]; + unsigned char* buff = malloc(sizeof(char)*buff_size); GoSlice go_buff = {buff, buff_size, buff_size}; int read = gcf_read_file(sessionID, handleID, offset, go_buff); if (read > 0){ - (*env)->SetByteArrayRegion(env, jbuff, 0, read, buff); + (*env)->SetByteArrayRegion(env, jbuff, 0, read, (const jbyte*)buff); } - + free(buff); return read; } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1truncate(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_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 -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1close_1file(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1close_1file(JNIEnv *env, jobject thiz, jint sessionID, jint handleID) { gcf_close_file(sessionID, handleID); } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1remove_1file(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1remove_1file(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)}; @@ -421,7 +417,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1remove_1file(JNIEnv *env, jo } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1mkdir(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1mkdir(JNIEnv *env, jobject thiz, jint sessionID, jstring jdir_path, jint mode) { const char* dir_path = (*env)->GetStringUTFChars(env, jdir_path, NULL); GoString go_dir_path = {dir_path, strlen(dir_path)}; @@ -434,7 +430,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1mkdir(JNIEnv *env, jobject t } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1rmdir(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1rmdir(JNIEnv *env, jobject thiz, jint sessionID, jstring jdir_path) { const char* dir_path = (*env)->GetStringUTFChars(env, jdir_path, NULL); GoString go_dir_path = {dir_path, strlen(dir_path)}; @@ -447,7 +443,7 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1rmdir(JNIEnv *env, jobject t } JNIEXPORT jboolean JNICALL -Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1rename(JNIEnv *env, jobject thiz, +Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1rename(JNIEnv *env, jobject thiz, jint sessionID, jstring jold_path, jstring jnew_path) { const char* old_path = (*env)->GetStringUTFChars(env, jold_path, NULL); @@ -461,4 +457,4 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_native_1rename(JNIEnv *env, jobject (*env)->ReleaseStringUTFChars(env, jnew_path, new_path); return result; -} +} \ No newline at end of file diff --git a/app/src/main/native/libcryfs.c b/app/src/main/native/libcryfs.c new file mode 100644 index 0000000..134f660 --- /dev/null +++ b/app/src/main/native/libcryfs.c @@ -0,0 +1,184 @@ +#include +#include +#include + +JNIEXPORT jlong JNICALL +Java_sushi_hardcore_droidfs_filesystems_CryfsVolume_00024Companion_nativeInit(JNIEnv *env, jobject thiz, + jstring base_dir, jstring jlocalStateDir, + jbyteArray password, jboolean createBaseDir, + jstring cipher) { + return cryfs_init(env, base_dir, jlocalStateDir, password, createBaseDir, cipher); +} + +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, "", "()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, "", "(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); +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_volume.xml b/app/src/main/res/layout/fragment_create_volume.xml index 62abdc4..d0c73b4 100644 --- a/app/src/main/res/layout/fragment_create_volume.xml +++ b/app/src/main/res/layout/fragment_create_volume.xml @@ -6,6 +6,18 @@ android:gravity="center_vertical" android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap"> + + + + diff --git a/app/src/main/res/values-ar/arrays.xml b/app/src/main/res/values-ar/arrays.xml index 7142577..71f169d 100644 --- a/app/src/main/res/values-ar/arrays.xml +++ b/app/src/main/res/values-ar/arrays.xml @@ -1,10 +1,4 @@  - - AES-GCM - XChaCha20-Poly1305 - @string/auto - - اسم حجم diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d5690b1..dd385af 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -39,7 +39,7 @@ فتح بتطبيق خارجي هل أنت متأكد من حذف %s ? هل أنت متأكد من حذف هذه %s العناصر ? - موقع: /%s + موقع: %s الحجم الكلي: %s استيراد من مجلد مشفر آخر لقد فشل فتح هذا الملف. @@ -138,7 +138,6 @@ تم حفظ تغييرات الصورة بنجاح. فشل ضغط الصورة النقطية. فشل كتابة الملف. - لم يتم التعرف على مجلد التشفير Gocryptfs. يرجى التحقق من المسار المحدد. إصدار تشفير الخطأ فارغ KeyPermanentlyInvalidatedException @@ -173,7 +172,6 @@ دقة أعلى أداء أفضل تلقائي - XChaCha20-Poly1305 مدعوم فقط منذ إصدار gocryptfs v2.2.0. لن تتمكن الإصدارات الأقدم من فتح وحدة تخزين بناءً على هذا التشفير. خوارزمية التشفير: سمة تخصيص سمة التطبيق diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7d11eb1..a312805 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -39,7 +39,7 @@ Abrir con una aplicación externa ¿Estás seguro de que quieres borrar %s ? ¿Estás seguro de que quiere borrar %s objetos? - Ubicación: /%s + Ubicación: %s Tamaño total: %s Importar desde otro volumen No se ha podido abrir este archivo. @@ -138,7 +138,6 @@ Cambios de la imagen guardados con éxito. Fallo al comprimir el mapa de bits. No se ha podido escribir el archivo. - No se reconoce el volumen Gocryptfs. Por favor, comprueba la ruta seleccionada. Versión Error de cifrado núlo KeyPermanentlyInvalidatedException @@ -173,7 +172,6 @@ Maximizar la calidad Minimizar la latencia Automático - XChaCha20-Poly1305 sólo está soportado desde gocryptfs v2.2.0. Las versiones anteriores no podrán abrir un volumen basado en este cifrado. Cifrado de encriptación: Tema Personalizar el tema de la aplicación diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index f6335b9..faf99cf 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -39,7 +39,7 @@ Abrir com app externo Você tem certeza que quer excluir %s? Você realmente deseja excluir estos %s itens? - Localização: /%s + Localização: %s Tamanho total: %s Importar do outro volume Falha ao abrir este arquivo. @@ -135,7 +135,6 @@ Mudanças na imagem salva com sucesso. Falha ao compactar o bitmap. Falha ao salvar o arquivo. - O volume do Gocryptfs não foi reconhecido. Por favor, verifique a localização selecionada. Versão Erro, o cipher é nulo KeyPermanentlyInvalidatedException @@ -170,7 +169,6 @@ Maximizar a qualidade Minimizar a latência Autom - XChaCha20-Poly1305 só tem suporte desde gocryptfs v2.2.0. Versões mais antigas não podem abrir um volume baseado neste cipher. Cipher de criptografia: Tema Personalizar o tema do app diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6b6355f..4849d0b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -37,7 +37,7 @@ Открыть внешним приложением Удалить %s? Удалить %s элементов? - Путь: /%s + Путь: %s Общий размер: %s Импорт из другого тома Невозможно открыть файл. @@ -133,7 +133,6 @@ Изменения изображения успешно сохранены. Невозможно сжать растровое изображение. Невозможно записать файл. - Том GocryptFS не распознан. Проверьте выбранный путь. Версия Шифр ошибки нулевой Похоже, вы добавили новый отпечаток пальца. Сохранённый хеш паролей стал непригодным для использования. @@ -167,7 +166,6 @@ Максимальное качество Минимальная задержка Авто - Шифр XChaCha20-Poly1305 поддерживается только с версии GocryptFS 2.2.0. Более старые версии не смогут открыть том, использующий данный шифр. Шифр: Тема Настройка темы приложения diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index f143993..8e75c9e 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,10 +1,24 @@ - + AES-GCM XChaCha20-Poly1305 @string/auto + + xchacha20-poly1305 + aes-256-gcm + aes-128-gcm + twofish-256-gcm + twofish-128-gcm + serpent-256-gcm + serpent-128-gcm + cast-256-gcm + mars-448-gcm + mars-256-gcm + mars-128-gcm + + Name Size diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3cd357b..58216ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,7 @@ Open with external app Are you sure you want to delete %s ? Are you sure you want to delete these %s items ? - Location: /%s + Location: %s Total size: %s Import from another volume Failed to open this file. @@ -138,7 +138,7 @@ Image changes successfully saved. Failed to compress the bitmap. Failed to write the file. - Gocryptfs volume not recognized. Please check the selected path. + Encrypted volume not recognized. Please check the selected path. Version Error cipher is null KeyPermanentlyInvalidatedException @@ -173,7 +173,6 @@ Maximize quality Minimize latency Auto - XChaCha20-Poly1305 is only supported since gocryptfs v2.2.0. Older versions won\'t be able to open a volume based on this cipher. Encryption cipher: Theme Customize app theme @@ -242,4 +241,9 @@ %d/%d selected Numeric keypad layout Use a numeric keypad layout when entering volume passwords + Volume type: + Gocryptfs + CryFS + Gocryptfs support has been disabled + CryFS support has been disabled diff --git a/build.gradle b/build.gradle index b036d8f..e119c0c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index bbe5421..876d5d9 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,15 +1,15 @@ DroidFS is an alternative way to use encrypted overlay file-systems on Android that uses its own internal file explorer instead of mounting virtual volumes. This allows you to store files in encrypted virtual volumes that other apps can't decrypt. -Currently, DroidFS supports only gocryptfs. This algorithm protects file contents and file names but doesn't hide directory structure or file sizes. If you want more details, take a look at their GitHub repository: https://github.com/rfjakob/gocryptfs +Currently, DroidFS supports the following encrypted containers: +- gocryptfs (https://github.com/rfjakob/gocryptfs): encrypts file contents and file names but doesn't hide directory structure or file sizes. +- CryFS (https://github.com/cryfs/cryfs): slower, but encrypts everything and obfuscates file sizes. Features: -- Volumes fully compatibles with their original algorithm implementation -- Internal opening of images, videos, audios and text files -- Common file and folder operations: copy, move, rename, delete -- Volume password changing +- Volumes fully compatibles with their original implementations +- Internal opening of images, videos, audios, PDF and text files +- Common file manager operations: copy, move, rename, create, delete - Internal and shared storage support - Cross-volume imports -- Internal camera to take encrypted pictures or videos -- Theme color fully customizable +- Internal camera to take encrypted pictures and videos Permissions: Read & write access to shared storage: required to create, open and modify volumes and import/export files to/from volumes. diff --git a/gradle.properties b/gradle.properties index 4d15d01..4b2efe1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,6 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official + +android.native.buildOutput=verbose \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fbca759..1e3ef59 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Sep 01 11:25:55 UTC 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME