Полное управление шифрованием и ключами

This commit is contained in:
2026-04-18 17:36:29 +03:00
parent 3455b91bca
commit db9463c2c6
18 changed files with 484 additions and 128 deletions

View File

@@ -12,7 +12,7 @@ interface IAppDb {
val storageMetaInfoDao: StorageMetaInfoDao
}
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 2, exportSchema = false)
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 3, exportSchema = false)
abstract class AppDb : IAppDb, RoomDatabase() {
abstract override val storageKeyMapDao: StorageKeyMapDao
abstract override val storageMetaInfoDao: StorageMetaInfoDao

View File

@@ -7,16 +7,15 @@ import com.github.nullptroma.wallenc.data.model.StorageKeyMap
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import java.util.UUID
@Entity(tableName = "storage_key_maps", primaryKeys = [ "source_uuid", "dest_uuid" ])
@Entity(tableName = "storage_key_maps")
data class DbStorageKeyMap(
@androidx.room.PrimaryKey
@ColumnInfo(name = "source_uuid") val sourceUuid: UUID,
@ColumnInfo(name = "dest_uuid") val destUuid: UUID,
@ColumnInfo(name = "key") val key: ByteArray
) {
fun toModel(): StorageKeyMap {
return StorageKeyMap(
sourceUuid = sourceUuid,
destUuid = destUuid,
key = EncryptKey(key)
)
}
@@ -28,7 +27,6 @@ data class DbStorageKeyMap(
other as DbStorageKeyMap
if (sourceUuid != other.sourceUuid) return false
if (destUuid != other.destUuid) return false
if (!key.contentEquals(other.key)) return false
return true
@@ -36,7 +34,6 @@ data class DbStorageKeyMap(
override fun hashCode(): Int {
var result = sourceUuid.hashCode()
result = 31 * result + destUuid.hashCode()
result = 31 * result + key.contentHashCode()
return result
}
@@ -45,7 +42,6 @@ data class DbStorageKeyMap(
fun fromModel(keymap: StorageKeyMap): DbStorageKeyMap {
return DbStorageKeyMap(
sourceUuid = keymap.sourceUuid,
destUuid = keymap.destUuid,
key = keymap.key.bytes
)
}

View File

@@ -6,6 +6,5 @@ import java.util.UUID
data class StorageKeyMap(
val sourceUuid: UUID,
val destUuid: UUID,
val key: EncryptKey
)

View File

@@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.UUID
class UnlockManager(
@@ -45,7 +47,7 @@ class UnlockManager(
continue
}
try {
val encStorage = createEncryptedStorage(storage, key.key, key.destUuid)
val encStorage = createEncryptedStorage(storage, key.key, getDestUuid(storage.uuid))
map[storage.uuid] = encStorage
allStorages.removeAt(allStorages.size - 1)
allStorages.add(encStorage)
@@ -72,9 +74,34 @@ class UnlockManager(
)
}
private fun getDestUuid(sourceUuid: UUID): UUID {
return uuid5(
namespace = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"), // URL namespace
name = "$sourceUuid:open"
)
}
private fun uuid5(namespace: UUID, name: String): UUID {
val digest = MessageDigest.getInstance("SHA-1")
val nsBytes = ByteBuffer.allocate(16)
.putLong(namespace.mostSignificantBits)
.putLong(namespace.leastSignificantBits)
.array()
digest.update(nsBytes)
digest.update(name.toByteArray(Charsets.UTF_8))
val hash = digest.digest()
hash[6] = (hash[6].toInt() and 0x0f or 0x50).toByte() // version 5
hash[8] = (hash[8].toInt() and 0x3f or 0x80).toByte() // RFC 4122 variant
val bb = ByteBuffer.wrap(hash, 0, 16)
return UUID(bb.long, bb.long)
}
override suspend fun open(
storage: IStorage,
key: EncryptKey
key: EncryptKey,
rememberPassword: Boolean
): EncryptedStorage = withContext(ioDispatcher) {
return@withContext mutex.withLock {
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
@@ -84,17 +111,18 @@ class UnlockManager(
val opened = _openedStorages.value.toMutableMap()
val cur = opened[storage.uuid]
if (cur != null)
throw Exception("Storage is already open")
return@withLock cur
val keymap = StorageKeyMap(
sourceUuid = storage.uuid,
destUuid = UUID.randomUUID(),
key = key
)
val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid)
val encStorage = createEncryptedStorage(storage, keymap.key, getDestUuid(storage.uuid))
opened[storage.uuid] = encStorage
_openedStorages.value = opened
keymapRepository.add(keymap)
if (rememberPassword) {
keymapRepository.add(keymap)
}
encStorage
}
}
@@ -111,30 +139,21 @@ class UnlockManager(
}
}
// Закрытие отображения по экземпляру (source или decrypted).
// Закрытие только по source-экземпляру.
override suspend fun close(storage: IStorage) {
val opened = _openedStorages.value
val source = opened.entries.firstOrNull {
it.key == storage.uuid || it.value.uuid == storage.uuid
if (opened.containsKey(storage.uuid)) {
close(storage.uuid)
}
if (source != null)
close(source.key)
}
private suspend fun closeBySourceUuid(opened: MutableMap<UUID, EncryptedStorage>, sourceUuid: UUID) {
val enc = opened[sourceUuid] ?: return
val childSourceUuid = opened.entries.firstOrNull { it.value.uuid == enc.uuid }?.key
if (childSourceUuid != null) {
closeBySourceUuid(opened, childSourceUuid)
val nestedSourceUuid = enc.uuid
if (nestedSourceUuid != sourceUuid && opened.containsKey(nestedSourceUuid)) {
closeBySourceUuid(opened, nestedSourceUuid)
}
opened.remove(sourceUuid)
enc.dispose()
keymapRepository.delete(
StorageKeyMap(
sourceUuid = sourceUuid,
destUuid = enc.uuid,
key = EncryptKey("")
)
)
}
}

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
@@ -35,6 +36,8 @@ class EncryptedStorage private constructor(
get() = accessor.size
override val numberOfFiles: StateFlow<Int?>
get() = accessor.numberOfFiles
override val isEmpty: Flow<Boolean?>
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
CommonStorageMetaInfo()
@@ -102,7 +105,7 @@ class EncryptedStorage private constructor(
)
}
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = scope.run {
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = scope.run {
val curMeta = metaInfo.value
updateMetaInfo(
CommonStorageMetaInfo(
@@ -112,6 +115,20 @@ class EncryptedStorage private constructor(
)
}
override suspend fun clearAllContent() = scope.run {
val files = accessor.getAllFiles()
val dirs = accessor.getAllDirs()
val paths = buildList {
addAll(files.map { it.metaInfo.path })
addAll(dirs.map { it.metaInfo.path })
}
.filter { it != "/" && it.isNotBlank() }
.sortedByDescending { it.length }
for (path in paths) {
accessor.delete(path)
}
}
override fun dispose() {
accessor.dispose()
job.cancel()

View File

@@ -7,8 +7,10 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.UUID
@@ -23,6 +25,8 @@ class LocalStorage(
get() = accessor.size
override val numberOfFiles: StateFlow<Int?>
get() = accessor.numberOfFiles
override val isEmpty: Flow<Boolean?>
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
CommonStorageMetaInfo()
@@ -82,7 +86,7 @@ class LocalStorage(
))
}
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = withContext(ioDispatcher) {
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) {
val curMeta = metaInfo.value
updateMetaInfo(CommonStorageMetaInfo(
encInfo = encInfo,
@@ -90,6 +94,20 @@ class LocalStorage(
))
}
override suspend fun clearAllContent() = withContext(ioDispatcher) {
val files = accessor.getAllFiles()
val dirs = accessor.getAllDirs()
val paths = buildList {
addAll(files.map { it.metaInfo.path })
addAll(dirs.map { it.metaInfo.path })
}
.filter { it != "/" && it.isNotBlank() }
.sortedByDescending { it.length }
for (path in paths) {
accessor.delete(path)
}
}
companion object {
const val STORAGE_INFO_FILE_POSTFIX = ".storage-info"
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }

View File

@@ -494,10 +494,15 @@ class LocalStorageAccessor(
}
override suspend fun delete(path: String) = withContext(ioDispatcher) {
if (path == "/" || path.isBlank()) {
throw IllegalArgumentException("Deleting root path is forbidden")
}
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
if (pair != null) {
pair.file.delete()
if (pair.file.isDirectory) pair.file.deleteRecursively()
else pair.file.delete()
pair.metaFile.delete()
scanSizeAndNumOfFiles()
}
}