2dfe4d9c3398
fix: Remove stale revoked device rows (v2.5.16)
- time
- 2026-07-27 17:22 UTC · 2 months ago
- machine
- koh2
- agent
- codex-gpt-5
- session
- peach-sync-e2e
- parent
- f90df691e6d2
↓ peach-android-2dfe4d9c.tar.gzatlas at this save
files changed4 files
VERSION+1 −1app/build.gradle+2 −2app/src/main/java/com/peachpasswords/sync/PairedDeviceStore.kt+47 −8app/src/test/java/com/peachpasswords/sync/PairedDeviceRevocationTest.kt+53 −0app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 24011520- versionName "2.5.15"19+ versionCode 24011620+ versionName "2.5.16"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/sync/PairedDeviceStore.kt+47 −8
@@ -94,17 +94,22 @@9494 revokedByDeviceId: String,9595 nowMs: Long = System.currentTimeMillis(),9696 ): DeviceRevocation? {97- val existing = listRevocations()98- if (existing.any { it.deviceId == deviceId }) return null99- val record = DeviceRevocation(97+ val currentDevices = getAll()98+ val currentRevocations = listRevocations()99+ val mutation = planDeviceRevocation(100+ devices = currentDevices,101+ revocations = currentRevocations,100102 deviceId = deviceId,101- longTermPublicKeyHex = get(deviceId)?.longTermPublicKeyHex ?: "0".repeat(64),102- revokedAt = nowMs,103103 revokedByDeviceId = revokedByDeviceId,104+ nowMs = nowMs,104105 )105- persistRevocations(existing + record)106- remove(deviceId)107- return record106+ if (mutation.revocations !== currentRevocations) {107+ persistRevocations(mutation.revocations)108+ }109+ if (mutation.devices != currentDevices) {110+ persist(mutation.devices)111+ }112+ return mutation.added108113 }109114 110115 /**@@ -196,6 +201,40 @@196201 const val MAX_REVOCATIONS = 500197202 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }198203 }204+}205+206+internal data class DeviceRevocationMutation(207+ val devices: List<PairedDevice>,208+ val revocations: List<DeviceRevocation>,209+ val added: DeviceRevocation?,210+)211+212+/**213+ * Compute one revoke transaction without Android storage so the impossible214+ * "paired and already revoked" state can be regression-tested directly.215+ *216+ * Older builds could leave that overlap behind. A repeated Remove must still217+ * drop the live row even though the terminal record itself is idempotent.218+ */219+internal fun planDeviceRevocation(220+ devices: List<PairedDevice>,221+ revocations: List<DeviceRevocation>,222+ deviceId: String,223+ revokedByDeviceId: String,224+ nowMs: Long,225+): DeviceRevocationMutation {226+ val remaining = devices.filterNot { it.deviceId == deviceId }227+ if (revocations.any { it.deviceId == deviceId }) {228+ return DeviceRevocationMutation(remaining, revocations, null)229+ }230+ val device = devices.firstOrNull { it.deviceId == deviceId }231+ val record = DeviceRevocation(232+ deviceId = deviceId,233+ longTermPublicKeyHex = device?.longTermPublicKeyHex ?: "0".repeat(64),234+ revokedAt = nowMs,235+ revokedByDeviceId = revokedByDeviceId,236+ )237+ return DeviceRevocationMutation(remaining, revocations + record, record)199238 }200239 201240 /**app/src/test/java/com/peachpasswords/sync/PairedDeviceRevocationTest.kt+53 −0
@@ -0,0 +1,53 @@1+package com.peachpasswords.sync2+3+import org.junit.Assert.assertEquals4+import org.junit.Assert.assertNull5+import org.junit.Test6+7+class PairedDeviceRevocationTest {8+ private val browser = PairedDevice(9+ deviceId = "browser-1",10+ deviceName = "Chrome",11+ deviceType = "extension",12+ longTermPublicKeyHex = "ab".repeat(32),13+ pairedAt = 1L,14+ )15+16+ @Test17+ fun `new revocation records the key and removes the paired row`() {18+ val mutation = planDeviceRevocation(19+ devices = listOf(browser),20+ revocations = emptyList(),21+ deviceId = browser.deviceId,22+ revokedByDeviceId = "phone-1",23+ nowMs = 42L,24+ )25+26+ assertEquals(emptyList<PairedDevice>(), mutation.devices)27+ assertEquals(1, mutation.revocations.size)28+ assertEquals(browser.longTermPublicKeyHex, mutation.added?.longTermPublicKeyHex)29+ assertEquals(42L, mutation.added?.revokedAt)30+ }31+32+ @Test33+ fun `existing revocation still removes a stale overlapping paired row`() {34+ val existing = DeviceRevocation(35+ deviceId = browser.deviceId,36+ longTermPublicKeyHex = browser.longTermPublicKeyHex!!,37+ revokedAt = 1L,38+ revokedByDeviceId = "phone-1",39+ )40+41+ val mutation = planDeviceRevocation(42+ devices = listOf(browser),43+ revocations = listOf(existing),44+ deviceId = browser.deviceId,45+ revokedByDeviceId = "phone-1",46+ nowMs = 99L,47+ )48+49+ assertEquals(emptyList<PairedDevice>(), mutation.devices)50+ assertEquals(listOf(existing), mutation.revocations)51+ assertNull(mutation.added)52+ }53+}