Полное управление шифрованием и ключами
This commit is contained in:
@@ -43,7 +43,9 @@ class UseCasesModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideManageStoragesEncryptionUseCase(unlockManager: IUnlockManager): ManageStoragesEncryptionUseCase {
|
fun provideManageStoragesEncryptionUseCase(
|
||||||
|
unlockManager: IUnlockManager,
|
||||||
|
): ManageStoragesEncryptionUseCase {
|
||||||
return ManageStoragesEncryptionUseCase(unlockManager)
|
return ManageStoragesEncryptionUseCase(unlockManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +54,8 @@ class UseCasesModule {
|
|||||||
fun provideRemoveStorageUseCase(
|
fun provideRemoveStorageUseCase(
|
||||||
vaultsManager: IVaultsManager,
|
vaultsManager: IVaultsManager,
|
||||||
unlockManager: IUnlockManager,
|
unlockManager: IUnlockManager,
|
||||||
|
manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
|
||||||
): RemoveStorageUseCase {
|
): RemoveStorageUseCase {
|
||||||
return RemoveStorageUseCase(vaultsManager, unlockManager)
|
return RemoveStorageUseCase(vaultsManager, unlockManager, manageStoragesEncryptionUseCase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ interface IAppDb {
|
|||||||
val storageMetaInfoDao: StorageMetaInfoDao
|
val storageMetaInfoDao: StorageMetaInfoDao
|
||||||
}
|
}
|
||||||
|
|
||||||
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 2, exportSchema = false)
|
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 3, exportSchema = false)
|
||||||
abstract class AppDb : IAppDb, RoomDatabase() {
|
abstract class AppDb : IAppDb, RoomDatabase() {
|
||||||
abstract override val storageKeyMapDao: StorageKeyMapDao
|
abstract override val storageKeyMapDao: StorageKeyMapDao
|
||||||
abstract override val storageMetaInfoDao: StorageMetaInfoDao
|
abstract override val storageMetaInfoDao: StorageMetaInfoDao
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ import com.github.nullptroma.wallenc.data.model.StorageKeyMap
|
|||||||
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Entity(tableName = "storage_key_maps", primaryKeys = [ "source_uuid", "dest_uuid" ])
|
@Entity(tableName = "storage_key_maps")
|
||||||
data class DbStorageKeyMap(
|
data class DbStorageKeyMap(
|
||||||
|
@androidx.room.PrimaryKey
|
||||||
@ColumnInfo(name = "source_uuid") val sourceUuid: UUID,
|
@ColumnInfo(name = "source_uuid") val sourceUuid: UUID,
|
||||||
@ColumnInfo(name = "dest_uuid") val destUuid: UUID,
|
|
||||||
@ColumnInfo(name = "key") val key: ByteArray
|
@ColumnInfo(name = "key") val key: ByteArray
|
||||||
) {
|
) {
|
||||||
fun toModel(): StorageKeyMap {
|
fun toModel(): StorageKeyMap {
|
||||||
return StorageKeyMap(
|
return StorageKeyMap(
|
||||||
sourceUuid = sourceUuid,
|
sourceUuid = sourceUuid,
|
||||||
destUuid = destUuid,
|
|
||||||
key = EncryptKey(key)
|
key = EncryptKey(key)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -28,7 +27,6 @@ data class DbStorageKeyMap(
|
|||||||
other as DbStorageKeyMap
|
other as DbStorageKeyMap
|
||||||
|
|
||||||
if (sourceUuid != other.sourceUuid) return false
|
if (sourceUuid != other.sourceUuid) return false
|
||||||
if (destUuid != other.destUuid) return false
|
|
||||||
if (!key.contentEquals(other.key)) return false
|
if (!key.contentEquals(other.key)) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -36,7 +34,6 @@ data class DbStorageKeyMap(
|
|||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = sourceUuid.hashCode()
|
var result = sourceUuid.hashCode()
|
||||||
result = 31 * result + destUuid.hashCode()
|
|
||||||
result = 31 * result + key.contentHashCode()
|
result = 31 * result + key.contentHashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -45,7 +42,6 @@ data class DbStorageKeyMap(
|
|||||||
fun fromModel(keymap: StorageKeyMap): DbStorageKeyMap {
|
fun fromModel(keymap: StorageKeyMap): DbStorageKeyMap {
|
||||||
return DbStorageKeyMap(
|
return DbStorageKeyMap(
|
||||||
sourceUuid = keymap.sourceUuid,
|
sourceUuid = keymap.sourceUuid,
|
||||||
destUuid = keymap.destUuid,
|
|
||||||
key = keymap.key.bytes
|
key = keymap.key.bytes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ import java.util.UUID
|
|||||||
|
|
||||||
data class StorageKeyMap(
|
data class StorageKeyMap(
|
||||||
val sourceUuid: UUID,
|
val sourceUuid: UUID,
|
||||||
val destUuid: UUID,
|
|
||||||
val key: EncryptKey
|
val key: EncryptKey
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class UnlockManager(
|
class UnlockManager(
|
||||||
@@ -45,7 +47,7 @@ class UnlockManager(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val encStorage = createEncryptedStorage(storage, key.key, key.destUuid)
|
val encStorage = createEncryptedStorage(storage, key.key, getDestUuid(storage.uuid))
|
||||||
map[storage.uuid] = encStorage
|
map[storage.uuid] = encStorage
|
||||||
allStorages.removeAt(allStorages.size - 1)
|
allStorages.removeAt(allStorages.size - 1)
|
||||||
allStorages.add(encStorage)
|
allStorages.add(encStorage)
|
||||||
@@ -72,9 +74,34 @@ class UnlockManager(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDestUuid(sourceUuid: UUID): UUID {
|
||||||
|
return uuid5(
|
||||||
|
namespace = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"), // URL namespace
|
||||||
|
name = "$sourceUuid:open"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uuid5(namespace: UUID, name: String): UUID {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-1")
|
||||||
|
val nsBytes = ByteBuffer.allocate(16)
|
||||||
|
.putLong(namespace.mostSignificantBits)
|
||||||
|
.putLong(namespace.leastSignificantBits)
|
||||||
|
.array()
|
||||||
|
digest.update(nsBytes)
|
||||||
|
digest.update(name.toByteArray(Charsets.UTF_8))
|
||||||
|
val hash = digest.digest()
|
||||||
|
|
||||||
|
hash[6] = (hash[6].toInt() and 0x0f or 0x50).toByte() // version 5
|
||||||
|
hash[8] = (hash[8].toInt() and 0x3f or 0x80).toByte() // RFC 4122 variant
|
||||||
|
|
||||||
|
val bb = ByteBuffer.wrap(hash, 0, 16)
|
||||||
|
return UUID(bb.long, bb.long)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun open(
|
override suspend fun open(
|
||||||
storage: IStorage,
|
storage: IStorage,
|
||||||
key: EncryptKey
|
key: EncryptKey,
|
||||||
|
rememberPassword: Boolean
|
||||||
): EncryptedStorage = withContext(ioDispatcher) {
|
): EncryptedStorage = withContext(ioDispatcher) {
|
||||||
return@withContext mutex.withLock {
|
return@withContext mutex.withLock {
|
||||||
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
|
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
|
||||||
@@ -84,17 +111,18 @@ class UnlockManager(
|
|||||||
val opened = _openedStorages.value.toMutableMap()
|
val opened = _openedStorages.value.toMutableMap()
|
||||||
val cur = opened[storage.uuid]
|
val cur = opened[storage.uuid]
|
||||||
if (cur != null)
|
if (cur != null)
|
||||||
throw Exception("Storage is already open")
|
return@withLock cur
|
||||||
|
|
||||||
val keymap = StorageKeyMap(
|
val keymap = StorageKeyMap(
|
||||||
sourceUuid = storage.uuid,
|
sourceUuid = storage.uuid,
|
||||||
destUuid = UUID.randomUUID(),
|
|
||||||
key = key
|
key = key
|
||||||
)
|
)
|
||||||
val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid)
|
val encStorage = createEncryptedStorage(storage, keymap.key, getDestUuid(storage.uuid))
|
||||||
opened[storage.uuid] = encStorage
|
opened[storage.uuid] = encStorage
|
||||||
_openedStorages.value = opened
|
_openedStorages.value = opened
|
||||||
|
if (rememberPassword) {
|
||||||
keymapRepository.add(keymap)
|
keymapRepository.add(keymap)
|
||||||
|
}
|
||||||
encStorage
|
encStorage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,30 +139,21 @@ class UnlockManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Закрытие отображения по экземпляру (source или decrypted).
|
// Закрытие только по source-экземпляру.
|
||||||
override suspend fun close(storage: IStorage) {
|
override suspend fun close(storage: IStorage) {
|
||||||
val opened = _openedStorages.value
|
val opened = _openedStorages.value
|
||||||
val source = opened.entries.firstOrNull {
|
if (opened.containsKey(storage.uuid)) {
|
||||||
it.key == storage.uuid || it.value.uuid == storage.uuid
|
close(storage.uuid)
|
||||||
}
|
}
|
||||||
if (source != null)
|
|
||||||
close(source.key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun closeBySourceUuid(opened: MutableMap<UUID, EncryptedStorage>, sourceUuid: UUID) {
|
private suspend fun closeBySourceUuid(opened: MutableMap<UUID, EncryptedStorage>, sourceUuid: UUID) {
|
||||||
val enc = opened[sourceUuid] ?: return
|
val enc = opened[sourceUuid] ?: return
|
||||||
val childSourceUuid = opened.entries.firstOrNull { it.value.uuid == enc.uuid }?.key
|
val nestedSourceUuid = enc.uuid
|
||||||
if (childSourceUuid != null) {
|
if (nestedSourceUuid != sourceUuid && opened.containsKey(nestedSourceUuid)) {
|
||||||
closeBySourceUuid(opened, childSourceUuid)
|
closeBySourceUuid(opened, nestedSourceUuid)
|
||||||
}
|
}
|
||||||
opened.remove(sourceUuid)
|
opened.remove(sourceUuid)
|
||||||
enc.dispose()
|
enc.dispose()
|
||||||
keymapRepository.delete(
|
|
||||||
StorageKeyMap(
|
|
||||||
sourceUuid = sourceUuid,
|
|
||||||
destUuid = enc.uuid,
|
|
||||||
key = EncryptKey("")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DisposableHandle
|
import kotlinx.coroutines.DisposableHandle
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -35,6 +36,8 @@ class EncryptedStorage private constructor(
|
|||||||
get() = accessor.size
|
get() = accessor.size
|
||||||
override val numberOfFiles: StateFlow<Int?>
|
override val numberOfFiles: StateFlow<Int?>
|
||||||
get() = accessor.numberOfFiles
|
get() = accessor.numberOfFiles
|
||||||
|
override val isEmpty: Flow<Boolean?>
|
||||||
|
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
|
||||||
|
|
||||||
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
|
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
|
||||||
CommonStorageMetaInfo()
|
CommonStorageMetaInfo()
|
||||||
@@ -102,7 +105,7 @@ class EncryptedStorage private constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = scope.run {
|
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = scope.run {
|
||||||
val curMeta = metaInfo.value
|
val curMeta = metaInfo.value
|
||||||
updateMetaInfo(
|
updateMetaInfo(
|
||||||
CommonStorageMetaInfo(
|
CommonStorageMetaInfo(
|
||||||
@@ -112,6 +115,20 @@ class EncryptedStorage private constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun clearAllContent() = scope.run {
|
||||||
|
val files = accessor.getAllFiles()
|
||||||
|
val dirs = accessor.getAllDirs()
|
||||||
|
val paths = buildList {
|
||||||
|
addAll(files.map { it.metaInfo.path })
|
||||||
|
addAll(dirs.map { it.metaInfo.path })
|
||||||
|
}
|
||||||
|
.filter { it != "/" && it.isNotBlank() }
|
||||||
|
.sortedByDescending { it.length }
|
||||||
|
for (path in paths) {
|
||||||
|
accessor.delete(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
accessor.dispose()
|
accessor.dispose()
|
||||||
job.cancel()
|
job.cancel()
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
|||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -23,6 +25,8 @@ class LocalStorage(
|
|||||||
get() = accessor.size
|
get() = accessor.size
|
||||||
override val numberOfFiles: StateFlow<Int?>
|
override val numberOfFiles: StateFlow<Int?>
|
||||||
get() = accessor.numberOfFiles
|
get() = accessor.numberOfFiles
|
||||||
|
override val isEmpty: Flow<Boolean?>
|
||||||
|
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
|
||||||
|
|
||||||
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
|
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
|
||||||
CommonStorageMetaInfo()
|
CommonStorageMetaInfo()
|
||||||
@@ -82,7 +86,7 @@ class LocalStorage(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = withContext(ioDispatcher) {
|
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) {
|
||||||
val curMeta = metaInfo.value
|
val curMeta = metaInfo.value
|
||||||
updateMetaInfo(CommonStorageMetaInfo(
|
updateMetaInfo(CommonStorageMetaInfo(
|
||||||
encInfo = encInfo,
|
encInfo = encInfo,
|
||||||
@@ -90,6 +94,20 @@ class LocalStorage(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun clearAllContent() = withContext(ioDispatcher) {
|
||||||
|
val files = accessor.getAllFiles()
|
||||||
|
val dirs = accessor.getAllDirs()
|
||||||
|
val paths = buildList {
|
||||||
|
addAll(files.map { it.metaInfo.path })
|
||||||
|
addAll(dirs.map { it.metaInfo.path })
|
||||||
|
}
|
||||||
|
.filter { it != "/" && it.isNotBlank() }
|
||||||
|
.sortedByDescending { it.length }
|
||||||
|
for (path in paths) {
|
||||||
|
accessor.delete(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val STORAGE_INFO_FILE_POSTFIX = ".storage-info"
|
const val STORAGE_INFO_FILE_POSTFIX = ".storage-info"
|
||||||
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
|
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
|
||||||
|
|||||||
@@ -494,10 +494,15 @@ class LocalStorageAccessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
||||||
|
if (path == "/" || path.isBlank()) {
|
||||||
|
throw IllegalArgumentException("Deleting root path is forbidden")
|
||||||
|
}
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
if (pair != null) {
|
if (pair != null) {
|
||||||
pair.file.delete()
|
if (pair.file.isDirectory) pair.file.deleteRecursively()
|
||||||
|
else pair.file.delete()
|
||||||
pair.metaFile.delete()
|
pair.metaFile.delete()
|
||||||
|
scanSizeAndNumOfFiles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.enums
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Политика удаления/закрытия хранилища.
|
|
||||||
*
|
|
||||||
* [CLOSE_ENCRYPTED_OVERLAYS_ONLY] — только закрыть расшифрованные представления (overlay),
|
|
||||||
* физические данные не трогаем.
|
|
||||||
*
|
|
||||||
* [REMOVE_PHYSICAL] — удалить физическое хранилище у провайдера (сейчас local vault),
|
|
||||||
* предварительно закрыв все overlay.
|
|
||||||
*/
|
|
||||||
enum class StorageDeletionPolicy {
|
|
||||||
CLOSE_ENCRYPTED_OVERLAYS_ONLY,
|
|
||||||
REMOVE_PHYSICAL,
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.interfaces
|
package com.github.nullptroma.wallenc.domain.interfaces
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -10,6 +11,7 @@ sealed interface IStorageInfo {
|
|||||||
val isAvailable: StateFlow<Boolean>
|
val isAvailable: StateFlow<Boolean>
|
||||||
val size: StateFlow<Long?>
|
val size: StateFlow<Long?>
|
||||||
val numberOfFiles: StateFlow<Int?>
|
val numberOfFiles: StateFlow<Int?>
|
||||||
|
val isEmpty: Flow<Boolean?>
|
||||||
val metaInfo: StateFlow<IStorageMetaInfo>
|
val metaInfo: StateFlow<IStorageMetaInfo>
|
||||||
val isVirtualStorage: Boolean
|
val isVirtualStorage: Boolean
|
||||||
}
|
}
|
||||||
@@ -18,7 +20,8 @@ interface IStorage: IStorageInfo {
|
|||||||
val accessor: IStorageAccessor
|
val accessor: IStorageAccessor
|
||||||
|
|
||||||
suspend fun rename(newName: String)
|
suspend fun rename(newName: String)
|
||||||
suspend fun setEncInfo(encInfo: StorageEncryptionInfo)
|
suspend fun setEncInfo(encInfo: StorageEncryptionInfo?)
|
||||||
|
suspend fun clearAllContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStorageMetaInfo {
|
interface IStorageMetaInfo {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface IUnlockManager {
|
|||||||
*/
|
*/
|
||||||
val openedStorages: StateFlow<Map<UUID, IStorage>>
|
val openedStorages: StateFlow<Map<UUID, IStorage>>
|
||||||
|
|
||||||
suspend fun open(storage: IStorage, key: EncryptKey): IStorage
|
suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean = true): IStorage
|
||||||
suspend fun close(storage: IStorage)
|
suspend fun close(storage: IStorage)
|
||||||
suspend fun close(uuid: UUID): Unit
|
suspend fun close(uuid: UUID): Unit
|
||||||
}
|
}
|
||||||
@@ -5,23 +5,70 @@ import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
|
|||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
class ManageStoragesEncryptionUseCase(
|
||||||
|
private val unlockManager: IUnlockManager,
|
||||||
|
) {
|
||||||
|
sealed interface CanEncryptResult {
|
||||||
|
data object Allowed : CanEncryptResult
|
||||||
|
data object UnsupportedStorageType : CanEncryptResult
|
||||||
|
data object AlreadyEncrypted : CanEncryptResult
|
||||||
|
data object StorageIsNotEmpty : CanEncryptResult
|
||||||
|
data object StorageStateUnknown : CanEncryptResult
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun canEncrypt(storage: IStorageInfo): CanEncryptResult {
|
||||||
|
if (storage !is IStorage) return CanEncryptResult.UnsupportedStorageType
|
||||||
|
if (storage.metaInfo.value.encInfo != null) return CanEncryptResult.AlreadyEncrypted
|
||||||
|
|
||||||
|
val isEmpty = storage.isEmpty.first()
|
||||||
|
return when (isEmpty) {
|
||||||
|
true -> CanEncryptResult.Allowed
|
||||||
|
false -> CanEncryptResult.StorageIsNotEmpty
|
||||||
|
null -> CanEncryptResult.StorageStateUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ManageStoragesEncryptionUseCase(private val unlockManager: IUnlockManager) {
|
|
||||||
suspend fun enableEncryption(storage: IStorageInfo, key: EncryptKey, encryptPath: Boolean) {
|
suspend fun enableEncryption(storage: IStorageInfo, key: EncryptKey, encryptPath: Boolean) {
|
||||||
when(storage) {
|
when (val result = canEncrypt(storage)) {
|
||||||
is IStorage -> {
|
CanEncryptResult.Allowed -> (storage as IStorage).setEncInfo(
|
||||||
if(storage.metaInfo.value.encInfo != null)
|
Encryptor.generateEncryptionInfo(key, encryptPath)
|
||||||
throw Exception() // TODO
|
)
|
||||||
storage.setEncInfo(Encryptor.generateEncryptionInfo(key, encryptPath))
|
CanEncryptResult.AlreadyEncrypted -> throw IllegalStateException("Storage is already encrypted")
|
||||||
}
|
CanEncryptResult.StorageIsNotEmpty -> throw IllegalStateException("Storage is not empty")
|
||||||
|
CanEncryptResult.StorageStateUnknown -> throw IllegalStateException("Storage state is unknown")
|
||||||
|
CanEncryptResult.UnsupportedStorageType -> throw IllegalStateException("Unsupported storage type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey): IStorageInfo {
|
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey, rememberPassword: Boolean): IStorageInfo {
|
||||||
when(storage) {
|
if (storage is IStorage) return unlockManager.open(storage, key, rememberPassword)
|
||||||
is IStorage -> {
|
throw IllegalStateException("Unsupported storage type")
|
||||||
return unlockManager.open(storage, key)
|
}
|
||||||
|
|
||||||
|
suspend fun closeStorage(storage: IStorageInfo) {
|
||||||
|
if (storage is IStorage) {
|
||||||
|
unlockManager.close(storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun disableEncryption(storage: IStorageInfo) {
|
||||||
|
clearAndDisableEncryption(storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearAndDisableEncryption(storage: IStorageInfo) {
|
||||||
|
if (storage !is IStorage) return
|
||||||
|
storage.clearAllContent()
|
||||||
|
storage.setEncInfo(null)
|
||||||
|
unlockManager.close(storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun changePassword(storage: IStorageInfo, newKey: EncryptKey, encryptPath: Boolean) {
|
||||||
|
if (storage !is IStorage) return
|
||||||
|
if (storage.metaInfo.value.encInfo == null) {
|
||||||
|
throw IllegalStateException("Storage is not encrypted")
|
||||||
|
}
|
||||||
|
storage.setEncInfo(Encryptor.generateEncryptionInfo(newKey, encryptPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.usecases
|
package com.github.nullptroma.wallenc.domain.usecases
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.enums.StorageDeletionPolicy
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||||
@@ -10,36 +9,30 @@ import java.util.UUID
|
|||||||
class RemoveStorageUseCase(
|
class RemoveStorageUseCase(
|
||||||
private val vaultsManager: IVaultsManager,
|
private val vaultsManager: IVaultsManager,
|
||||||
private val unlockManager: IUnlockManager,
|
private val unlockManager: IUnlockManager,
|
||||||
|
private val manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun remove(storage: IStorageInfo, policy: StorageDeletionPolicy) {
|
suspend fun remove(storage: IStorageInfo) {
|
||||||
|
|
||||||
if (storage !is IStorage) return
|
if (storage !is IStorage) return
|
||||||
|
|
||||||
when (policy) {
|
if (!storage.isVirtualStorage) {
|
||||||
StorageDeletionPolicy.CLOSE_ENCRYPTED_OVERLAYS_ONLY -> {
|
|
||||||
unlockManager.close(storage)
|
unlockManager.close(storage)
|
||||||
}
|
vaultsManager.localVault.remove(storage)
|
||||||
StorageDeletionPolicy.REMOVE_PHYSICAL -> {
|
return
|
||||||
val physical = findPhysicalRootStorage(storage) ?: return
|
|
||||||
unlockManager.close(physical.uuid)
|
|
||||||
vaultsManager.localVault.remove(physical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val parent = findParentStorage(storage) ?: return
|
||||||
* Поднимается по цепочке overlay (sourceUuid -> decrypted view), пока не дойдёт
|
manageStoragesEncryptionUseCase.clearAndDisableEncryption(parent)
|
||||||
* до корневого физического storage из [IVaultsManager.localVault].
|
}
|
||||||
*/
|
|
||||||
private fun findPhysicalRootStorage(storage: IStorage): IStorage? {
|
|
||||||
val locals = vaultsManager.localVault.storages.value ?: return null
|
private fun findParentStorage(storage: IStorage): IStorage? {
|
||||||
val opened = unlockManager.openedStorages.value
|
val opened = unlockManager.openedStorages.value
|
||||||
|
val parentUuid = opened.entries.firstOrNull { it.value.uuid == storage.uuid }?.key ?: return null
|
||||||
|
val locals = vaultsManager.localVault.storages.value.orEmpty()
|
||||||
|
return locals.firstOrNull { it.uuid == parentUuid }
|
||||||
|
?: opened[parentUuid]
|
||||||
|
}
|
||||||
|
|
||||||
var id: UUID = storage.uuid
|
|
||||||
while (true) {
|
|
||||||
val parent = opened.entries.firstOrNull { it.value.uuid == id }?.key ?: break
|
|
||||||
id = parent
|
|
||||||
}
|
|
||||||
return locals.firstOrNull { it.uuid == id }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.BasicAlertDialog
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -21,6 +22,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -93,3 +95,98 @@ fun ConfirmationCancelOkDialog(onDismiss: () -> Unit, onConfirmation: () -> Unit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EncryptionSetupDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirmation: (password: String, encryptPath: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var encryptPath by remember { mutableStateOf(false) }
|
||||||
|
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||||
|
Card {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text("Enable encryption", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
TextField(value = password, onValueChange = { password = it }, label = { Text("Password") })
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(checked = encryptPath, onCheckedChange = { encryptPath = it })
|
||||||
|
Text("Encrypt paths")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
|
Button(modifier = Modifier.weight(1f), onClick = onDismiss) { Text("Cancel") }
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { onConfirmation(password, encryptPath) },
|
||||||
|
enabled = password.isNotEmpty()
|
||||||
|
) { Text("Apply") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun OpenEncryptedStorageDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirmation: (password: String, rememberPassword: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var rememberPassword by remember { mutableStateOf(false) }
|
||||||
|
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||||
|
Card {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text("Open encrypted storage", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
TextField(value = password, onValueChange = { password = it }, label = { Text("Password") })
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(checked = rememberPassword, onCheckedChange = { rememberPassword = it })
|
||||||
|
Text("Remember password")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
|
Button(modifier = Modifier.weight(1f), onClick = onDismiss) { Text("Cancel") }
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { onConfirmation(password, rememberPassword) },
|
||||||
|
enabled = password.isNotEmpty()
|
||||||
|
) { Text("Open") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun StorageEncryptionActionsDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
isOpened: Boolean,
|
||||||
|
onOpen: () -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onDisable: () -> Unit,
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||||
|
Card {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text(title, style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
if (isOpened) {
|
||||||
|
Button(onClick = onClose, modifier = Modifier.fillMaxWidth()) { Text("Close") }
|
||||||
|
} else {
|
||||||
|
Button(onClick = onOpen, modifier = Modifier.fillMaxWidth()) { Text("Open") }
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(onClick = onDisable, modifier = Modifier.fillMaxWidth()) { Text("Disable encryption") }
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Button(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) { Text("Done") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import androidx.compose.foundation.layout.offset
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.LockOpen
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -60,7 +61,12 @@ fun StorageTree(
|
|||||||
onClick: (Tree<IStorageInfo>) -> Unit,
|
onClick: (Tree<IStorageInfo>) -> Unit,
|
||||||
onRename: (Tree<IStorageInfo>, String) -> Unit,
|
onRename: (Tree<IStorageInfo>, String) -> Unit,
|
||||||
onRemove: (Tree<IStorageInfo>) -> Unit,
|
onRemove: (Tree<IStorageInfo>) -> Unit,
|
||||||
onEncrypt: (Tree<IStorageInfo>) -> Unit,
|
onEncrypt: (Tree<IStorageInfo>, String, Boolean) -> Unit,
|
||||||
|
onOpenEncrypted: (Tree<IStorageInfo>, String, Boolean) -> Unit,
|
||||||
|
onCloseEncrypted: (Tree<IStorageInfo>) -> Unit,
|
||||||
|
onDisableEncryption: (Tree<IStorageInfo>) -> Unit,
|
||||||
|
getStatusText: (Tree<IStorageInfo>) -> String,
|
||||||
|
isEncryptionOpened: (Tree<IStorageInfo>) -> Boolean,
|
||||||
) {
|
) {
|
||||||
val cur = tree.value
|
val cur = tree.value
|
||||||
val available by cur.isAvailable.collectAsStateWithLifecycle()
|
val available by cur.isAvailable.collectAsStateWithLifecycle()
|
||||||
@@ -68,6 +74,8 @@ fun StorageTree(
|
|||||||
val size by cur.size.collectAsStateWithLifecycle()
|
val size by cur.size.collectAsStateWithLifecycle()
|
||||||
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle()
|
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle()
|
||||||
val isAvailable by cur.isAvailable.collectAsStateWithLifecycle()
|
val isAvailable by cur.isAvailable.collectAsStateWithLifecycle()
|
||||||
|
val isEncrypted = metaInfo.encInfo != null
|
||||||
|
val isOpened = isEncryptionOpened(tree)
|
||||||
val borderColor =
|
val borderColor =
|
||||||
if (cur.isVirtualStorage) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary
|
if (cur.isVirtualStorage) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
@@ -120,10 +128,13 @@ fun StorageTree(
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp)) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var showRenameDialog by remember { mutableStateOf(false) }
|
var showRenameDialog by remember { mutableStateOf(false) }
|
||||||
var showRemoveConfirmationDiaglog by remember { mutableStateOf(false) }
|
var showRemoveConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
var showLockDialog by remember { mutableStateOf(false) }
|
||||||
|
var showSetupEncryptionDialog by remember { mutableStateOf(false) }
|
||||||
|
var showOpenEncryptionDialog by remember { mutableStateOf(false) }
|
||||||
|
Box(modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp)) {
|
||||||
IconButton(onClick = { expanded = !expanded }) {
|
IconButton(onClick = { expanded = !expanded }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.MoreVert,
|
Icons.Default.MoreVert,
|
||||||
@@ -145,10 +156,20 @@ fun StorageTree(
|
|||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
showRemoveConfirmationDiaglog = true;
|
showRemoveConfirmDialog = true
|
||||||
},
|
},
|
||||||
text = { Text(stringResource(R.string.remove)) }
|
text = { Text(stringResource(R.string.remove)) }
|
||||||
)
|
)
|
||||||
|
if (!isEncrypted) {
|
||||||
|
HorizontalDivider()
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
showSetupEncryptionDialog = true
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(R.string.encrypt)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showRenameDialog) {
|
if (showRenameDialog) {
|
||||||
@@ -163,26 +184,78 @@ fun StorageTree(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showRemoveConfirmationDiaglog) {
|
if (showRemoveConfirmDialog) {
|
||||||
ConfirmationCancelOkDialog(
|
ConfirmationCancelOkDialog(
|
||||||
onDismiss = {
|
onDismiss = { showRemoveConfirmDialog = false },
|
||||||
showRemoveConfirmationDiaglog = false
|
|
||||||
},
|
|
||||||
onConfirmation = {
|
|
||||||
showRemoveConfirmationDiaglog = false
|
|
||||||
onRemove(tree)
|
|
||||||
},
|
|
||||||
title = stringResource(
|
title = stringResource(
|
||||||
R.string.remove_confirmation_dialog,
|
R.string.remove_confirmation_dialog,
|
||||||
metaInfo.name ?: "<noname>"
|
metaInfo.name ?: "<noname>"
|
||||||
|
),
|
||||||
|
onConfirmation = {
|
||||||
|
showRemoveConfirmDialog = false
|
||||||
|
onRemove(tree)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLockDialog) {
|
||||||
|
StorageEncryptionActionsDialog(
|
||||||
|
onDismiss = { showLockDialog = false },
|
||||||
|
title = metaInfo.name ?: stringResource(R.string.no_name),
|
||||||
|
isOpened = isOpened,
|
||||||
|
onOpen = {
|
||||||
|
showLockDialog = false
|
||||||
|
showOpenEncryptionDialog = true
|
||||||
|
},
|
||||||
|
onClose = {
|
||||||
|
showLockDialog = false
|
||||||
|
onCloseEncrypted(tree)
|
||||||
|
},
|
||||||
|
onDisable = {
|
||||||
|
showLockDialog = false
|
||||||
|
onDisableEncryption(tree)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSetupEncryptionDialog) {
|
||||||
|
EncryptionSetupDialog(
|
||||||
|
onDismiss = { showSetupEncryptionDialog = false },
|
||||||
|
onConfirmation = { password, encryptPath ->
|
||||||
|
showSetupEncryptionDialog = false
|
||||||
|
onEncrypt(tree, password, encryptPath)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showOpenEncryptionDialog) {
|
||||||
|
OpenEncryptedStorageDialog(
|
||||||
|
onDismiss = { showOpenEncryptionDialog = false },
|
||||||
|
onConfirmation = { password, rememberPassword ->
|
||||||
|
showOpenEncryptionDialog = false
|
||||||
|
onOpenEncrypted(tree, password, rememberPassword)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Button(onClick = { onEncrypt(tree) }, enabled = metaInfo.encInfo == null) {
|
if (isEncrypted) {
|
||||||
Text("Encrypt")
|
IconButton(onClick = { showLockDialog = true }) {
|
||||||
|
Icon(
|
||||||
|
if (isOpened) Icons.Default.LockOpen else Icons.Default.Lock,
|
||||||
|
contentDescription = stringResource(R.string.storage_lock_actions)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(0.dp, 0.dp, 12.dp, 0.dp)
|
||||||
|
.align(Alignment.End),
|
||||||
|
text = getStatusText(tree),
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -221,7 +294,12 @@ fun StorageTree(
|
|||||||
onClick,
|
onClick,
|
||||||
onRename,
|
onRename,
|
||||||
onRemove,
|
onRemove,
|
||||||
onEncrypt
|
onEncrypt,
|
||||||
|
onOpenEncrypted,
|
||||||
|
onCloseEncrypted,
|
||||||
|
onDisableEncryption,
|
||||||
|
getStatusText,
|
||||||
|
isEncryptionOpened
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
||||||
|
|
||||||
import android.widget.ProgressBar
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
@@ -23,16 +21,18 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.presentation.elements.StorageTree
|
import com.github.nullptroma.wallenc.presentation.elements.StorageTree
|
||||||
import com.github.nullptroma.wallenc.presentation.extensions.gesturesDisabled
|
import com.github.nullptroma.wallenc.presentation.extensions.gesturesDisabled
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LocalVaultScreen(
|
fun LocalVaultScreen(
|
||||||
@@ -42,6 +42,13 @@ fun LocalVaultScreen(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.messages.collect { message ->
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = {
|
Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
@@ -66,9 +73,24 @@ fun LocalVaultScreen(
|
|||||||
onRemove = { tree ->
|
onRemove = { tree ->
|
||||||
viewModel.remove(tree.value)
|
viewModel.remove(tree.value)
|
||||||
},
|
},
|
||||||
onEncrypt = { tree ->
|
onEncrypt = { tree, password, encryptPath ->
|
||||||
viewModel.enableEncryptionAndOpenStorage(tree.value)
|
viewModel.enableEncryption(tree.value, password, encryptPath)
|
||||||
}
|
},
|
||||||
|
onOpenEncrypted = { tree, password, remember ->
|
||||||
|
viewModel.openEncryptedStorage(tree.value, password, remember)
|
||||||
|
},
|
||||||
|
onCloseEncrypted = { tree ->
|
||||||
|
viewModel.closeEncryptedStorage(tree.value)
|
||||||
|
},
|
||||||
|
onDisableEncryption = { tree ->
|
||||||
|
viewModel.disableEncryption(tree.value)
|
||||||
|
},
|
||||||
|
getStatusText = { tree ->
|
||||||
|
viewModel.getStorageStatus(tree.value)
|
||||||
|
},
|
||||||
|
isEncryptionOpened = { tree ->
|
||||||
|
viewModel.isEncryptionSessionOpen(tree.value)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
|||||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
|
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||||
import com.github.nullptroma.wallenc.domain.enums.StorageDeletionPolicy
|
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
|
||||||
@@ -19,6 +18,8 @@ import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@@ -33,6 +34,9 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
private val renameStorageUseCase: RenameStorageUseCase,
|
private val renameStorageUseCase: RenameStorageUseCase,
|
||||||
private val logger: ILogger
|
private val logger: ILogger
|
||||||
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
|
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
|
||||||
|
private val _messages = MutableSharedFlow<String>()
|
||||||
|
val messages: SharedFlow<String> = _messages
|
||||||
|
|
||||||
private var _taskCount: Int = 0
|
private var _taskCount: Int = 0
|
||||||
private var tasksCount
|
private var tasksCount
|
||||||
get() = _taskCount
|
get() = _taskCount
|
||||||
@@ -114,37 +118,105 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val runningStorages = mutableSetOf<IStorageInfo>()
|
private val runningStorages = mutableSetOf<java.util.UUID>()
|
||||||
fun enableEncryptionAndOpenStorage(storage: IStorageInfo) {
|
fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boolean) {
|
||||||
if(runningStorages.contains(storage))
|
val id = storage.uuid
|
||||||
|
if (runningStorages.contains(id))
|
||||||
return
|
return
|
||||||
tasksCount++
|
tasksCount++
|
||||||
runningStorages.add(storage)
|
runningStorages.add(id)
|
||||||
val key = EncryptKey("Hello")
|
val key = EncryptKey(password)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
manageStoragesEncryptionUseCase.enableEncryption(storage, key, false)
|
when (manageStoragesEncryptionUseCase.canEncrypt(storage)) {
|
||||||
manageStoragesEncryptionUseCase.openStorage(storage, key)
|
ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> {
|
||||||
|
manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath)
|
||||||
|
manageStoragesEncryptionUseCase.openStorage(storage, key, true)
|
||||||
|
_messages.emit("Encryption enabled")
|
||||||
|
}
|
||||||
|
ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> {
|
||||||
|
_messages.emit("Storage is already encrypted")
|
||||||
|
}
|
||||||
|
ManageStoragesEncryptionUseCase.CanEncryptResult.StorageIsNotEmpty -> {
|
||||||
|
_messages.emit("Storage is not empty")
|
||||||
|
}
|
||||||
|
ManageStoragesEncryptionUseCase.CanEncryptResult.StorageStateUnknown -> {
|
||||||
|
_messages.emit("Cannot determine whether storage is empty")
|
||||||
|
}
|
||||||
|
ManageStoragesEncryptionUseCase.CanEncryptResult.UnsupportedStorageType -> {
|
||||||
|
_messages.emit("Unsupported storage type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_messages.emit(e.message ?: "Failed to enable encryption")
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
runningStorages.remove(storage)
|
runningStorages.remove(id)
|
||||||
tasksCount--
|
tasksCount--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openEncryptedStorage(storage: IStorageInfo, password: String, rememberPassword: Boolean) {
|
||||||
|
val id = storage.uuid
|
||||||
|
if (runningStorages.contains(id)) return
|
||||||
|
tasksCount++
|
||||||
|
runningStorages.add(id)
|
||||||
|
val key = EncryptKey(password)
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_messages.emit(e.message ?: "Failed to open encrypted storage")
|
||||||
|
} finally {
|
||||||
|
runningStorages.remove(id)
|
||||||
|
tasksCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeEncryptedStorage(storage: IStorageInfo) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
manageStoragesEncryptionUseCase.closeStorage(storage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_messages.emit(e.message ?: "Failed to close encrypted storage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableEncryption(storage: IStorageInfo) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
manageStoragesEncryptionUseCase.disableEncryption(storage)
|
||||||
|
_messages.emit("Encryption disabled")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_messages.emit(e.message ?: "Failed to disable encryption")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun rename(storage: IStorageInfo, newName: String) {
|
fun rename(storage: IStorageInfo, newName: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
renameStorageUseCase.rename(storage, newName)
|
renameStorageUseCase.rename(storage, newName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(
|
fun remove(storage: IStorageInfo) {
|
||||||
storage: IStorageInfo,
|
|
||||||
policy: StorageDeletionPolicy = StorageDeletionPolicy.REMOVE_PHYSICAL,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
removeStorageUseCase.remove(storage, policy)
|
removeStorageUseCase.remove(storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getStorageStatus(storage: IStorageInfo): String {
|
||||||
|
val encrypted = storage.metaInfo.value.encInfo != null
|
||||||
|
if (!encrypted) return "Not encrypted"
|
||||||
|
val opened = isEncryptionSessionOpen(storage)
|
||||||
|
return if (opened) "Encrypted (opened)" else "Encrypted (closed)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEncryptionSessionOpen(storage: IStorageInfo): Boolean {
|
||||||
|
val openedMap = getOpenedStoragesUseCase.openedStorages.value
|
||||||
|
return openedMap.containsKey(storage.uuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
<string name="show_storage_item_menu">Show storage item menu</string>
|
<string name="show_storage_item_menu">Show storage item menu</string>
|
||||||
<string name="rename">Rename</string>
|
<string name="rename">Rename</string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
|
<string name="encrypt">Encrypt</string>
|
||||||
<string name="new_name_title">New name</string>
|
<string name="new_name_title">New name</string>
|
||||||
<string name="remove_confirmation_dialog">Delete storage "%1$s"?</string>
|
<string name="remove_confirmation_dialog">Delete storage "%1$s"?</string>
|
||||||
|
<string name="storage_lock_actions">Storage encryption actions</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user