2020-11-04 18:42:04 +01:00
|
|
|
package sushi.hardcore.droidfs
|
|
|
|
|
|
|
|
import android.content.ContentValues
|
|
|
|
import android.content.Context
|
2022-09-30 21:22:37 +02:00
|
|
|
import android.database.Cursor
|
2020-11-04 18:42:04 +01:00
|
|
|
import android.database.sqlite.SQLiteDatabase
|
|
|
|
import android.database.sqlite.SQLiteOpenHelper
|
2022-06-18 21:13:16 +02:00
|
|
|
import android.util.Log
|
|
|
|
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
|
|
|
import sushi.hardcore.droidfs.util.PathUtils
|
|
|
|
import java.io.File
|
2020-11-04 18:42:04 +01:00
|
|
|
|
2023-09-06 19:27:41 +02:00
|
|
|
class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Constants.VOLUME_DATABASE_NAME, null, 6) {
|
2020-11-04 18:42:04 +01:00
|
|
|
companion object {
|
2023-09-06 19:27:41 +02:00
|
|
|
private const val TAG = "VolumeDatabase"
|
|
|
|
private const val TABLE_NAME = "Volumes"
|
|
|
|
private const val COLUMN_UUID = "uuid"
|
|
|
|
private const val COLUMN_NAME = "name"
|
|
|
|
private const val COLUMN_HIDDEN = "hidden"
|
|
|
|
private const val COLUMN_TYPE = "type"
|
|
|
|
private const val COLUMN_HASH = "hash"
|
|
|
|
private const val COLUMN_IV = "iv"
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
2023-09-06 19:27:41 +02:00
|
|
|
|
|
|
|
private fun createTable(db: SQLiteDatabase) =
|
2020-11-04 18:42:04 +01:00
|
|
|
db.execSQL(
|
2023-09-06 19:27:41 +02:00
|
|
|
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
|
|
|
|
"$COLUMN_UUID TEXT PRIMARY KEY," +
|
|
|
|
"$COLUMN_NAME TEXT," +
|
|
|
|
"$COLUMN_HIDDEN SHORT," +
|
|
|
|
"$COLUMN_TYPE BLOB," +
|
|
|
|
"$COLUMN_HASH BLOB," +
|
|
|
|
"$COLUMN_IV BLOB" +
|
|
|
|
");"
|
2020-11-04 18:42:04 +01:00
|
|
|
)
|
2023-09-06 19:27:41 +02:00
|
|
|
|
|
|
|
override fun onCreate(db: SQLiteDatabase) {
|
|
|
|
createTable(db)
|
2022-09-30 21:22:37 +02:00
|
|
|
File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
|
|
|
|
2023-09-19 13:39:35 +02:00
|
|
|
override fun onOpen(db: SQLiteDatabase) {
|
|
|
|
//check if database has been corrupted by v2.1.1
|
|
|
|
val cursor = db.rawQuery("SELECT * FROM $TABLE_NAME WHERE $COLUMN_TYPE IS NULL;", null)
|
|
|
|
if (cursor.count > 0) {
|
|
|
|
Log.w(TAG, "Found ${cursor.count} corrupted volumes")
|
|
|
|
while (cursor.moveToNext()) {
|
|
|
|
// fix columns left shift
|
|
|
|
val uuid = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_UUID)+5)
|
|
|
|
val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)-1)
|
|
|
|
val isHidden = cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)-1) == 1.toShort()
|
|
|
|
val type = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE)-1)[0]
|
|
|
|
val hash = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)-1)
|
|
|
|
val iv = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_IV)-1)
|
|
|
|
if (db.delete(TABLE_NAME, "$COLUMN_IV=?", arrayOf(uuid)) < 1) {
|
|
|
|
Log.e(TAG, "Failed to remove volume $name")
|
|
|
|
}
|
|
|
|
if (db.insert(TABLE_NAME, null, ContentValues().apply {
|
|
|
|
put(COLUMN_UUID, uuid)
|
|
|
|
put(COLUMN_NAME, name)
|
|
|
|
put(COLUMN_HIDDEN, isHidden)
|
|
|
|
put(COLUMN_TYPE, byteArrayOf(type))
|
|
|
|
put(COLUMN_HASH, hash)
|
|
|
|
put(COLUMN_IV, iv)
|
|
|
|
}) < 0) {
|
|
|
|
Log.e(TAG, "Failed to insert volume $name")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cursor.close()
|
|
|
|
}
|
|
|
|
|
2023-04-25 15:06:20 +02:00
|
|
|
private fun getNewVolumePath(volumeName: String): File {
|
|
|
|
return File(
|
2023-09-06 19:27:41 +02:00
|
|
|
VolumeData.getFullPath(volumeName, true, context.filesDir.path)
|
2023-04-25 15:06:20 +02:00
|
|
|
).canonicalFile
|
|
|
|
}
|
|
|
|
|
2022-06-18 21:13:16 +02:00
|
|
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
2023-04-25 15:06:20 +02:00
|
|
|
if (oldVersion == 3) {
|
|
|
|
// 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)
|
2022-06-18 21:13:16 +02:00
|
|
|
|
2023-04-25 15:06:20 +02:00
|
|
|
// Moving registered hidden volumes to the "volumes" directory
|
|
|
|
if (File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()) {
|
|
|
|
val cursor = db.query(
|
|
|
|
TABLE_NAME,
|
|
|
|
arrayOf(COLUMN_NAME),
|
|
|
|
"$COLUMN_HIDDEN=?",
|
|
|
|
arrayOf("1"),
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null
|
2022-06-18 21:13:16 +02:00
|
|
|
)
|
2023-04-25 15:06:20 +02:00
|
|
|
while (cursor.moveToNext()) {
|
|
|
|
val volumeName = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME))
|
|
|
|
val success = File(
|
|
|
|
PathUtils.pathJoin(
|
|
|
|
context.filesDir.path,
|
|
|
|
volumeName
|
|
|
|
)
|
|
|
|
).renameTo(getNewVolumePath(volumeName))
|
|
|
|
if (!success) {
|
|
|
|
Log.e(TAG, "Failed to move $volumeName")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cursor.close()
|
|
|
|
} else {
|
|
|
|
Log.e(TAG, "Volumes directory creation failed while upgrading")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Moving unregistered hidden volumes to the "volumes" directory
|
|
|
|
File(context.filesDir.path).listFiles()?.let {
|
|
|
|
for (i in it) {
|
|
|
|
if (i.isDirectory && i.name != Constants.CRYFS_LOCAL_STATE_DIR && i.name != VolumeData.VOLUMES_DIRECTORY) {
|
|
|
|
if (EncryptedVolume.getVolumeType(i.path) != (-1).toByte()) {
|
|
|
|
if (!i.renameTo(getNewVolumePath(i.name))) {
|
|
|
|
Log.e(TAG, "Failed to move "+i.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-18 21:13:16 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-06 19:27:41 +02:00
|
|
|
if (oldVersion < 6) {
|
2023-09-17 19:11:52 +02:00
|
|
|
val cursor = db.rawQuery("SELECT $COLUMN_NAME FROM $TABLE_NAME;", null)
|
|
|
|
val volumeNames = arrayOfNulls<String>(cursor.count)
|
|
|
|
var i = 0
|
|
|
|
while (cursor.moveToNext()) {
|
|
|
|
volumeNames[i++] = cursor.getString(0)
|
2023-09-06 19:27:41 +02:00
|
|
|
}
|
2023-09-17 19:11:52 +02:00
|
|
|
cursor.close()
|
2023-09-19 11:41:01 +02:00
|
|
|
if (volumeNames.isEmpty()) {
|
|
|
|
db.execSQL("DROP TABLE $TABLE_NAME;")
|
|
|
|
createTable(db)
|
|
|
|
} else {
|
|
|
|
db.execSQL("ALTER TABLE $TABLE_NAME RENAME TO OLD;")
|
|
|
|
createTable(db)
|
|
|
|
val uuidsValues = volumeNames.indices.joinToString(", ") { "('${VolumeData.newUuid()}', ?)" }
|
|
|
|
// add uuids to old data
|
|
|
|
db.execSQL(
|
|
|
|
"INSERT INTO $TABLE_NAME " +
|
|
|
|
"WITH uuids($COLUMN_UUID, $COLUMN_NAME) AS (VALUES $uuidsValues) " +
|
|
|
|
"SELECT $COLUMN_UUID, OLD.$COLUMN_NAME, $COLUMN_HIDDEN, $COLUMN_TYPE, $COLUMN_HASH, $COLUMN_IV " +
|
|
|
|
"FROM OLD JOIN uuids ON OLD.name = uuids.name;",
|
|
|
|
volumeNames
|
|
|
|
)
|
|
|
|
db.execSQL("DROP TABLE OLD;")
|
|
|
|
}
|
2023-09-06 19:27:41 +02:00
|
|
|
}
|
2022-06-18 21:13:16 +02:00
|
|
|
}
|
2020-11-04 18:42:04 +01:00
|
|
|
|
2022-09-30 21:22:37 +02:00
|
|
|
private fun extractVolumeData(cursor: Cursor): VolumeData {
|
|
|
|
return VolumeData(
|
2023-09-06 19:27:41 +02:00
|
|
|
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_UUID)),
|
2022-09-30 21:22:37 +02:00
|
|
|
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))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getVolumeCursor(volumeName: String, isHidden: Boolean): Cursor {
|
|
|
|
return readableDatabase.query(
|
|
|
|
TABLE_NAME, null,
|
|
|
|
"$COLUMN_NAME=? AND $COLUMN_HIDDEN=?",
|
2022-03-05 12:51:02 +01:00
|
|
|
arrayOf(volumeName, (if (isHidden) 1 else 0).toString()),
|
|
|
|
null, null, null
|
|
|
|
)
|
2022-09-30 21:22:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fun getVolume(volumeName: String, isHidden: Boolean): VolumeData? {
|
|
|
|
val cursor = getVolumeCursor(volumeName, isHidden)
|
|
|
|
val volumeData = if (cursor.moveToNext()) {
|
|
|
|
extractVolumeData(cursor)
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
cursor.close()
|
|
|
|
return volumeData
|
|
|
|
}
|
|
|
|
|
|
|
|
fun isVolumeSaved(volumeName: String, isHidden: Boolean): Boolean {
|
|
|
|
val cursor = getVolumeCursor(volumeName, isHidden)
|
2020-11-04 18:42:04 +01:00
|
|
|
val result = cursor.count > 0
|
|
|
|
cursor.close()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2022-09-30 21:22:37 +02:00
|
|
|
fun saveVolume(volume: VolumeData): Boolean {
|
2022-03-05 12:51:02 +01:00
|
|
|
if (!isVolumeSaved(volume.name, volume.isHidden)) {
|
2023-09-06 19:27:41 +02:00
|
|
|
return (writableDatabase.insert(TABLE_NAME, null, ContentValues().apply {
|
|
|
|
put(COLUMN_UUID, volume.uuid)
|
|
|
|
put(COLUMN_NAME, volume.name)
|
|
|
|
put(COLUMN_HIDDEN, volume.isHidden)
|
|
|
|
put(COLUMN_TYPE, byteArrayOf(volume.type))
|
|
|
|
put(COLUMN_HASH, volume.encryptedHash)
|
|
|
|
put(COLUMN_IV, volume.iv)
|
2023-09-10 19:12:51 +02:00
|
|
|
}) >= 0.toLong())
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-09-30 21:22:37 +02:00
|
|
|
fun getVolumes(): List<VolumeData> {
|
|
|
|
val list: MutableList<VolumeData> = ArrayList()
|
2020-11-04 18:42:04 +01:00
|
|
|
val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null)
|
|
|
|
while (cursor.moveToNext()){
|
2022-09-30 21:22:37 +02:00
|
|
|
list.add(extractVolumeData(cursor))
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
|
|
|
cursor.close()
|
|
|
|
return list
|
|
|
|
}
|
|
|
|
|
2023-09-06 19:27:41 +02:00
|
|
|
fun isHashSaved(volume: VolumeData): Boolean {
|
|
|
|
val cursor = readableDatabase.rawQuery("SELECT $COLUMN_HASH FROM $TABLE_NAME WHERE $COLUMN_UUID=?", arrayOf(volume.uuid))
|
2020-11-04 18:42:04 +01:00
|
|
|
var isHashSaved = false
|
2022-03-05 12:51:02 +01:00
|
|
|
if (cursor.moveToNext()) {
|
2022-06-18 21:13:16 +02:00
|
|
|
if (cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)) != null) {
|
2020-11-04 18:42:04 +01:00
|
|
|
isHashSaved = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cursor.close()
|
|
|
|
return isHashSaved
|
|
|
|
}
|
|
|
|
|
2022-09-30 21:22:37 +02:00
|
|
|
fun addHash(volume: VolumeData): Boolean {
|
2023-09-06 19:27:41 +02:00
|
|
|
return writableDatabase.update(TABLE_NAME, ContentValues().apply {
|
|
|
|
put(COLUMN_HASH, volume.encryptedHash)
|
|
|
|
put(COLUMN_IV, volume.iv)
|
|
|
|
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
|
|
|
|
2022-09-30 21:22:37 +02:00
|
|
|
fun removeHash(volume: VolumeData): Boolean {
|
2020-11-04 18:42:04 +01:00
|
|
|
return writableDatabase.update(
|
2023-09-06 19:27:41 +02:00
|
|
|
TABLE_NAME,
|
|
|
|
ContentValues().apply {
|
|
|
|
put(COLUMN_HASH, null as ByteArray?)
|
|
|
|
put(COLUMN_IV, null as ByteArray?)
|
|
|
|
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)
|
|
|
|
) > 0
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
|
|
|
|
2023-09-06 19:27:41 +02:00
|
|
|
fun renameVolume(volume: VolumeData, newName: String): Boolean {
|
|
|
|
return writableDatabase.update(
|
|
|
|
TABLE_NAME,
|
2022-03-24 20:08:23 +01:00
|
|
|
ContentValues().apply {
|
|
|
|
put(COLUMN_NAME, newName)
|
|
|
|
},
|
2023-09-06 19:27:41 +02:00
|
|
|
"$COLUMN_UUID=?", arrayOf(volume.uuid)
|
2022-03-24 20:08:23 +01:00
|
|
|
) > 0
|
|
|
|
}
|
|
|
|
|
2023-09-06 19:27:41 +02:00
|
|
|
fun removeVolume(volume: VolumeData): Boolean {
|
|
|
|
return writableDatabase.delete(TABLE_NAME, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
|
2020-11-04 18:42:04 +01:00
|
|
|
}
|
|
|
|
}
|