Опциональное шифрование имён файлов

This commit is contained in:
Пытков Роман
2025-02-08 20:51:28 +03:00
parent da8808a4b9
commit 86b5c6cae2
15 changed files with 278 additions and 95 deletions

View File

@@ -53,12 +53,10 @@ class SingletonModule {
fun provideUnlockManager( fun provideUnlockManager(
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
keyRepo: StorageKeyMapRepository, keyRepo: StorageKeyMapRepository,
metaRepo: StorageMetaInfoRepository,
vaultsManager: IVaultsManager vaultsManager: IVaultsManager
): IUnlockManager { ): IUnlockManager {
return UnlockManager( return UnlockManager(
keymapRepository = keyRepo, keymapRepository = keyRepo,
metaInfoRepository = metaRepo,
ioDispatcher = ioDispatcher, ioDispatcher = ioDispatcher,
vaultsManager = vaultsManager vaultsManager = vaultsManager
) )

View File

@@ -4,6 +4,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
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.RenameStorageUseCase import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import dagger.Module import dagger.Module
@@ -38,4 +39,10 @@ class UseCasesModule {
fun provideRenameStorageUseCase(): RenameStorageUseCase { fun provideRenameStorageUseCase(): RenameStorageUseCase {
return RenameStorageUseCase() return RenameStorageUseCase()
} }
@Provides
@Singleton
fun provideManageStoragesEncryptionUseCase(unlockManager: IUnlockManager): ManageStoragesEncryptionUseCase {
return ManageStoragesEncryptionUseCase(unlockManager)
}
} }

View File

@@ -11,6 +11,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -22,7 +23,6 @@ import java.util.UUID
class UnlockManager( class UnlockManager(
private val keymapRepository: StorageKeyMapRepository, private val keymapRepository: StorageKeyMapRepository,
private val metaInfoRepository: StorageMetaInfoRepository,
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
vaultsManager: IVaultsManager vaultsManager: IVaultsManager
) : IUnlockManager { ) : IUnlockManager {
@@ -36,23 +36,32 @@ class UnlockManager(
vaultsManager.allStorages.collectLatest { vaultsManager.allStorages.collectLatest {
mutex.lock() mutex.lock()
val allKeys = keymapRepository.getAll() val allKeys = keymapRepository.getAll()
val usedKeys = mutableListOf<StorageKeyMap>()
val keysToRemove = mutableListOf<StorageKeyMap>() val keysToRemove = mutableListOf<StorageKeyMap>()
val allStorages = it.associateBy({ it.uuid }, { it }) val allStorages = it.toMutableList()
val map = _openedStorages.value?.toMutableMap() ?: mutableMapOf() val map = _openedStorages.value?.toMutableMap() ?: mutableMapOf()
for(keymap in allKeys) { while(allStorages.size > 0) {
if(map.contains(keymap.sourceUuid)) val storage = allStorages[allStorages.size-1]
val key = allKeys.find { key -> key.sourceUuid == storage.uuid }
if(key == null) {
allStorages.removeAt(allStorages.size - 1)
continue continue
}
try { try {
val storage = allStorages[keymap.sourceUuid] ?: continue val encStorage = createEncryptedStorage(storage, key.key, key.destUuid)
val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid)
map[storage.uuid] = encStorage map[storage.uuid] = encStorage
usedKeys.add(key)
allStorages.removeAt(allStorages.size - 1)
allStorages.add(encStorage)
} }
catch (_: Exception) { catch (_: Exception) {
keysToRemove.add(keymap) // ключ не подошёл
keysToRemove.add(key)
allStorages.removeAt(allStorages.size - 1)
} }
} }
_openedStorages.value = map
keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи
_openedStorages.value = map.toMap()
mutex.unlock() mutex.unlock()
} }
} }
@@ -63,7 +72,6 @@ class UnlockManager(
source = storage, source = storage,
key = key, key = key,
ioDispatcher = ioDispatcher, ioDispatcher = ioDispatcher,
metaInfoProvider = metaInfoRepository.createSingleStorageProvider(uuid),
uuid = uuid uuid = uuid
) )
} }
@@ -71,7 +79,7 @@ class UnlockManager(
override suspend fun open( override suspend fun open(
storage: IStorage, storage: IStorage,
key: EncryptKey key: EncryptKey
) = withContext(ioDispatcher) { ): EncryptedStorage = withContext(ioDispatcher) {
mutex.lock() mutex.lock()
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
if (!Encryptor.checkKey(key, encInfo)) if (!Encryptor.checkKey(key, encInfo))
@@ -92,6 +100,7 @@ class UnlockManager(
_openedStorages.value = opened _openedStorages.value = opened
keymapRepository.add(keymap) keymapRepository.add(keymap)
mutex.unlock() mutex.unlock()
return@withContext encStorage
} }
override suspend fun close(storage: IStorage) = withContext(ioDispatcher) { override suspend fun close(storage: IStorage) = withContext(ioDispatcher) {

View File

@@ -1,5 +1,6 @@
package com.github.nullptroma.wallenc.data.storages.encrypt package com.github.nullptroma.wallenc.data.storages.encrypt
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.nullptroma.wallenc.data.db.app.repository.StorageMetaInfoRepository import com.github.nullptroma.wallenc.data.db.app.repository.StorageMetaInfoRepository
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
@@ -13,24 +14,27 @@ import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
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.util.UUID import java.util.UUID
class EncryptedStorage private constructor( class EncryptedStorage private constructor(
private val source: IStorage, private val source: IStorage,
private val key: EncryptKey, private val key: EncryptKey,
ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
private val metaInfoProvider: StorageMetaInfoRepository.SingleStorageMetaInfoProvider,
override val uuid: UUID = UUID.randomUUID() override val uuid: UUID = UUID.randomUUID()
) : IStorage, DisposableHandle { ) : IStorage, DisposableHandle {
private val job = Job() private val job = Job()
private val scope = CoroutineScope(ioDispatcher + job) private val scope = CoroutineScope(ioDispatcher + job)
private val encInfo = source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO private val encInfo =
source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO
private val metaInfoFileName: String = "${uuid.toString().take(8)}$STORAGE_INFO_FILE_POSTFIX"
override val size: StateFlow<Long?> override val size: StateFlow<Long?>
get() = source.size get() = accessor.size
override val numberOfFiles: StateFlow<Int?> override val numberOfFiles: StateFlow<Int?>
get() = source.numberOfFiles get() = accessor.numberOfFiles
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>( private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
CommonStorageMetaInfo() CommonStorageMetaInfo()
@@ -42,45 +46,64 @@ class EncryptedStorage private constructor(
override val isAvailable: StateFlow<Boolean> override val isAvailable: StateFlow<Boolean>
get() = source.isAvailable get() = source.isAvailable
override val accessor: EncryptedStorageAccessor = override val accessor: EncryptedStorageAccessor =
EncryptedStorageAccessor(source.accessor, encInfo.pathIv, key, scope) EncryptedStorageAccessor(source.accessor, encInfo.pathIv, key, "${uuid.toString().take(8)}$SYSTEM_HIDDEN_DIRNAME_POSTFIX", scope)
private suspend fun init() { private suspend fun init() {
checkKey() checkKey()
readMeta() readMetaInfo()
} }
private fun checkKey() { private fun checkKey() {
if(!Encryptor.checkKey(key, encInfo)) if (!Encryptor.checkKey(key, encInfo))
throw Exception("Incorrect key") // TODO throw Exception("Incorrect key") // TODO
} }
private suspend fun readMeta() = scope.run { private suspend fun readMetaInfo() = scope.run {
var meta = metaInfoProvider.get() var meta: CommonStorageMetaInfo
if(meta == null) { var reader: InputStream? = null
try {
reader = accessor.openReadSystemFile(metaInfoFileName)
meta = jackson.readValue(reader, CommonStorageMetaInfo::class.java)
} catch (e: Exception) {
// чтение не удалось, значит нужно записать файл
meta = CommonStorageMetaInfo() meta = CommonStorageMetaInfo()
metaInfoProvider.set(meta) updateMetaInfo(meta)
} finally {
reader?.close()
}
_metaInfo.value = meta
}
private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = scope.run {
val writer = accessor.openWriteSystemFile(metaInfoFileName)
try {
jackson.writeValue(writer, meta)
} catch (e: Exception) {
throw e
} finally {
writer.close()
} }
_metaInfo.value = meta _metaInfo.value = meta
} }
override suspend fun rename(newName: String) = scope.run { override suspend fun rename(newName: String) = scope.run {
val cur = _metaInfo.value val curMeta = metaInfo.value
val newMeta = CommonStorageMetaInfo( updateMetaInfo(
encInfo = cur.encInfo, CommonStorageMetaInfo(
name = newName encInfo = curMeta.encInfo,
name = newName
)
) )
_metaInfo.value = newMeta
metaInfoProvider.set(newMeta)
} }
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = scope.run { override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = scope.run {
val cur = _metaInfo.value val curMeta = metaInfo.value
val newMeta = CommonStorageMetaInfo( updateMetaInfo(
encInfo = encInfo, CommonStorageMetaInfo(
name = cur.name encInfo = encInfo,
name = curMeta.name
)
) )
_metaInfo.value = newMeta
metaInfoProvider.set(newMeta)
} }
override fun dispose() { override fun dispose() {
@@ -93,24 +116,25 @@ class EncryptedStorage private constructor(
source: IStorage, source: IStorage,
key: EncryptKey, key: EncryptKey,
ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
metaInfoProvider: StorageMetaInfoRepository.SingleStorageMetaInfoProvider,
uuid: UUID = UUID.randomUUID() uuid: UUID = UUID.randomUUID()
): EncryptedStorage = withContext(ioDispatcher) { ): EncryptedStorage = withContext(ioDispatcher) {
val storage = EncryptedStorage( val storage = EncryptedStorage(
source = source, source = source,
key = key, key = key,
ioDispatcher = ioDispatcher, ioDispatcher = ioDispatcher,
metaInfoProvider = metaInfoProvider,
uuid = uuid uuid = uuid
) )
try { try {
storage.init() storage.init()
} } catch (e: Exception) {
catch (e: Exception) {
storage.dispose() storage.dispose()
throw e throw e
} }
return@withContext storage return@withContext storage
} }
private const val SYSTEM_HIDDEN_DIRNAME_POSTFIX = "-enc-dir"
const val STORAGE_INFO_FILE_POSTFIX = ".enc-meta"
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
} }
} }

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.data.storages.encrypt package com.github.nullptroma.wallenc.data.storages.encrypt
import android.util.Log import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosed
import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosing
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo
@@ -16,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -27,12 +29,17 @@ import kotlin.io.path.pathString
class EncryptedStorageAccessor( class EncryptedStorageAccessor(
private val source: IStorageAccessor, private val source: IStorageAccessor,
pathIv: ByteArray, pathIv: ByteArray?,
key: EncryptKey, key: EncryptKey,
private val systemHiddenDirName: String,
private val scope: CoroutineScope private val scope: CoroutineScope
) : IStorageAccessor, DisposableHandle { ) : IStorageAccessor, DisposableHandle {
override val size: StateFlow<Long?> = source.size private val _size = MutableStateFlow<Long?>(null)
override val numberOfFiles: StateFlow<Int?> = source.numberOfFiles override val size: StateFlow<Long?> = _size
private val _numberOfFiles = MutableStateFlow<Int?>(null)
override val numberOfFiles: StateFlow<Int?> = _numberOfFiles
override val isAvailable: StateFlow<Boolean> = source.isAvailable override val isAvailable: StateFlow<Boolean> = source.isAvailable
private val _filesUpdates = MutableSharedFlow<DataPackage<List<IFile>>>() private val _filesUpdates = MutableSharedFlow<DataPackage<List<IFile>>>()
@@ -42,46 +49,70 @@ class EncryptedStorageAccessor(
override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates
private val dataEncryptor = Encryptor(key.toAesKey()) private val dataEncryptor = Encryptor(key.toAesKey())
private val pathEncryptor = EncryptorWithStaticIv(key.toAesKey(), pathIv) private val pathEncryptor: EncryptorWithStaticIv? = if(pathIv != null) EncryptorWithStaticIv(key.toAesKey(), pathIv) else null
private var systemHiddenFiles: List<IFile>? = null
private var systemHiddenFilesIsActual = false
init { init {
collectSourceState() collectSourceState()
for(i in 1..5) {
val orig = "/hello/path/test.txt"
val enc = encryptPath(orig)
val dec = decryptPath(enc)
Log.d("MyTag", "Path $orig to $enc to $dec")
}
} }
private fun collectSourceState() { private fun collectSourceState() {
scope.launch { scope.launch {
launch { launch {
source.filesUpdates.collect { source.filesUpdates.collect {
val files = it.data.map(::decryptEntity) val files = it.data.map(::decryptEntity).filterSystemHiddenFiles()
_filesUpdates.emit(DataPackage( _filesUpdates.emit(
data = files, DataPackage(
isLoading = it.isLoading, data = files,
isError = it.isError isLoading = it.isLoading,
)) isError = it.isError
)
)
} }
} }
launch { launch {
source.dirsUpdates.collect { source.dirsUpdates.collect {
val dirs = it.data.map(::decryptEntity) val dirs = it.data.map(::decryptEntity).filterSystemHiddenDirs()
_dirsUpdates.emit(DataPackage( _dirsUpdates.emit(
data = dirs, DataPackage(
isLoading = it.isLoading, data = dirs,
isError = it.isError isLoading = it.isLoading,
)) isError = it.isError
)
)
}
}
launch {
source.numberOfFiles.collect {
if(it == null)
_numberOfFiles.value = null
else
{
_numberOfFiles.value = it - getSystemFiles().size
}
}
}
launch {
source.size.collect { sourceSize ->
if(sourceSize == null)
_size.value = null
else
{
_size.value = sourceSize - getSystemFiles().sumOf { it.metaInfo.size }
}
} }
} }
} }
} }
private suspend fun getSystemFiles(): List<IFile> {
return source.getFiles(encryptPath(systemHiddenDirName))
}
private fun encryptEntity(file: IFile): IFile { private fun encryptEntity(file: IFile): IFile {
return CommonFile(encryptMeta(file.metaInfo)) return CommonFile(encryptMeta(file.metaInfo))
} }
@@ -119,35 +150,40 @@ class EncryptedStorageAccessor(
} }
private fun encryptPath(pathStr: String): String { private fun encryptPath(pathStr: String): String {
if(pathEncryptor == null)
return pathStr
val path = Path(pathStr) val path = Path(pathStr)
val segments = mutableListOf<String>() val segments = mutableListOf<String>()
for (segment in path) for (segment in path)
segments.add(pathEncryptor.encryptString(segment.pathString)) segments.add(pathEncryptor.encryptString(segment.pathString))
val res = Path("/",*(segments.toTypedArray())) val res = Path("/", *(segments.toTypedArray()))
return res.pathString return res.pathString
} }
private fun decryptPath(pathStr: String): String { private fun decryptPath(pathStr: String): String {
if(pathEncryptor == null)
return pathStr
val path = Path(pathStr) val path = Path(pathStr)
val segments = mutableListOf<String>() val segments = mutableListOf<String>()
for (segment in path) for (segment in path)
segments.add(pathEncryptor.decryptString(segment.pathString)) segments.add(pathEncryptor.decryptString(segment.pathString))
val res = Path("/",*(segments.toTypedArray())) val res = Path("/", *(segments.toTypedArray()))
return res.pathString return res.pathString
} }
override suspend fun getAllFiles(): List<IFile> { override suspend fun getAllFiles(): List<IFile> {
return source.getAllFiles().map(::decryptEntity) return source.getAllFiles().map(::decryptEntity).filterSystemHiddenFiles()
} }
override suspend fun getFiles(path: String): List<IFile> { override suspend fun getFiles(path: String): List<IFile> {
return source.getFiles(encryptPath(path)).map(::decryptEntity) return source.getFiles(encryptPath(path)).map(::decryptEntity).filterSystemHiddenFiles()
} }
override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> { override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> {
val flow = source.getFilesFlow(encryptPath(path)).map { val flow = source.getFilesFlow(encryptPath(path)).map {
DataPackage( DataPackage(
data = it.data.map(::decryptEntity), data = it.data.map(::decryptEntity).filterSystemHiddenFiles(),
isLoading = it.isLoading, isLoading = it.isLoading,
isError = it.isError isError = it.isError
) )
@@ -156,17 +192,18 @@ class EncryptedStorageAccessor(
} }
override suspend fun getAllDirs(): List<IDirectory> { override suspend fun getAllDirs(): List<IDirectory> {
return source.getAllDirs().map(::decryptEntity) return source.getAllDirs().map(::decryptEntity).filterSystemHiddenDirs()
} }
override suspend fun getDirs(path: String): List<IDirectory> { override suspend fun getDirs(path: String): List<IDirectory> {
return source.getDirs(encryptPath(path)).map(::decryptEntity) return source.getDirs(encryptPath(path)).map(::decryptEntity).filterSystemHiddenDirs()
} }
override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> { override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> {
val flow = source.getDirsFlow(encryptPath(path)).map { val flow = source.getDirsFlow(encryptPath(path)).map {
DataPackage( DataPackage(
data = it.data.map(::decryptEntity), // включать все папки, кроме системной
data = it.data.map(::decryptEntity).filterSystemHiddenDirs(),
isLoading = it.isLoading, isLoading = it.isLoading,
isError = it.isError isError = it.isError
) )
@@ -220,4 +257,33 @@ class EncryptedStorageAccessor(
dataEncryptor.dispose() dataEncryptor.dispose()
} }
suspend fun openReadSystemFile(name: String): InputStream = scope.run {
val path = Path(systemHiddenDirName, name).pathString
return@run openRead(path)
}
suspend fun openWriteSystemFile(name: String): OutputStream = scope.run {
val path = Path(systemHiddenDirName, name).pathString
systemHiddenFilesIsActual = false
return@run openWrite(path).onClosing {
systemHiddenFilesIsActual = false
}
}
private fun Iterable<IFile>.filterSystemHiddenFiles(): List<IFile> {
return this.filter { file ->
!file.metaInfo.path.contains(
systemHiddenDirName
)
}
}
private fun Iterable<IDirectory>.filterSystemHiddenDirs(): List<IDirectory> {
return this.filter { dir ->
!dir.metaInfo.path.contains(
systemHiddenDirName
)
}
}
} }

View File

@@ -3,7 +3,7 @@ package com.github.nullptroma.wallenc.data.storages.local
import com.fasterxml.jackson.core.JacksonException import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClose import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosed
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo
@@ -433,6 +433,8 @@ class LocalStorageAccessor(
if (file.exists() && file.isDirectory) { if (file.exists() && file.isDirectory) {
throw Exception("Что то пошло не так") // TODO throw Exception("Что то пошло не так") // TODO
} else if(!file.exists()) { } else if(!file.exists()) {
val parent = Path(storagePath).parent
createDir(parent.pathString)
file.createNewFile() file.createNewFile()
val cur = _numberOfFiles.value val cur = _numberOfFiles.value
@@ -441,7 +443,7 @@ class LocalStorageAccessor(
val pair = LocalStorageFilePair.from(_filesystemBasePath, file) val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
?: throw Exception("Что то пошло не так") // TODO ?: throw Exception("Что то пошло не так") // TODO
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant()) val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
writeMeta(pair.metaFile, newMeta) writeMeta(pair.metaFile, newMeta)
_filesUpdates.emit( _filesUpdates.emit(
DataPage( DataPage(
@@ -503,9 +505,10 @@ class LocalStorageAccessor(
touchFile(path) touchFile(path)
val pair = LocalStorageFilePair.from(_filesystemBasePath, path) val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Файла нет") // TODO ?: throw Exception("Файла нет") // TODO
return@withContext pair.file.outputStream().onClose { return@withContext pair.file.outputStream().onClosed {
CoroutineScope(ioDispatcher).launch { CoroutineScope(ioDispatcher).launch {
touchFile(path) touchFile(path)
scanSizeAndNumOfFiles()
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import java.io.OutputStream
private class CloseHandledOutputStream( private class CloseHandledOutputStream(
private val stream: OutputStream, private val stream: OutputStream,
private val onClosing: () -> Unit,
private val onClose: () -> Unit private val onClose: () -> Unit
) : OutputStream() { ) : OutputStream() {
@@ -25,6 +26,7 @@ private class CloseHandledOutputStream(
} }
override fun close() { override fun close() {
onClosing()
try { try {
stream.close() stream.close()
} finally { } finally {
@@ -35,6 +37,7 @@ private class CloseHandledOutputStream(
private class CloseHandledInputStream( private class CloseHandledInputStream(
private val stream: InputStream, private val stream: InputStream,
private val onClosing: () -> Unit,
private val onClose: () -> Unit private val onClose: () -> Unit
) : InputStream() { ) : InputStream() {
@@ -59,6 +62,7 @@ private class CloseHandledInputStream(
} }
override fun close() { override fun close() {
onClosing()
try { try {
stream.close() stream.close()
} finally { } finally {
@@ -81,12 +85,20 @@ private class CloseHandledInputStream(
class CloseHandledStreamExtension { class CloseHandledStreamExtension {
companion object { companion object {
fun OutputStream.onClose(callback: ()->Unit): OutputStream { fun OutputStream.onClosed(callback: ()->Unit): OutputStream {
return CloseHandledOutputStream(this, callback) return CloseHandledOutputStream(this, {}, callback)
} }
fun InputStream.onClose(callback: ()->Unit): InputStream { fun InputStream.onClosed(callback: ()->Unit): InputStream {
return CloseHandledInputStream(this, callback) return CloseHandledInputStream(this, {}, callback)
}
fun OutputStream.onClosing(callback: ()->Unit): OutputStream {
return CloseHandledOutputStream(this, callback, {})
}
fun InputStream.onClosing(callback: ()->Unit): InputStream {
return CloseHandledInputStream(this, callback, {})
} }
} }
} }

View File

@@ -2,7 +2,7 @@ package com.github.nullptroma.wallenc.domain.datatypes
data class StorageEncryptionInfo( data class StorageEncryptionInfo(
val encryptedTestData: String, val encryptedTestData: String,
val pathIv: ByteArray val pathIv: ByteArray?
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@@ -74,7 +74,7 @@ class Encryptor(private var secretKey: SecretKey) : DisposableHandle {
} }
override fun dispose() { override fun dispose() {
secretKey.destroy() //secretKey.destroy()
} }
companion object { companion object {
@@ -83,13 +83,13 @@ class Encryptor(private var secretKey: SecretKey) : DisposableHandle {
private const val TEST_DATA_LEN = 512 private const val TEST_DATA_LEN = 512
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
fun generateEncryptionInfo(key: EncryptKey) : StorageEncryptionInfo { fun generateEncryptionInfo(key: EncryptKey, encryptPath: Boolean) : StorageEncryptionInfo {
val encryptor = Encryptor(key.toAesKey()) val encryptor = Encryptor(key.toAesKey())
val testData = ByteArray(TEST_DATA_LEN) val testData = ByteArray(TEST_DATA_LEN)
val encryptedData = encryptor.encryptBytes(testData) val encryptedData = encryptor.encryptBytes(testData)
return StorageEncryptionInfo( return StorageEncryptionInfo(
encryptedTestData = Base64.Default.encode(encryptedData), encryptedTestData = Base64.Default.encode(encryptedData),
pathIv = Random.nextBytes(IV_LEN) pathIv = if(encryptPath) Random.nextBytes(IV_LEN) else null
) )
} }

View File

@@ -10,6 +10,6 @@ interface IUnlockManager {
*/ */
val openedStorages: StateFlow<Map<UUID, IStorage>?> val openedStorages: StateFlow<Map<UUID, IStorage>?>
suspend fun open(storage: IStorage, key: EncryptKey) suspend fun open(storage: IStorage, key: EncryptKey): IStorage
suspend fun close(storage: IStorage) suspend fun close(storage: IStorage)
} }

View File

@@ -17,12 +17,6 @@ class ManageLocalVaultUseCase(private val manager: IVaultsManager, private val u
manager.localVault.createStorage() manager.localVault.createStorage()
} }
suspend fun createStorage(key: EncryptKey) {
val encInfo = Encryptor.generateEncryptionInfo(key)
val storage = manager.localVault.createStorage(encInfo)
unlockManager.open(storage, key)
}
suspend fun remove(storage: IStorageInfo) { suspend fun remove(storage: IStorageInfo) {
when(storage) { when(storage) {
is IStorage -> manager.localVault.remove(storage) is IStorage -> manager.localVault.remove(storage)

View File

@@ -0,0 +1,27 @@
package com.github.nullptroma.wallenc.domain.usecases
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
class ManageStoragesEncryptionUseCase(private val unlockManager: IUnlockManager) {
suspend fun enableEncryption(storage: IStorageInfo, key: EncryptKey, encryptPath: Boolean) {
when(storage) {
is IStorage -> {
if(storage.metaInfo.value.encInfo != null)
throw Exception() // TODO
storage.setEncInfo(Encryptor.generateEncryptionInfo(key, encryptPath))
}
}
}
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey): IStorageInfo {
when(storage) {
is IStorage -> {
return unlockManager.open(storage, key)
}
}
}
}

View File

@@ -19,6 +19,7 @@ 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.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
@@ -57,6 +58,7 @@ 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,
) { ) {
val cur = tree.value val cur = tree.value
val available by cur.isAvailable.collectAsStateWithLifecycle() val available by cur.isAvailable.collectAsStateWithLifecycle()
@@ -66,7 +68,9 @@ fun StorageTree(
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) {
Box(modifier = Modifier.height(IntrinsicSize.Min).zIndex(100f)) { Box(modifier = Modifier
.height(IntrinsicSize.Min)
.zIndex(100f)) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = Modifier modifier = Modifier
@@ -171,6 +175,9 @@ fun StorageTree(
} }
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Button(onClick = { onEncrypt(tree) }, enabled = metaInfo.encInfo == null) {
Text("Encrypt")
}
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -190,7 +197,16 @@ fun StorageTree(
} }
} }
for (i in tree.children ?: listOf()) { for (i in tree.children ?: listOf()) {
StorageTree(Modifier.padding(16.dp, 0.dp, 0.dp, 0.dp).offset(y = (-4).dp), i, onClick, onRename, onRemove) StorageTree(
Modifier
.padding(16.dp, 0.dp, 0.dp, 0.dp)
.offset(y = (-4).dp),
i,
onClick,
onRename,
onRemove,
onEncrypt
)
} }
} }
} }

View File

@@ -51,6 +51,9 @@ fun LocalVaultScreen(
}, },
onRemove = { tree -> onRemove = { tree ->
viewModel.remove(tree.value) viewModel.remove(tree.value)
},
onEncrypt = { tree ->
viewModel.enableEncryptionAndOpenStorage(tree.value)
} }
) )
} }

View File

@@ -9,10 +9,11 @@ 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.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.RenameStorageUseCase import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
import com.github.nullptroma.wallenc.presentation.ViewModelBase import com.github.nullptroma.wallenc.presentation.ViewModelBase
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
@@ -25,6 +26,7 @@ class LocalVaultViewModel @Inject constructor(
private val manageLocalVaultUseCase: ManageLocalVaultUseCase, private val manageLocalVaultUseCase: ManageLocalVaultUseCase,
private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase, private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase,
private val storageFileManagementUseCase: StorageFileManagementUseCase, private val storageFileManagementUseCase: StorageFileManagementUseCase,
private val manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
private val renameStorageUseCase: RenameStorageUseCase, private val renameStorageUseCase: RenameStorageUseCase,
private val logger: ILogger private val logger: ILogger
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) { ) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) {
@@ -50,6 +52,11 @@ class LocalVaultViewModel @Inject constructor(
updateState(newState) updateState(newState)
} }
} }
viewModelScope.launch {
getOpenedStoragesUseCase.openedStorages.collectLatest {
logger.debug("ViewModel", "Collected opened: ${it?.size}")
}
}
} }
fun printStorageInfoToLog(storage: IStorageInfo) { fun printStorageInfoToLog(storage: IStorageInfo) {
@@ -74,7 +81,24 @@ class LocalVaultViewModel @Inject constructor(
fun createStorage() { fun createStorage() {
viewModelScope.launch { viewModelScope.launch {
manageLocalVaultUseCase.createStorage(EncryptKey("Hello")) manageLocalVaultUseCase.createStorage()
}
}
private val runningStorages = mutableSetOf<IStorageInfo>()
fun enableEncryptionAndOpenStorage(storage: IStorageInfo) {
if(runningStorages.contains(storage))
return
runningStorages.add(storage)
val key = EncryptKey("Hello")
viewModelScope.launch {
try {
manageStoragesEncryptionUseCase.enableEncryption(storage, key, false)
manageStoragesEncryptionUseCase.openStorage(storage, key)
}
finally {
runningStorages.remove(storage)
}
} }
} }