Skip to content

Commit 7bd0fe0

Browse files
committed
Add maintenance snapshot workflow and timeline updates
1 parent eb8e3fb commit 7bd0fe0

22 files changed

+1676
-182
lines changed

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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -559,16 +559,17 @@ fun MonicaContent(
559559
}
560560
}
561561

562+
// Emergency safe mode: disable global shared transition lookahead to avoid
563+
// "Placement happened before lookahead" crashes on affected devices/builds.
562564
@OptIn(androidx.compose.animation.ExperimentalSharedTransitionApi::class)
563-
androidx.compose.animation.SharedTransitionLayout {
564-
androidx.compose.runtime.CompositionLocalProvider(
565-
takagi.ru.monica.ui.LocalSharedTransitionScope provides this,
566-
takagi.ru.monica.ui.LocalReduceAnimations provides settings.reduceAnimations
565+
androidx.compose.runtime.CompositionLocalProvider(
566+
takagi.ru.monica.ui.LocalSharedTransitionScope provides null,
567+
takagi.ru.monica.ui.LocalReduceAnimations provides true
568+
) {
569+
NavHost(
570+
navController = navController,
571+
startDestination = fixedStartDestination
567572
) {
568-
NavHost(
569-
navController = navController,
570-
startDestination = fixedStartDestination
571-
) {
572573
composable(Screen.Login.route) {
573574
LoginScreen(
574575
viewModel = viewModel,
@@ -2131,17 +2132,20 @@ fun MonicaContent(
21312132
val quickMaintenanceViewModel: takagi.ru.monica.viewmodel.QuickDatabaseMaintenanceViewModel = viewModel {
21322133
takagi.ru.monica.viewmodel.QuickDatabaseMaintenanceViewModel(
21332134
takagi.ru.monica.data.maintenance.createQuickDatabaseMaintenanceEngine(
2135+
database = database,
21342136
passwordRepository = repository,
21352137
secureItemRepository = secureItemRepository,
21362138
passkeyRepository = passkeyRepository,
21372139
localKeePassDatabaseDao = database.localKeePassDatabaseDao(),
2140+
bitwardenFolderDao = database.bitwardenFolderDao(),
21382141
bitwardenVaultDao = database.bitwardenVaultDao(),
21392142
securityManager = securityManager,
21402143
workspaceRepository = takagi.ru.monica.repository.KeePassWorkspaceRepository(
21412144
context = context.applicationContext,
21422145
dao = database.localKeePassDatabaseDao(),
21432146
securityManager = securityManager
2144-
)
2147+
),
2148+
operationLogRepository = takagi.ru.monica.repository.OperationLogRepository(database.operationLogDao())
21452149
)
21462150
)
21472151
}
@@ -2156,9 +2160,13 @@ fun MonicaContent(
21562160
onIncludeAuthenticatorsChange = quickMaintenanceViewModel::updateIncludeAuthenticators,
21572161
onIncludeBankCardsChange = quickMaintenanceViewModel::updateIncludeBankCards,
21582162
onIncludePasskeysChange = quickMaintenanceViewModel::updateIncludePasskeys,
2163+
onModeChange = quickMaintenanceViewModel::updateMode,
2164+
onTargetSourceChange = quickMaintenanceViewModel::updateTargetSource,
21592165
onRun = {
21602166
quickMaintenanceViewModel.runMaintenance()
2161-
}
2167+
},
2168+
onConfirmRun = quickMaintenanceViewModel::confirmAndRunMaintenance,
2169+
onDismissPlan = quickMaintenanceViewModel::dismissPlan
21622170
)
21632171
}
21642172

@@ -2373,8 +2381,8 @@ fun MonicaContent(
23732381
)
23742382
}
23752383
}
2376-
}
2377-
}
2384+
}
2385+
23782386
}
23792387

23802388
@Composable

