Browse Source

CryFS

master v2.0.0-alpha1
Hardcore Sushi 2 months ago
parent
commit
cf4927a90b
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
  1. 3
      .gitmodules
  2. 108
      BUILD.md
  3. 79
      README.md
  4. 45
      app/CMakeLists.txt
  5. 37
      app/build.gradle
  6. 1
      app/libcryfs
  7. 2
      app/libgocryptfs
  8. 24
      app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt
  9. 3
      app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt
  10. 4
      app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt
  11. 4
      app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt
  12. 243
      app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt
  13. 110
      app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
  14. 20
      app/src/main/java/sushi/hardcore/droidfs/SavedVolume.kt
  15. 87
      app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt
  16. 21
      app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt
  17. 10
      app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt
  18. 123
      app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt
  19. 17
      app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt
  20. 14
      app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt
  21. 70
      app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
  22. 73
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
  23. 12
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt
  24. 34
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt
  25. 63
      app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
  26. 15
      app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt
  27. 18
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/EncryptedVolumeDataSource.kt
  28. 21
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt
  29. 4
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt
  30. 2
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt
  31. 2
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt
  32. 14
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt
  33. 123
      app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt
  34. 214
      app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
  35. 103
      app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt
  36. 14
      app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt
  37. 49
      app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt
  38. 18
      app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt
  39. 188
      app/src/main/native/gocryptfs_jni.c
  40. 184
      app/src/main/native/libcryfs.c
  41. 16
      app/src/main/res/layout/fragment_create_volume.xml
  42. 6
      app/src/main/res/values-ar/arrays.xml
  43. 4
      app/src/main/res/values-ar/strings.xml
  44. 4
      app/src/main/res/values-es/strings.xml
  45. 4
      app/src/main/res/values-pt-rBR/strings.xml
  46. 4
      app/src/main/res/values-ru/strings.xml
  47. 16
      app/src/main/res/values/arrays.xml
  48. 10
      app/src/main/res/values/strings.xml
  49. 2
      build.gradle
  50. 14
      fastlane/metadata/android/en-US/full_description.txt
  51. 4
      gradle.properties
  52. 2
      gradle/wrapper/gradle-wrapper.properties

3
.gitmodules vendored

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

108
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 <hardcore.sushi@disroot.org>`
# 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/\<user\>/Android/SDK/ndk/\<NDK version\>`. Then, make it available in your shell:
```
$ export ANDROID_NDK_HOME="<your ndk path>"
```
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 <output file> -alias <key alias> -keyalg EC -validity 10000
```
Then, sign the APK with:
```
$ apksigner sign --out droidfs.apk -v --ks <keystore> app/build/outputs/apk/release/<unsigned apk file>
```
Now you can install `droidfs.apk` on your device.

79
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).
<p align="center">
<img src="https://forge.chapril.org/hardcoresushi/DroidFS/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" height="500">
@ -46,7 +46,7 @@ It is strongly recommended to read the documentation of a feature before enablin
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">
</a>
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:
</ul>
# 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 <output file> -alias <key alias> -keyalg EC -validity 10000
```
#### Build
Retrieve your Android NDK installation path, usually something like "/home/\<user\>/Android/SDK/ndk/\<NDK version\>". Now you can build libgocryptfs:
```
$ cd DroidFS/app/libgocryptfs
$ env ANDROID_NDK_HOME="<your ndk path>" OPENSSL_PATH="./openssl-1.1.1n" ./build.sh
```
Then FFmpeg:
```
$ cd app/ffmpeg
$ env ANDROID_NDK_HOME="<your ndk path>" ./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 <keystore> app/build/outputs/apk/release/<unsigned apk file>
```
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

45
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}
)

37
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'
}
}
}

1
app/libcryfs

@ -0,0 +1 @@
Subproject commit 356cf8a1604776cb2cc4f4ff873936f7b396bd49

2
app/libgocryptfs

@ -1 +1 @@
Subproject commit 9e98192442b08362660b45f4e2e50221ba7bc65b
Subproject commit e6e4c201dbf3834de1a49a8b67b4b54239d24249

24
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<ImageView>
private lateinit var gocryptfsVolume: GocryptfsVolume
private lateinit var encryptedVolume: EncryptedVolume
private lateinit var outputDirectory: String
private var isFinishingIntentionally = false
private var isAskingPermissions = false
@ -93,7 +94,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1))
encryptedVolume = intent.getParcelableExtra("volume")!!
outputDirectory = intent.getStringExtra("path")!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -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)
}
}

3
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

4
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"

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

243
app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt

