Skip to content

Commit 85f5e25

Browse files
committed
save changes
1 parent 239fdb2 commit 85f5e25

36 files changed

+2142
-2920
lines changed

Monica for Android/app/src/main/java/takagi/ru/monica/MainActivity.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ import takagi.ru.monica.ui.screens.MonicaPlusScreen
103103
import takagi.ru.monica.ui.screens.PaymentScreen
104104
import takagi.ru.monica.ui.screens.SupportAuthorScreen
105105
import takagi.ru.monica.ui.screens.WebDavBackupScreen
106-
import takagi.ru.monica.ui.screens.KeePassWebDavViewModel
106+
import takagi.ru.monica.ui.screens.KeePassKdbxViewModel
107107
import takagi.ru.monica.ui.theme.MonicaTheme
108108
import takagi.ru.monica.utils.LocaleHelper
109109
import takagi.ru.monica.viewmodel.BankCardViewModel
@@ -344,7 +344,7 @@ fun MonicaApp(
344344
}
345345

346346
// KeePass KDBX 导出/导入
347-
val keePassViewModel = remember { KeePassWebDavViewModel() }
347+
val keePassViewModel = remember { KeePassKdbxViewModel() }
348348

349349
// 本地 KeePass 数据库管理
350350
val localKeePassViewModel: takagi.ru.monica.viewmodel.LocalKeePassViewModel = viewModel {
@@ -454,7 +454,7 @@ fun MonicaContent(
454454
generatorViewModel: GeneratorViewModel,
455455
noteViewModel: takagi.ru.monica.viewmodel.NoteViewModel,
456456
passkeyViewModel: takagi.ru.monica.viewmodel.PasskeyViewModel,
457-
keePassViewModel: KeePassWebDavViewModel,
457+
keePassViewModel: KeePassKdbxViewModel,
458458
localKeePassViewModel: takagi.ru.monica.viewmodel.LocalKeePassViewModel,
459459
securityManager: SecurityManager,
460460
repository: PasswordRepository,
@@ -493,6 +493,7 @@ fun MonicaContent(
493493
// 如果已禁用密码验证,跳过自动锁定
494494
// 使用 currentSettings 和 currentIsFirstTime 确保访问最新值
495495
if (currentSettings.disablePasswordVerification && !currentIsFirstTime) {
496+
viewModel.refreshKeePassFromSourceForCurrentContext()
496497
lastBackgroundTimestamp = null
497498
return@LifecycleEventObserver
498499
}
@@ -520,6 +521,9 @@ fun MonicaContent(
520521
} else if (timeoutMs == null) {
521522
lastBackgroundTimestamp = null
522523
}
524+
if (currentIsAuthenticated || (currentSettings.disablePasswordVerification && !currentIsFirstTime)) {
525+
viewModel.refreshKeePassFromSourceForCurrentContext()
526+
}
523527
}
524528
else -> Unit
525529
}
@@ -540,6 +544,7 @@ fun MonicaContent(
540544
// 当认证状态变化时处理导航
541545
LaunchedEffect(isAuthenticated) {
542546
if (isAuthenticated) {
547+
viewModel.refreshKeePassFromSourceForCurrentContext()
543548
val currentRoute = navController.currentDestination?.route
544549
if (currentRoute == Screen.Login.route) {
545550
navController.navigate(Screen.Main.createRoute()) {

Monica for Android/app/src/main/java/takagi/ru/monica/data/BackupPreferences.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ data class BackupPreferences(
1717
val includeTrash: Boolean = true, // 保留用于向后兼容
1818
val includeTrashAndHistory: Boolean = true, // ✅ 新增:回收站与历史(合并项)
1919
val includeWebDavConfig: Boolean = false, // WebDAV 配置(默认关闭,需手动开启)
20-
val includeLocalKeePass: Boolean = false, // 本地 KeePass 数据库(默认关闭)
21-
val includeKeePassWebDavConfig: Boolean = false // KeePass WebDAV 配置(默认关闭)
20+
val includeLocalKeePass: Boolean = false // 本地 KeePass 数据库(默认关闭)
2221
) {
2322
/**
2423
* 检查是否至少启用了一种内容类型

Monica for Android/app/src/main/java/takagi/ru/monica/data/LocalKeePassDatabase.kt

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,61 @@ enum class KeePassStorageLocation {
1111
EXTERNAL // 外部存储(用户选择的位置)
1212
}
1313

14+
enum class KeePassFormatVersion(val majorVersion: Int) {
15+
KDBX3(3),
16+
KDBX4(4)
17+
}
18+
19+
enum class KeePassCipherAlgorithm {
20+
AES,
21+
CHACHA20,
22+
TWOFISH
23+
}
24+
25+
enum class KeePassKdfAlgorithm {
26+
AES_KDF,
27+
ARGON2D,
28+
ARGON2ID
29+
}
30+
31+
data class KeePassDatabaseCreationOptions(
32+
val formatVersion: KeePassFormatVersion = KeePassFormatVersion.KDBX4,
33+
val cipherAlgorithm: KeePassCipherAlgorithm = KeePassCipherAlgorithm.AES,
34+
val kdfAlgorithm: KeePassKdfAlgorithm = KeePassKdfAlgorithm.ARGON2D,
35+
val transformRounds: Long = 8L,
36+
val memoryBytes: Long = DEFAULT_ARGON_MEMORY_BYTES,
37+
val parallelism: Int = 2
38+
) {
39+
fun normalized(): KeePassDatabaseCreationOptions {
40+
val normalizedVersion = formatVersion
41+
val normalizedCipher = when (normalizedVersion) {
42+
KeePassFormatVersion.KDBX3 -> when (cipherAlgorithm) {
43+
KeePassCipherAlgorithm.CHACHA20 -> KeePassCipherAlgorithm.AES
44+
else -> cipherAlgorithm
45+
}
46+
KeePassFormatVersion.KDBX4 -> cipherAlgorithm
47+
}
48+
val normalizedKdf = when (normalizedVersion) {
49+
KeePassFormatVersion.KDBX3 -> KeePassKdfAlgorithm.AES_KDF
50+
KeePassFormatVersion.KDBX4 -> kdfAlgorithm
51+
}
52+
return copy(
53+
formatVersion = normalizedVersion,
54+
cipherAlgorithm = normalizedCipher,
55+
kdfAlgorithm = normalizedKdf,
56+
transformRounds = transformRounds.coerceAtLeast(1L),
57+
memoryBytes = memoryBytes.coerceIn(MIN_MEMORY_BYTES, MAX_MEMORY_BYTES),
58+
parallelism = parallelism.coerceIn(1, 32)
59+
)
60+
}
61+
62+
companion object {
63+
const val DEFAULT_ARGON_MEMORY_BYTES = 32L * 1024L * 1024L
64+
const val MIN_MEMORY_BYTES = 1L * 1024L * 1024L
65+
const val MAX_MEMORY_BYTES = 1024L * 1024L * 1024L
66+
}
67+
}
68+
1469
/**
1570
* 本地 KeePass 数据库信息
1671
*/
@@ -64,9 +119,51 @@ data class LocalKeePassDatabase(
64119

65120
/** 排序顺序 */
66121
@ColumnInfo(name = "sort_order")
67-
val sortOrder: Int = 0
68-
) {
69-
fun isWebDavDatabase(): Boolean = filePath.startsWith("webdav://")
122+
val sortOrder: Int = 0,
123+
124+
/** KDBX 主版本(3 或 4) */
125+
@ColumnInfo(name = "kdbx_major_version")
126+
val kdbxMajorVersion: Int = KeePassFormatVersion.KDBX4.majorVersion,
127+
128+
/** 外层加密算法 */
129+
@ColumnInfo(name = "cipher_algorithm")
130+
val cipherAlgorithm: String = KeePassCipherAlgorithm.AES.name,
131+
132+
/** 密钥派生函数 */
133+
@ColumnInfo(name = "kdf_algorithm")
134+
val kdfAlgorithm: String = KeePassKdfAlgorithm.ARGON2D.name,
135+
136+
/** 转换次数(Argon2 Iterations 或 AES-KDF Rounds) */
137+
@ColumnInfo(name = "kdf_transform_rounds")
138+
val kdfTransformRounds: Long = 8L,
139+
140+
/** KDF 内存占用(字节) */
141+
@ColumnInfo(name = "kdf_memory_bytes")
142+
val kdfMemoryBytes: Long = KeePassDatabaseCreationOptions.DEFAULT_ARGON_MEMORY_BYTES,
143+
144+
/** KDF 并行度 */
145+
@ColumnInfo(name = "kdf_parallelism")
146+
val kdfParallelism: Int = 2
147+
)
148+
149+
fun LocalKeePassDatabase.toCreationOptions(): KeePassDatabaseCreationOptions {
150+
val parsedVersion = KeePassFormatVersion
151+
.entries
152+
.firstOrNull { it.majorVersion == kdbxMajorVersion }
153+
?: KeePassFormatVersion.KDBX4
154+
val parsedCipher = runCatching { KeePassCipherAlgorithm.valueOf(cipherAlgorithm) }
155+
.getOrDefault(KeePassCipherAlgorithm.AES)
156+
val parsedKdf = runCatching { KeePassKdfAlgorithm.valueOf(kdfAlgorithm) }
157+
.getOrDefault(KeePassKdfAlgorithm.ARGON2D)
158+
159+
return KeePassDatabaseCreationOptions(
160+
formatVersion = parsedVersion,
161+
cipherAlgorithm = parsedCipher,
162+
kdfAlgorithm = parsedKdf,
163+
transformRounds = kdfTransformRounds,
164+
memoryBytes = kdfMemoryBytes,
165+
parallelism = kdfParallelism
166+
).normalized()
70167
}
71168

72169
/**

Monica for Android/app/src/main/java/takagi/ru/monica/data/PasswordDatabase.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import takagi.ru.monica.data.bitwarden.*
2828
BitwardenConflictBackup::class,
2929
BitwardenPendingOperation::class
3030
],
31-
version = 45,
31+
version = 46,
3232
exportSchema = false
3333
)
3434
@TypeConverters(Converters::class)
@@ -1202,6 +1202,39 @@ abstract class PasswordDatabase : RoomDatabase() {
12021202
}
12031203
}
12041204

1205+
/**
1206+
* Migration 45 -> 46:
1207+
* 为 local_keepass_databases 增加 KDBX 配置字段(版本/算法/KDF 参数)。
1208+
*/
1209+
private val MIGRATION_45_46 = object : androidx.room.migration.Migration(45, 46) {
1210+
override fun migrate(database: androidx.sqlite.db.SupportSQLiteDatabase) {
1211+
try {
1212+
android.util.Log.i("PasswordDatabase", "Starting migration 45→46: keepass kdbx config columns")
1213+
database.execSQL(
1214+
"ALTER TABLE local_keepass_databases ADD COLUMN kdbx_major_version INTEGER NOT NULL DEFAULT 4"
1215+
)
1216+
database.execSQL(
1217+
"ALTER TABLE local_keepass_databases ADD COLUMN cipher_algorithm TEXT NOT NULL DEFAULT 'AES'"
1218+
)
1219+
database.execSQL(
1220+
"ALTER TABLE local_keepass_databases ADD COLUMN kdf_algorithm TEXT NOT NULL DEFAULT 'ARGON2D'"
1221+
)
1222+
database.execSQL(
1223+
"ALTER TABLE local_keepass_databases ADD COLUMN kdf_transform_rounds INTEGER NOT NULL DEFAULT 8"
1224+
)
1225+
database.execSQL(
1226+
"ALTER TABLE local_keepass_databases ADD COLUMN kdf_memory_bytes INTEGER NOT NULL DEFAULT 33554432"
1227+
)
1228+
database.execSQL(
1229+
"ALTER TABLE local_keepass_databases ADD COLUMN kdf_parallelism INTEGER NOT NULL DEFAULT 2"
1230+
)
1231+
android.util.Log.i("PasswordDatabase", "Migration 45→46 completed successfully")
1232+
} catch (e: Exception) {
1233+
android.util.Log.e("PasswordDatabase", "Migration 45→46 failed: ${e.message}")
1234+
}
1235+
}
1236+
}
1237+
12051238
fun getDatabase(context: Context): PasswordDatabase {
12061239
return INSTANCE ?: synchronized(this) {
12071240
val instance = Room.databaseBuilder(
@@ -1253,7 +1286,8 @@ abstract class PasswordDatabase : RoomDatabase() {
12531286
MIGRATION_41_42, // Passkey 文件夹/分组字段
12541287
MIGRATION_42_43, // 清理 legacy KeePass WebDAV 残余
12551288
MIGRATION_43_44, // Bitwarden 多库去重与唯一约束
1256-
MIGRATION_44_45 // 密码归档同步元信息
1289+
MIGRATION_44_45, // 密码归档同步元信息
1290+
MIGRATION_45_46 // KeePass KDBX 配置字段
12571291
)
12581292
.build()
12591293
INSTANCE = instance

Monica for Android/app/src/main/java/takagi/ru/monica/ui/components/SelectiveBackupCard.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,9 @@ fun SelectiveBackupCard(
9595
passkeyCount: Int = 0, // ✅ 新增:验证密钥数量
9696
localKeePassCount: Int = 0,
9797
isWebDavConfigured: Boolean = false,
98-
isKeePassWebDavConfigured: Boolean = false,
9998
modifier: Modifier = Modifier
10099
) {
101100
var expanded by remember { mutableStateOf(false) }
102-
// KeePass WebDAV feature has been removed. Keep parameter for call-site compatibility.
103-
if (isKeePassWebDavConfigured) {
104-
// no-op
105-
}
106101

107102
Card(
108103
modifier = modifier.fillMaxWidth()

Monica for Android/app/src/main/java/takagi/ru/monica/ui/components/StorageTargetSelectorCard.kt

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import androidx.compose.ui.unit.dp
5050
import kotlinx.coroutines.flow.flowOf
5151
import takagi.ru.monica.R
5252
import takagi.ru.monica.data.Category
53-
import takagi.ru.monica.data.KeePassStorageLocation
5453
import takagi.ru.monica.data.LocalKeePassDatabase
5554
import takagi.ru.monica.data.PasswordDatabase
5655
import takagi.ru.monica.data.bitwarden.BitwardenFolder
@@ -76,8 +75,9 @@ fun StorageTargetSelectorCard(
7675
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
7776
val context = LocalContext.current
7877
val database = remember { PasswordDatabase.getDatabase(context) }
78+
val availableKeePassDatabases = keepassDatabases
7979

80-
val selectedKeePassDatabase = keepassDatabases.find { it.id == selectedKeePassDatabaseId }
80+
val selectedKeePassDatabase = availableKeePassDatabases.find { it.id == selectedKeePassDatabaseId }
8181
val selectedBitwardenVault = bitwardenVaults.find { it.id == selectedBitwardenVaultId }
8282
val selectedLocalCategory = categories.find { it.id == selectedCategoryId }
8383

@@ -277,6 +277,24 @@ fun StorageTargetSelectorCard(
277277
}
278278
}
279279

280+
availableKeePassDatabases.forEach { keepassDatabase ->
281+
StorageTargetLeafItem(
282+
title = keepassDatabase.name,
283+
subtitle = stringResource(R.string.vault_sync_hint),
284+
icon = Icons.Default.Key,
285+
isSelected = selectedKeePassDatabaseId == keepassDatabase.id,
286+
containerColor = MaterialTheme.colorScheme.primaryContainer,
287+
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
288+
iconColor = MaterialTheme.colorScheme.primary,
289+
onClick = {
290+
onCategorySelected(null)
291+
onBitwardenVaultSelected(null)
292+
onBitwardenFolderSelected(null)
293+
onKeePassDatabaseSelected(keepassDatabase.id)
294+
}
295+
)
296+
}
297+
280298
bitwardenVaults.forEach { vault ->
281299
val vaultExpanded = expandedBitwardenVaultId == vault.id
282300
val folders by (
@@ -339,30 +357,6 @@ fun StorageTargetSelectorCard(
339357
}
340358
}
341359

342-
keepassDatabases.forEach { databaseItem ->
343-
val storageText = if (databaseItem.storageLocation == KeePassStorageLocation.EXTERNAL) {
344-
stringResource(R.string.external_storage)
345-
} else {
346-
stringResource(R.string.internal_storage)
347-
}
348-
349-
StorageTargetLeafItem(
350-
title = databaseItem.name,
351-
subtitle = storageText,
352-
icon = Icons.Default.Key,
353-
isSelected = selectedKeePassDatabaseId == databaseItem.id,
354-
containerColor = MaterialTheme.colorScheme.primaryContainer,
355-
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
356-
iconColor = MaterialTheme.colorScheme.primary,
357-
onClick = {
358-
onKeePassDatabaseSelected(databaseItem.id)
359-
onBitwardenVaultSelected(null)
360-
onBitwardenFolderSelected(null)
361-
expandedBitwardenVaultId = null
362-
localExpanded = false
363-
}
364-
)
365-
}
366360
}
367361
}
368362
}

Monica for Android/app/src/main/java/takagi/ru/monica/ui/components/UnifiedCategoryFilterBottomSheet.kt

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,10 @@ fun UnifiedCategoryFilterBottomSheet(
206206
}
207207
}
208208
LaunchedEffect(keepassDatabases) {
209-
val preferredId = keepassDatabases.firstOrNull { !it.isWebDavDatabase() }?.id
210-
?: keepassDatabases.firstOrNull()?.id
209+
val preferredId = keepassDatabases.firstOrNull()?.id
211210
val isCurrentValid = keepassDatabases.any { it.id == selectedCreateKeePassDbId }
212211
if (!isCurrentValid) {
213212
selectedCreateKeePassDbId = preferredId
214-
} else if (
215-
selectedCreateKeePassDbId != null &&
216-
keepassDatabases.firstOrNull { it.id == selectedCreateKeePassDbId }?.isWebDavDatabase() == true &&
217-
preferredId != null
218-
) {
219-
selectedCreateKeePassDbId = preferredId
220213
}
221214
}
222215

@@ -252,12 +245,12 @@ fun UnifiedCategoryFilterBottomSheet(
252245

253246
val canCreateLocal = onCreateCategoryWithName != null || onCreateCategory != null
254247
val canCreateBitwarden = onCreateBitwardenFolder != null && bitwardenVaults.isNotEmpty()
255-
val canCreateKeePass = onCreateKeePassGroup != null && keepassDatabases.any { !it.isWebDavDatabase() }
256-
val localKeePassDatabases = keepassDatabases.filterNot { it.isWebDavDatabase() }
248+
val canCreateKeePass = onCreateKeePassGroup != null && keepassDatabases.isNotEmpty()
249+
val localKeePassDatabases = keepassDatabases
257250
val localCategoryNodes = remember(categories) { buildLocalCategoryNodes(categories) }
258251

259252
@Composable
260-
fun KeePassDatabaseItems(databases: List<LocalKeePassDatabase>, forceWebDavBadge: Boolean) {
253+
fun KeePassDatabaseItems(databases: List<LocalKeePassDatabase>) {
261254
databases.forEach { database ->
262255
val expanded = keepassExpanded[database.id] ?: false
263256
val groups by (
@@ -288,10 +281,10 @@ fun UnifiedCategoryFilterBottomSheet(
288281
onClick = { onSelect(UnifiedCategoryFilterSelection.KeePassDatabaseFilter(database.id)) },
289282
badge = {
290283
Text(
291-
text = when {
292-
forceWebDavBadge || database.isWebDavDatabase() -> stringResource(R.string.keepass_webdav_database_badge)
293-
database.storageLocation == KeePassStorageLocation.EXTERNAL -> stringResource(R.string.external_storage)
294-
else -> stringResource(R.string.internal_storage)
284+
text = if (database.storageLocation == KeePassStorageLocation.EXTERNAL) {
285+
stringResource(R.string.external_storage)
286+
} else {
287+
stringResource(R.string.internal_storage)
295288
},
296289
style = MaterialTheme.typography.labelSmall,
297290
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -779,7 +772,7 @@ fun UnifiedCategoryFilterBottomSheet(
779772
if (localKeePassDatabases.isNotEmpty()) {
780773
item {
781774
StorageSectionCard(title = stringResource(R.string.local_keepass_database)) {
782-
KeePassDatabaseItems(localKeePassDatabases, forceWebDavBadge = false)
775+
KeePassDatabaseItems(localKeePassDatabases)
783776
}
784777
}
785778
}

Monica for Android/app/src/main/java/takagi/ru/monica/ui/components/UnifiedMoveToCategoryBottomSheet.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ fun UnifiedMoveToCategoryBottomSheet(
107107
dampingRatio = Spring.DampingRatioNoBouncy,
108108
stiffness = Spring.StiffnessMediumLow
109109
)
110-
val localKeePassDatabases = keepassDatabases.filterNot { it.isWebDavDatabase() }
110+
val localKeePassDatabases = keepassDatabases
111111
val monicaCategoryNodes = remember(categories) { buildMonicaCategoryNodes(categories) }
112112

113113
ModalBottomSheet(

0 commit comments

Comments
 (0)