Dispose для EncryptedStorage, скрытые системные файлы для LocalStorage
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,11 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
val children = dir.listFiles()
|
val children = dir.listFiles()
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
|
|
||||||
// вызвать коллбек для каждого элемента директории
|
// вызвать коллбек для каждого элемента директории
|
||||||
for (child in children) {
|
for (child in children) {
|
||||||
callback(child)
|
if(child.name != SYSTEM_HIDDEN_DIRNAME)
|
||||||
|
callback(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useCallbackForSelf)
|
if (useCallbackForSelf)
|
||||||
@@ -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() }
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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} }"
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user