@ -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<ExplorerElement>
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<ExplorerElement> {
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<ExplorerElement> {
val result = mutableListOf<ExplorerElement>()
val explorerElements = listDir(rootPath)
result.addAll(explorerElements)
for (e in explorerElements){
if (e.isDirectory){
result.addAll(recursiveMapFiles(e.fullPath))
}
}
return result
}
fun recursiveRemoveDirectory(plain_directory_path: String): String? {
val explorerElements = listDir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
val result = recursiveRemoveDirectory(fullPath)
result?.let { return it }
} else {
if (!removeFile(fullPath)) {
return fullPath
}
}
}
return if (!rmdir(plain_directory_path)) {
plain_directory_path
} else {
null
}
}
fun loadWholeFile(fullPath: String, size: Long? = null, maxSize: Long? = null): Pair<ByteArray?, Int> {
val fileSize = size ?: getSize(fullPath)
return if (fileSize >= 0) {
maxSize?.let {
if (fileSize > it) {
return Pair(null, 0)
}
}
try {
val fileBuff = ByteArray(fileSize.toInt())
val handleID = openReadMode(fullPath)
if (handleID == -1) {
Pair(null, 3)
} else {
var offset: Long = 0
val ioBuffer = ByteArray(DefaultBS)
var length: Int
while (readFile(handleID, offset, ioBuffer).also { length = it } > 0) {
System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length)
offset += length.toLong()
}
closeFile(handleID)
if (offset == fileBuff.size.toLong()) {
Pair(fileBuff, 0)
} else {
Pair(null, 4)
}
}
} catch (e: OutOfMemoryError) {
Pair(null, 2)
}
} else {
Pair(null, 1)
}
}
}

110
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<Volume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
private fun removeVolumes(volumes: List<SavedVolume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
if (i < volumes.size) {
if (volumes[i].isHidden) {
if (doDeleteVolumeContent == null) {
@ -306,7 +309,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(File(volume.name)),
DocumentFile.fromFile(filesDir),
) {
Volume(volume.shortName, true, volume.encryptedHash, volume.iv)
SavedVolume(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
}
}
}
@ -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<Int>(this@MainActivity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): Int {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
object : LoadingTask<EncryptedVolume?>(this@MainActivity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, filesDir.path, null, hash, null)
Arrays.fill(hash, 0)
return sessionId
return encryptedVolume
}
}.startTask(lifecycleScope) { sessionId ->
if (sessionId != -1) {
startExplorer(sessionId, volume.shortName)
} else {
}.startTask(lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(this@MainActivity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
startExplorer(encryptedVolume, volume.shortName)
}
}
}
@ -496,7 +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<Int>(this, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): Int {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
Arrays.fill(password, 0.toChar())
return sessionId
object : LoadingTask<EncryptedVolume?>(this, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, filesDir.path, password, null, returnedHash)
Arrays.fill(password, 0)
return encryptedVolume
}
}.startTask(lifecycleScope) { sessionId ->
if (sessionId != -1) {
}.startTask(lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok) { _, _ ->
askForPassword(volume, position, savePasswordHash)
}
.show()
} else {
val fingerprintProtector = fingerprintProtector
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
@ -575,12 +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<EncryptedVolume>("volume")!!)
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
}
if (explorerIntent == null) {
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
}
explorerIntent.putExtra("sessionID", sessionId)
explorerIntent.putExtra("volume", encryptedVolume)
explorerIntent.putExtra("volume_name", volumeShortName)
startActivity(explorerIntent)
if (pickMode)
@ -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<EncryptedVolume>("volume")?.close()
RestrictedFileProvider.wipeAll(this)
}
}
}

20
app/src/main/java/sushi/hardcore/droidfs/Volume.kt → 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<Volume> {
override fun createFromParcel(parcel: Parcel): Volume {
return Volume(parcel)
companion object {
const val VOLUMES_DIRECTORY = "volumes"
@JvmField
val CREATOR = object : Parcelable.Creator<SavedVolume> {
override fun createFromParcel(parcel: Parcel) = SavedVolume(parcel)
override fun newArray(size: Int) = arrayOfNulls<SavedVolume>(size)
}
override fun newArray(size: Int): Array<Volume?> {
return arrayOfNulls(size)
fun getHiddenVolumeFullPath(filesDir: String, name: String): String {
return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name)
}
}
}

87
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<Volume> {
val list: MutableList<Volume> = ArrayList()
fun getVolumes(): List<SavedVolume> {
val list: MutableList<SavedVolume> = ArrayList()
val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null)
while (cursor.moveToNext()){
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
}