Dispose для EncryptedStorage, скрытые системные файлы для LocalStorage

This commit is contained in:
Roman Pytkov
2025-01-05 15:50:14 +03:00
parent abbaa62412
commit ab82aa6e7b
8 changed files with 67 additions and 38 deletions

View File

@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.data.vaults.local
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -20,7 +21,8 @@ class LocalStorage(
get() = accessor.numberOfFiles get() = accessor.numberOfFiles
override val isAvailable: StateFlow<Boolean> override val isAvailable: StateFlow<Boolean>
get() = accessor.isAvailable get() = accessor.isAvailable
override val accessor = LocalStorageAccessor(absolutePath, ioDispatcher) private val _accessor = LocalStorageAccessor(absolutePath, ioDispatcher)
override val accessor: IStorageAccessor = _accessor
private val _encInfo = MutableStateFlow<StorageEncryptionInfo?>(null) private val _encInfo = MutableStateFlow<StorageEncryptionInfo?>(null)
override val encInfo: StateFlow<StorageEncryptionInfo?> override val encInfo: StateFlow<StorageEncryptionInfo?>
@@ -31,14 +33,12 @@ class LocalStorage(
private val encInfoFileName: String = "$uuid$ENC_INFO_FILE_POSTFIX" private val encInfoFileName: String = "$uuid$ENC_INFO_FILE_POSTFIX"
suspend fun init() { suspend fun init() {
accessor.init() _accessor.init()
readEncInfo() readEncInfo()
} }
private suspend fun readEncInfo() { private suspend fun readEncInfo() {
accessor.touchFile(encInfoFileName) val reader = _accessor.openReadSystemFile(encInfoFileName)
accessor.setHidden(encInfoFileName, true)
val reader = accessor.openRead(encInfoFileName)
var enc: StorageEncryptionInfo? = null var enc: StorageEncryptionInfo? = null
try { try {
enc = _jackson.readValue(reader, StorageEncryptionInfo::class.java) enc = _jackson.readValue(reader, StorageEncryptionInfo::class.java)
@@ -51,23 +51,13 @@ class LocalStorage(
isEncrypted = false, isEncrypted = false,
encryptedTestData = null encryptedTestData = null
) )
val writer = accessor.openWrite(encInfoFileName) setEncInfo(enc)
try {
_jackson.writeValue(writer, enc)
}
catch (e: Exception) {
TODO("Это никогда не должно произойти")
}
writer.close()
} }
_encInfo.value = enc _encInfo.value = enc
} }
suspend fun setEncInfo(enc: StorageEncryptionInfo) { suspend fun setEncInfo(enc: StorageEncryptionInfo) {
accessor.touchFile(encInfoFileName) val writer = _accessor.openWriteSystemFile(encInfoFileName)
accessor.setHidden(encInfoFileName, true)
val writer = accessor.openWrite(encInfoFileName)
try { try {
_jackson.writeValue(writer, enc) _jackson.writeValue(writer, enc)
} }

View File

@@ -85,8 +85,10 @@ class LocalStorageAccessor(
val children = dir.listFiles() val children = dir.listFiles()
if (children != null) { if (children != null) {
// вызвать коллбек для каждого элемента директории // вызвать коллбек для каждого элемента директории
for (child in children) { for (child in children) {
if(child.name != SYSTEM_HIDDEN_DIRNAME)
callback(child) callback(child)
} }
@@ -96,7 +98,7 @@ class LocalStorageAccessor(
if (maxDepth != 0) { if (maxDepth != 0) {
val nextMaxDepth = if (maxDepth > 0) maxDepth - 1 else maxDepth val nextMaxDepth = if (maxDepth > 0) maxDepth - 1 else maxDepth
for (child in children) { for (child in children) {
if (child.isDirectory) { if (child.isDirectory && child.name != SYSTEM_HIDDEN_DIRNAME) {
scanFileSystem(child, nextMaxDepth, callback, false) scanFileSystem(child, nextMaxDepth, callback, false)
} }
} }
@@ -488,7 +490,33 @@ class LocalStorageAccessor(
writeMeta(pair.metaFile, newMeta) writeMeta(pair.metaFile, newMeta)
} }
suspend fun openReadSystemFile(name: String): InputStream = withContext(ioDispatcher) {
val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
val path = dirPath.resolve(name)
val file = path.toFile()
if(!file.exists()) {
Files.createDirectories(dirPath)
file.createNewFile()
}
return@withContext file.inputStream()
}
suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) {
val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
val path = dirPath.resolve(name)
val file = path.toFile()
if(!file.exists()) {
Files.createDirectories(dirPath)
file.createNewFile()
}
return@withContext file.outputStream()
}
companion object { companion object {
// Файлы, которые можно использовать для чтения и записи, но не отображаются в хранилище
private const val SYSTEM_HIDDEN_DIRNAME = "wallenc-local-storage-meta-dir"
private const val META_INFO_POSTFIX = ".wallenc-meta" private const val META_INFO_POSTFIX = ".wallenc-meta"
private const val DATA_PAGE_LENGTH = 10 private const val DATA_PAGE_LENGTH = 10
private val _jackson = jacksonObjectMapper().apply { findAndRegisterModules() } private val _jackson = jacksonObjectMapper().apply { findAndRegisterModules() }

View File

@@ -52,7 +52,7 @@ class EncryptedStorageAccessor(
private val _dirsUpdates = MutableSharedFlow<DataPackage<List<IDirectory>>>() private val _dirsUpdates = MutableSharedFlow<DataPackage<List<IDirectory>>>()
override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates
private val _secretKey = SecretKeySpec(key.to32Bytes(), "AES") private var _secretKey: SecretKeySpec? = SecretKeySpec(key.to32Bytes(), "AES")
init { init {
collectSourceState() collectSourceState()
@@ -123,6 +123,8 @@ class EncryptedStorageAccessor(
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
private fun encryptString(str: String): String { private fun encryptString(str: String): String {
if(_secretKey == null)
throw Exception("Object was disposed")
val cipher = Cipher.getInstance(AES_SETTINGS) val cipher = Cipher.getInstance(AES_SETTINGS)
val iv = IvParameterSpec(Random.nextBytes(IV_LEN)) val iv = IvParameterSpec(Random.nextBytes(IV_LEN))
cipher.init(Cipher.ENCRYPT_MODE, _secretKey, iv) cipher.init(Cipher.ENCRYPT_MODE, _secretKey, iv)
@@ -133,6 +135,8 @@ class EncryptedStorageAccessor(
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
private fun decryptString(str: String): String { private fun decryptString(str: String): String {
if(_secretKey == null)
throw Exception("Object was disposed")
val cipher = Cipher.getInstance(AES_SETTINGS) val cipher = Cipher.getInstance(AES_SETTINGS)
val bytesToDecrypt = Base64.Default.decode(str.replace(".", "/")) val bytesToDecrypt = Base64.Default.decode(str.replace(".", "/"))
val iv = IvParameterSpec(bytesToDecrypt.take(IV_LEN).toByteArray()) val iv = IvParameterSpec(bytesToDecrypt.take(IV_LEN).toByteArray())
@@ -226,6 +230,8 @@ class EncryptedStorageAccessor(
} }
override suspend fun openWrite(path: String): OutputStream { override suspend fun openWrite(path: String): OutputStream {
if(_secretKey == null)
throw Exception("Object was disposed")
val stream = source.openWrite(encryptPath(path)) val stream = source.openWrite(encryptPath(path))
val iv = IvParameterSpec(Random.nextBytes(IV_LEN)) val iv = IvParameterSpec(Random.nextBytes(IV_LEN))
stream.write(iv.iv) // Запись инициализационного вектора сырой файл stream.write(iv.iv) // Запись инициализационного вектора сырой файл
@@ -235,6 +241,8 @@ class EncryptedStorageAccessor(
} }
override suspend fun openRead(path: String): InputStream { override suspend fun openRead(path: String): InputStream {
if(_secretKey == null)
throw Exception("Object was disposed")
val stream = source.openRead(encryptPath(path)) val stream = source.openRead(encryptPath(path))
val ivBytes = ByteArray(IV_LEN) // Буфер для 16 байт IV val ivBytes = ByteArray(IV_LEN) // Буфер для 16 байт IV
val bytesRead = stream.read(ivBytes) // Чтение IV вектора val bytesRead = stream.read(ivBytes) // Чтение IV вектора
@@ -253,7 +261,7 @@ class EncryptedStorageAccessor(
override fun dispose() { override fun dispose() {
_job.cancel() _job.cancel()
// TODO сделать удаление ключа, чтобы нельзя было вызвать ни один из методов _secretKey = null
} }

View File

@@ -47,9 +47,6 @@ android {
} }
dependencies { dependencies {
// Timber
implementation(libs.timber)
implementation(libs.navigation) implementation(libs.navigation)
implementation(libs.navigation.hilt.compose) implementation(libs.navigation.hilt.compose)

View File

@@ -0,0 +1,7 @@
package com.github.nullptroma.wallenc.presentation.extensions
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
fun IStorageInfo.toPrintable(): String {
return "{ uuid: $uuid, enc: ${encInfo.value} }"
}

View File

@@ -1,5 +1,6 @@
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@@ -39,19 +40,21 @@ fun LocalVaultScreen(modifier: Modifier = Modifier,
}) { innerPadding -> }) { innerPadding ->
LazyColumn(modifier = Modifier.padding(innerPadding)) { LazyColumn(modifier = Modifier.padding(innerPadding)) {
items(uiState.storagesList) { items(uiState.storagesList) {
Card(modifier = Modifier.pointerInput(Unit) { Card(modifier = Modifier.clickable { }.pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { _ -> viewModel.printAllFilesToLog(it) } onTap = { _ -> viewModel.printStorageInfoToLog(it) }
) )
}) { }) {
val available by it.isAvailable.collectAsStateWithLifecycle() val available by it.isAvailable.collectAsStateWithLifecycle()
val numOfFiles by it.numberOfFiles.collectAsStateWithLifecycle() val numOfFiles by it.numberOfFiles.collectAsStateWithLifecycle()
val size by it.size.collectAsStateWithLifecycle() val size by it.size.collectAsStateWithLifecycle()
val enc by it.encInfo.collectAsStateWithLifecycle()
Column { Column {
Text(it.uuid.toString()) Text(it.uuid.toString())
Text("IsAvailable: $available") Text("IsAvailable: $available")
Text("Files: $numOfFiles") Text("Files: $numOfFiles")
Text("Size: $size") Text("Size: $size")
Text("Enc: $enc")
} }
} }
} }

View File

@@ -3,13 +3,14 @@ package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.va
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory 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.IStorageInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
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.viewmodel.ViewModelBase import com.github.nullptroma.wallenc.presentation.viewmodel.ViewModelBase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@@ -17,6 +18,7 @@ import kotlin.system.measureTimeMillis
class LocalVaultViewModel @Inject constructor( class LocalVaultViewModel @Inject constructor(
private val _manageLocalVaultUseCase: ManageLocalVaultUseCase, private val _manageLocalVaultUseCase: ManageLocalVaultUseCase,
private val _storageFileManagementUseCase: StorageFileManagementUseCase, private val _storageFileManagementUseCase: StorageFileManagementUseCase,
private val logger: ILogger
) : ) :
ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) { ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) {
init { init {
@@ -30,7 +32,7 @@ class LocalVaultViewModel @Inject constructor(
} }
} }
fun printAllFilesToLog(storage: IStorageInfo) { fun printStorageInfoToLog(storage: IStorageInfo) {
_storageFileManagementUseCase.setStorage(storage) _storageFileManagementUseCase.setStorage(storage)
viewModelScope.launch { viewModelScope.launch {
val files: List<IFile> val files: List<IFile>
@@ -40,14 +42,13 @@ class LocalVaultViewModel @Inject constructor(
dirs = _storageFileManagementUseCase.getAllDirs() dirs = _storageFileManagementUseCase.getAllDirs()
} }
for (file in files) { for (file in files) {
Timber.tag("Files") logger.debug("Files", file.metaInfo.toString())
Timber.d(file.metaInfo.toString())
} }
for (dir in dirs) { for (dir in dirs) {
Timber.tag("Dirs") logger.debug("Dirs", dir.metaInfo.toString())
Timber.d(dir.metaInfo.toString())
} }
Timber.d("Time: $time ms") logger.debug("Time", "Time: $time ms")
logger.debug("Storage", storage.toPrintable())
} }
} }

View File

@@ -4,15 +4,10 @@ package com.github.nullptroma.wallenc.presentation.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
abstract class ViewModelBase<TState>(initState: TState) : ViewModel() { abstract class ViewModelBase<TState>(initState: TState) : ViewModel() {
private val _state = MutableStateFlow<TState>(initState) private val _state = MutableStateFlow<TState>(initState)
init {
Timber.d("Init ViewModel ${this.javaClass.name}")
}
val state: StateFlow<TState> val state: StateFlow<TState>
get() = _state get() = _state