fix(vault): исправил шифрование, meta Yandex и enc-meta при первом открытии
Remember key после encrypt, мягкий auto-open в UnlockManager, StorageMetaLoadState без затирания meta на сетевых ошибках, фильтр storages в YandexVault и создание .enc-meta при FileNotFound.
This commit is contained in:
@@ -58,11 +58,17 @@ class UnlockManager(
|
||||
allStorages.removeAt(allStorages.size - 1)
|
||||
allStorages.add(encStorage)
|
||||
}
|
||||
catch (_: Exception) {
|
||||
// ключ не подошёл
|
||||
catch (e: WallencException.Storage.IncorrectKey) {
|
||||
keysToRemove.add(key)
|
||||
allStorages.removeAt(allStorages.size - 1)
|
||||
}
|
||||
catch (_: WallencException.Storage.EncInfoMissing) {
|
||||
keysToRemove.add(key)
|
||||
allStorages.removeAt(allStorages.size - 1)
|
||||
}
|
||||
catch (_: Exception) {
|
||||
allStorages.removeAt(allStorages.size - 1)
|
||||
}
|
||||
}
|
||||
keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи
|
||||
_openedStorages.value = map.toMap()
|
||||
@@ -104,6 +110,21 @@ class UnlockManager(
|
||||
return UUID(bb.long, bb.long)
|
||||
}
|
||||
|
||||
override suspend fun rememberKey(storage: IStorage, key: EncryptKey) = withContext(ioDispatcher) {
|
||||
mutex.withLock {
|
||||
val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing()
|
||||
if (!Encryptor.checkKey(key, encInfo)) {
|
||||
throw WallencException.Storage.IncorrectKey()
|
||||
}
|
||||
keymapRepository.add(
|
||||
StorageKeyMap(
|
||||
sourceUuid = storage.uuid,
|
||||
key = key,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun open(
|
||||
storage: IStorage,
|
||||
key: EncryptKey,
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.domain.vault.storages.common
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
|
||||
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
@@ -39,6 +40,10 @@ abstract class BaseStorage(
|
||||
final override val metaInfo: StateFlow<IStorageMetaInfo>
|
||||
get() = _metaInfo
|
||||
|
||||
private val _metaLoadState = MutableStateFlow(StorageMetaLoadState.Loading)
|
||||
final override val metaLoadState: StateFlow<StorageMetaLoadState>
|
||||
get() = _metaLoadState
|
||||
|
||||
final override val size: StateFlow<Long?>
|
||||
get() = accessor.size
|
||||
|
||||
@@ -71,32 +76,39 @@ abstract class BaseStorage(
|
||||
}
|
||||
|
||||
private suspend fun readMetaInfo() = withContext(ioDispatcher) {
|
||||
val meta = try {
|
||||
accessor.openReadSystemFile(metaInfoFileName).use { input ->
|
||||
val bytes = input.readBytes()
|
||||
when {
|
||||
bytes.isEmpty() -> {
|
||||
val default = CommonStorageMetaInfo()
|
||||
updateMetaInfo(default)
|
||||
default
|
||||
}
|
||||
else -> try {
|
||||
jackson.readValue(bytes, CommonStorageMetaInfo::class.java)
|
||||
} catch (_: Exception) {
|
||||
// Битый JSON — не перезаписываем файл на диске
|
||||
CommonStorageMetaInfo()
|
||||
}
|
||||
val (meta, state) = loadMetaFromDisk()
|
||||
_metaInfo.value = meta
|
||||
_metaLoadState.value = state
|
||||
}
|
||||
|
||||
private suspend fun loadMetaFromDisk(): Pair<IStorageMetaInfo, StorageMetaLoadState> {
|
||||
return try {
|
||||
val bytes = accessor.openReadSystemFile(metaInfoFileName).use { it.readBytes() }
|
||||
when {
|
||||
bytes.isEmpty() -> {
|
||||
val default = CommonStorageMetaInfo()
|
||||
updateMetaInfo(default)
|
||||
default to StorageMetaLoadState.Ready
|
||||
}
|
||||
else -> try {
|
||||
jackson.readValue(bytes, CommonStorageMetaInfo::class.java) to StorageMetaLoadState.Ready
|
||||
} catch (_: Exception) {
|
||||
CommonStorageMetaInfo() to StorageMetaLoadState.Unavailable
|
||||
}
|
||||
}
|
||||
} catch (_: WallencException.Storage.FileNotFound) {
|
||||
val default = CommonStorageMetaInfo()
|
||||
updateMetaInfo(default)
|
||||
default
|
||||
default to StorageMetaLoadState.Ready
|
||||
} catch (_: Exception) {
|
||||
// Сеть/IO — оставляем дефолт в памяти, существующий файл не трогаем
|
||||
CommonStorageMetaInfo()
|
||||
CommonStorageMetaInfo() to StorageMetaLoadState.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requireMetaReady() {
|
||||
if (_metaLoadState.value != StorageMetaLoadState.Ready) {
|
||||
throw WallencException.Storage.NotAvailable()
|
||||
}
|
||||
_metaInfo.value = meta
|
||||
}
|
||||
|
||||
private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) {
|
||||
@@ -108,6 +120,7 @@ abstract class BaseStorage(
|
||||
}
|
||||
|
||||
final override suspend fun rename(newName: String) = withContext(ioDispatcher) {
|
||||
requireMetaReady()
|
||||
val cur = metaInfo.value
|
||||
updateMetaInfo(
|
||||
CommonStorageMetaInfo(
|
||||
@@ -118,6 +131,7 @@ abstract class BaseStorage(
|
||||
}
|
||||
|
||||
final override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) {
|
||||
requireMetaReady()
|
||||
val cur = metaInfo.value
|
||||
updateMetaInfo(
|
||||
CommonStorageMetaInfo(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.domain.vault.storages.encrypt
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||
import com.github.nullptroma.wallenc.domain.vault.storages.common.readSystemFileBytesOrEmpty
|
||||
import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosed
|
||||
import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosing
|
||||
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
|
||||
@@ -28,6 +30,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
@@ -272,7 +275,11 @@ class EncryptedStorageAccessor(
|
||||
|
||||
override suspend fun openReadSystemFile(name: String): InputStream = scope.run {
|
||||
val path = Path(systemHiddenDirName, name).pathString
|
||||
openRead(path)
|
||||
try {
|
||||
openRead(path)
|
||||
} catch (_: WallencException.Storage.FileNotFound) {
|
||||
ByteArrayInputStream(ByteArray(0))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openWriteSystemFile(name: String): OutputStream = scope.run {
|
||||
@@ -284,7 +291,7 @@ class EncryptedStorageAccessor(
|
||||
}
|
||||
|
||||
override suspend fun readSyncJournal(): List<StorageSyncJournalEntry> {
|
||||
val bytes = openReadSystemFile(SYNC_JOURNAL_FILENAME).use { it.readBytes() }
|
||||
val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_JOURNAL_FILENAME) }
|
||||
if (bytes.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
@@ -316,7 +323,7 @@ class EncryptedStorageAccessor(
|
||||
}
|
||||
|
||||
override suspend fun readSyncLock(): StorageSyncLock? {
|
||||
val bytes = openReadSystemFile(SYNC_LOCK_FILENAME).use { it.readBytes() }
|
||||
val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_LOCK_FILENAME) }
|
||||
if (bytes.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskA
|
||||
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.repository.YandexDiskRepository
|
||||
import com.github.nullptroma.wallenc.domain.vault.storages.yandex.YandexStorage
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
||||
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
|
||||
@@ -132,7 +133,10 @@ class YandexVault(
|
||||
if (attempt > 0) {
|
||||
delay(STORAGE_INIT_RETRY_DELAY_MS * attempt)
|
||||
}
|
||||
if (runCatching { storage.init() }.isSuccess) {
|
||||
if (
|
||||
runCatching { storage.init() }.isSuccess &&
|
||||
storage.metaLoadState.value == StorageMetaLoadState.Ready
|
||||
) {
|
||||
return storage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.domain.datatypes
|
||||
|
||||
enum class StorageMetaLoadState {
|
||||
Loading,
|
||||
Ready,
|
||||
Unavailable,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.domain.interfaces
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -14,6 +15,7 @@ sealed interface IStorageInfo {
|
||||
val numberOfFiles: StateFlow<Int?>
|
||||
val isEmpty: Flow<Boolean?>
|
||||
val metaInfo: StateFlow<IStorageMetaInfo>
|
||||
val metaLoadState: StateFlow<StorageMetaLoadState>
|
||||
val isVirtualStorage: Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ interface IUnlockManager {
|
||||
fun getOpenedStorageKey(uuid: UUID): EncryptKey?
|
||||
|
||||
suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean = true): IStorage
|
||||
/** Сохранить ключ для auto-open без открытия виртуального storage. */
|
||||
suspend fun rememberKey(storage: IStorage, key: EncryptKey)
|
||||
suspend fun close(storage: IStorage)
|
||||
suspend fun close(uuid: UUID)
|
||||
}
|
||||
@@ -46,6 +46,14 @@ class ManageStoragesEncryptionUseCase @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun rememberStorageKey(storage: IStorageInfo, key: EncryptKey) {
|
||||
if (storage is IStorage) {
|
||||
unlockManager.rememberKey(storage, key)
|
||||
return
|
||||
}
|
||||
throw IllegalStateException("Unsupported storage type")
|
||||
}
|
||||
|
||||
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey, rememberPassword: Boolean): IStorageInfo {
|
||||
if (storage is IStorage) return unlockManager.open(storage, key, rememberPassword)
|
||||
throw IllegalStateException("Unsupported storage type")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.usecases.fakes
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
@@ -23,6 +24,8 @@ class FakeStorage(
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isEmpty: Flow<Boolean?> = flowOf(true)
|
||||
override val metaInfo: StateFlow<IStorageMetaInfo> = MutableStateFlow(meta)
|
||||
override val metaLoadState: StateFlow<StorageMetaLoadState> =
|
||||
MutableStateFlow(StorageMetaLoadState.Ready)
|
||||
override val isVirtualStorage: Boolean = false
|
||||
override val accessor: IStorageAccessor = accessorImpl
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ class FakeUnlockManager : IUnlockManager {
|
||||
|
||||
override suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean): IStorage = storage
|
||||
|
||||
override suspend fun rememberKey(storage: IStorage, key: EncryptKey) = Unit
|
||||
|
||||
override suspend fun close(storage: IStorage) = Unit
|
||||
|
||||
override suspend fun close(uuid: UUID) = Unit
|
||||
|
||||
Reference in New Issue
Block a user