1a3d936afbab
say plainly at the point of removal that revocation stops future changes but recovery words survive it, and stop the monotonic entry clock leaking between tests (v2.4.6)
- time
- 2026-07-26 08:42 UTC · 3 months ago
- machine
- koh2
- agent
- claude-opus-5
- session
- sync campaign remaining items
- parent
- 2ce28477fa2c
↓ peach-android-1a3d936a.tar.gzatlas at this save
files changed5 files
VERSION+1 −1app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+25 −4app/src/main/java/com/peachpasswords/vault/VaultCrypto.kt+13 −0app/src/test/java/com/peachpasswords/vault/VaultCryptoTest.kt+8 −1app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 24000520- versionName "2.4.5"19+ versionCode 24000620+ versionName "2.4.6"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+25 −4
@@ -720,13 +720,34 @@720720 modifier = Modifier.padding(top = 2.dp),721721 )722722 }723- if (confirmUnpair == d.deviceId) {724- Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {723+ if (confirmUnpair != d.deviceId) {724+ SubtleButton("remove", fg) { confirmUnpair = d.deviceId }725+ }726+ }727+ // HOLE 4: revocation stops this device receiving future changes. It728+ // does NOT secure the vault against it — that device may already729+ // hold the recovery words, which are master-password-equivalent and730+ // restore the whole vault anywhere, forever, with no sync involved.731+ // Say so here rather than leaving the user falsely reassured.732+ if (confirmUnpair == d.deviceId) {733+ Column(Modifier.fillMaxWidth().padding(bottom = PeachSpacing.sm)) {734+ Text(735+ "Stops this device receiving future changes. It doesn't lock it " +736+ "out of what it already has - if it's lost or stolen, also " +737+ "change your master password and generate new recovery words.",738+ color = fg.copy(alpha = 0.7f),739+ fontFamily = PeachType.Body,740+ fontSize = 11.sp,741+ lineHeight = 16.sp,742+ )743+ Row(744+ modifier = Modifier.fillMaxWidth().padding(top = PeachSpacing.sm),745+ horizontalArrangement = Arrangement.End,746+ ) {725747 SubtleButton("cancel", fg) { confirmUnpair = null }748+ Spacer(Modifier.width(PeachSpacing.sm))726749 SubtleButton("remove", peachColors.Breach) { syncVm.unpairDevice(d.deviceId); confirmUnpair = null }727750 }728- } else {729- SubtleButton("remove", fg) { confirmUnpair = d.deviceId }730751 }731752 }732753 }app/src/main/java/com/peachpasswords/vault/VaultCrypto.kt+13 −0
@@ -144,6 +144,19 @@144144 return base145145 }146146 147+ /**148+ * Reset the process-global monotonic entry clock. TEST ONLY.149+ *150+ * [monotonicEntryTimestamp] never moves backwards, so any test that feeds151+ * it a far-future baseline leaves every later test in the same JVM issuing152+ * far-future timestamps. That is invisible until something compares against153+ * a clock-skew bound — [SyncLimits.clampSyncTimestamp] does, and a local154+ * entry stamped beyond the skew window silently strands remote tombstones.155+ */156+ fun resetMonotonicClockForTest() {157+ _lastTimestamp = 0L158+ }159+147160 fun zero(bytes: ByteArray) {148161 bytes.fill(0)149162 }app/src/test/java/com/peachpasswords/vault/VaultCryptoTest.kt+8 −1
@@ -174,7 +174,14 @@174174 175175 @Test fun monotonic_timestamp_beats_supplied_baseline() {176176 val future = System.currentTimeMillis() + 1_000_000177- assertTrue(VaultCrypto.monotonicEntryTimestamp(future) > future)177+ try {178+ assertTrue(VaultCrypto.monotonicEntryTimestamp(future) > future)179+ } finally {180+ // The counter is process-global and never moves backwards, so181+ // leaving it ~17 minutes ahead makes every later test in this JVM182+ // stamp entries beyond the sync clock-skew window.183+ VaultCrypto.resetMonotonicClockForTest()184+ }178185 }179186 180187 @Test fun generate_salt_and_keys_have_expected_sizes() {