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