Monica for Android/app/src/main/java/takagi/ru/monica/bitwarden/repository/BitwardenRepository.kt

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class BitwardenRepository(private val context: Context) {
4545
private const val KEY_SYNC_ON_WIFI_ONLY = "sync_on_wifi_only"
4646
private const val KEY_LAST_SYNC_TIME = "last_sync_time"
4747
private const val KEY_NEVER_LOCK_BITWARDEN = "never_lock_bitwarden"
48+
private const val PBKDF2_DEFAULT_ITERATIONS = 600000
49+
private const val ARGON2_DEFAULT_ITERATIONS = 3
50+
private const val ARGON2_DEFAULT_MEMORY_MB = 64
51+
private const val ARGON2_DEFAULT_PARALLELISM = 4
4852
private val syncMutex = Mutex()
4953

5054
@Volatile
@@ -66,7 +70,7 @@ class BitwardenRepository(private val context: Context) {
6670
// 账号密码错误
6771
rawError.contains("invalid_username_or_password", ignoreCase = true) ||
6872
rawError.contains("Username or password is incorrect", ignoreCase = true) ->
69-
"邮箱或主密码错误,请检查后重试"
73+
"登录失败:账号凭据无效,或服务器区域/地址不匹配(.com/.eu/自建),请确认后重试"
7074

7175
// 验证码错误
7276
rawError.contains("Invalid New Device OTP", ignoreCase = true) ||
@@ -108,7 +112,7 @@ class BitwardenRepository(private val context: Context) {
108112

109113
// 其他 400 错误
110114
rawError.contains("400") && rawError.contains("invalid_grant") ->
111-
"认证失败,可能是两步验证未完成或验证码无效,请重试"
115+
"认证失败:可能是服务器区域或自建地址不匹配、SSO 账户限制、或验证流程未完成,请重试"
112116

113117
// 默认返回原始错误(截断过长内容)
114118
else -> {
@@ -388,22 +392,37 @@ class BitwardenRepository(private val context: Context) {
388392
suspend fun unlock(vaultId: Long, masterPassword: String): UnlockResult = withContext(Dispatchers.IO) {
389393
try {
390394
val vault = vaultDao.getVaultById(vaultId) ?: return@withContext UnlockResult.Error("Vault 不存在")
395+
val normalizedIterations = when (vault.kdfType) {
396+
BitwardenVault.KDF_TYPE_PBKDF2 -> vault.kdfIterations.takeIf { it > 0 } ?: PBKDF2_DEFAULT_ITERATIONS
397+
BitwardenVault.KDF_TYPE_ARGON2ID -> vault.kdfIterations.takeIf { it > 0 } ?: ARGON2_DEFAULT_ITERATIONS
398+
else -> vault.kdfIterations
399+
}
400+
val normalizedMemory = vault.kdfMemory.takeIf { it != null && it > 0 } ?: ARGON2_DEFAULT_MEMORY_MB
401+
val normalizedParallelism = vault.kdfParallelism.takeIf { it != null && it > 0 } ?: ARGON2_DEFAULT_PARALLELISM
391402

392403
// 派生主密钥
393-
val masterKey = if (vault.kdfType == BitwardenVault.KDF_TYPE_ARGON2ID) {
394-
BitwardenCrypto.deriveMasterKeyArgon2(
395-
password = masterPassword,
396-
salt = vault.email,
397-
iterations = vault.kdfIterations,
398-
memory = vault.kdfMemory ?: 64,
399-
parallelism = vault.kdfParallelism ?: 4
400-
)
401-
} else {
402-
BitwardenCrypto.deriveMasterKeyPbkdf2(
403-
password = masterPassword,
404-
salt = vault.email,
405-
iterations = vault.kdfIterations
406-
)
404+
val masterKey = when (vault.kdfType) {
405+
BitwardenVault.KDF_TYPE_ARGON2ID -> {
406+
BitwardenCrypto.deriveMasterKeyArgon2(
407+
password = masterPassword,
408+
salt = vault.email,
409+
iterations = normalizedIterations,
410+
memory = normalizedMemory,
411+
parallelism = normalizedParallelism
412+
)
413+
}
414+
415+
BitwardenVault.KDF_TYPE_PBKDF2 -> {
416+
BitwardenCrypto.deriveMasterKeyPbkdf2(
417+
password = masterPassword,
418+
salt = vault.email,
419+
iterations = normalizedIterations
420+
)
421+
}
422+
423+
else -> {
424+
return@withContext UnlockResult.Error("不支持的 KDF 类型: ${vault.kdfType},请重新登录")
425+
}
407426
}
408427

409428
try {

Monica for Android/app/src/main/java/takagi/ru/monica/bitwarden/service/BitwardenAuthService.kt

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ class BitwardenAuthService(
4242
private const val TAG = "BitwardenAuthService"
4343
private const val DIAG_PREFIX = "[BW_DIAG]"
4444
private const val ERROR_BODY_SNIPPET_LIMIT = 240
45+
private const val PBKDF2_DEFAULT_ITERATIONS = 600000
46+
private const val ARGON2_DEFAULT_ITERATIONS = 3
47+
private const val ARGON2_DEFAULT_MEMORY_MB = 64
48+
private const val ARGON2_DEFAULT_PARALLELISM = 4
4549

4650
// 两步验证类型
4751
const val TWO_FACTOR_AUTHENTICATOR = 0
@@ -106,11 +110,12 @@ class BitwardenAuthService(
106110
}
107111

108112
Result.success(
109-
PreLoginResult(
113+
normalizePreLoginResult(
110114
kdfType = body.kdf,
111115
kdfIterations = body.kdfIterations,
112116
kdfMemory = body.kdfMemory,
113-
kdfParallelism = body.kdfParallelism
117+
kdfParallelism = body.kdfParallelism,
118+
diagnosticAttemptId = diagnosticAttemptId
114119
)
115120
)
116121
} else {
@@ -210,20 +215,38 @@ class BitwardenAuthService(
210215
val emailLower = normalizedEmail.lowercase(Locale.ENGLISH)
211216

212217
// 2. 派生 Master Key
213-
masterKey = if (preLoginResult.kdfType == BitwardenVault.KDF_TYPE_ARGON2ID) {
214-
BitwardenCrypto.deriveMasterKeyArgon2(
215-
password = password,
216-
salt = emailLower, // 使用小写邮箱作为盐
217-
iterations = preLoginResult.kdfIterations,
218-
memory = preLoginResult.kdfMemory ?: 64,
219-
parallelism = preLoginResult.kdfParallelism ?: 4
220-
)
221-
} else {
222-
BitwardenCrypto.deriveMasterKeyPbkdf2(
223-
password = password,
224-
salt = emailLower, // 使用小写邮箱作为盐
225-
iterations = preLoginResult.kdfIterations
226-
)
218+
masterKey = when (preLoginResult.kdfType) {
219+
BitwardenVault.KDF_TYPE_ARGON2ID -> {
220+
BitwardenCrypto.deriveMasterKeyArgon2(
221+
password = password,
222+
salt = emailLower, // 使用小写邮箱作为盐
223+
iterations = preLoginResult.kdfIterations,
224+
memory = preLoginResult.kdfMemory ?: ARGON2_DEFAULT_MEMORY_MB,
225+
parallelism = preLoginResult.kdfParallelism ?: ARGON2_DEFAULT_PARALLELISM
226+
)
227+
}
228+
229+
BitwardenVault.KDF_TYPE_PBKDF2 -> {
230+
BitwardenCrypto.deriveMasterKeyPbkdf2(
231+
password = password,
232+
salt = emailLower, // 使用小写邮箱作为盐
233+
iterations = preLoginResult.kdfIterations
234+
)
235+
}
236+
237+
else -> {
238+
logDiag(
239+
flow = "primary",
240+
attemptId = attemptId,
241+
stage = "stop_unsupported_kdf",
242+
message =
243+
"kdf=${preLoginResult.kdfType}, iter=${preLoginResult.kdfIterations}, " +
244+
"mem=${preLoginResult.kdfMemory}, parallelism=${preLoginResult.kdfParallelism}"
245+
)
246+
return@withContext Result.failure(
247+
IllegalArgumentException("Unsupported KDF type: ${preLoginResult.kdfType}")
248+
)
249+
}
227250
}
228251

229252
// 3. 派生 Master Password Hash (用于服务器认证)
@@ -1018,6 +1041,61 @@ class BitwardenAuthService(
10181041
return isInvalidGrant && (isInvalidCredDescription || isInvalidCredBody)
10191042
}
10201043

1044+
private fun normalizePreLoginResult(
1045+
kdfType: Int,
1046+
kdfIterations: Int,
1047+
kdfMemory: Int?,
1048+
kdfParallelism: Int?,
1049+
diagnosticAttemptId: String?
1050+
): PreLoginResult {
1051+
val normalized = when (kdfType) {
1052+
BitwardenVault.KDF_TYPE_PBKDF2 -> PreLoginResult(
1053+
kdfType = kdfType,
1054+
kdfIterations = kdfIterations.takePositiveOrDefault(PBKDF2_DEFAULT_ITERATIONS),
1055+
kdfMemory = null,
1056+
kdfParallelism = null
1057+
)
1058+
1059+
BitwardenVault.KDF_TYPE_ARGON2ID -> PreLoginResult(
1060+
kdfType = kdfType,
1061+
kdfIterations = kdfIterations.takePositiveOrDefault(ARGON2_DEFAULT_ITERATIONS),
1062+
kdfMemory = kdfMemory.takePositiveOrDefault(ARGON2_DEFAULT_MEMORY_MB),
1063+
kdfParallelism = kdfParallelism.takePositiveOrDefault(ARGON2_DEFAULT_PARALLELISM)
1064+
)
1065+
1066+
else -> PreLoginResult(
1067+
kdfType = kdfType,
1068+
kdfIterations = kdfIterations,
1069+
kdfMemory = kdfMemory,
1070+
kdfParallelism = kdfParallelism
1071+
)
1072+
}
1073+
1074+
if (
1075+
normalized.kdfIterations != kdfIterations ||
1076+
normalized.kdfMemory != kdfMemory ||
1077+
normalized.kdfParallelism != kdfParallelism
1078+
) {
1079+
diagnosticAttemptId?.let { attemptId ->
1080+
logDiag(
1081+
flow = "primary",
1082+
attemptId = attemptId,
1083+
stage = "prelogin_normalized",
1084+
message =
1085+
"kdf=$kdfType, iter:$kdfIterations->${normalized.kdfIterations}, " +
1086+
"mem:$kdfMemory->${normalized.kdfMemory}, " +
1087+
"parallelism:$kdfParallelism->${normalized.kdfParallelism}"
1088+
)
1089+
}
1090+
}
1091+
1092+
return normalized
1093+
}
1094+
1095+
private fun Int?.takePositiveOrDefault(default: Int): Int {
1096+
return this?.takeIf { it > 0 } ?: default
1097+
}
1098+
10211099
private fun newAttemptId(): String = UUID.randomUUID().toString().substring(0, 8)
10221100

10231101
private fun classifyServer(vaultUrl: String): String {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ interface OperationLogDao {
8080
*/
8181
@Query("DELETE FROM operation_logs WHERE timestamp < :cutoffTime")
8282
suspend fun deleteOldLogs(cutoffTime: Long)
83+
84+
/**
85+
* 删除超过指定天数的维护快照日志
86+
*/
87+
@Query("DELETE FROM operation_logs WHERE timestamp < :cutoffTime AND changesJson LIKE '%' || :snapshotFieldName || '%'")
88+
suspend fun deleteOldMaintenanceSnapshotLogs(cutoffTime: Long, snapshotFieldName: String)
8389

8490
/**
8591
* 更新日志的恢复状态

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ interface PasswordEntryDao {
5454

5555
@Query("SELECT * FROM password_entries WHERE id IN (:ids)")
5656
suspend fun getPasswordsByIds(ids: List<Long>): List<PasswordEntry>
57+
58+
@Query("SELECT * FROM password_entries WHERE id IN (:ids) AND isDeleted = 0 AND isArchived = 0")
59+
suspend fun getActivePasswordsByIds(ids: List<Long>): List<PasswordEntry>
5760

5861
@Insert(onConflict = OnConflictStrategy.REPLACE)
5962
suspend fun insertPasswordEntry(entry: PasswordEntry): Long

0 commit comments

Comments
 (0)