fix(vault): исправил шифрование, meta Yandex и enc-meta при первом открытии

Remember key после encrypt, мягкий auto-open в UnlockManager,
StorageMetaLoadState без затирания meta на сетевых ошибках,
фильтр storages в YandexVault и создание .enc-meta при FileNotFound.
This commit is contained in:
2026-05-21 11:05:14 +03:00
parent da8b970078
commit 467ed64426
10 changed files with 95 additions and 25 deletions

View File

@@ -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,

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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
}
}