2c0edcf5291a
name a rotating device from local state instead of the vouching peer's manifest claim, and stop a third-party issuer renaming a paired record (v2.4.3)
- time
- 2026-07-26 08:04 UTC · 3 months ago
- machine
- koh2
- agent
- claude-opus-5
- session
- sync campaign remaining items
- parent
- 536cb610f6f6
↓ peach-android-2c0edcf5.tar.gzatlas at this save
files changed5 files
VERSION+1 −1app/build.gradle+2 −2app/src/main/java/com/peachpasswords/sync/SyncAlert.kt+6 −1app/src/main/java/com/peachpasswords/sync/VaultSyncAdapter.kt+5 −1app/src/test/java/com/peachpasswords/sync/SyncAlertTest.kt+25 −0app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 24000220- versionName "2.4.2"19+ versionCode 24000320+ versionName "2.4.3"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/sync/SyncAlert.kt+6 −1
@@ -84,9 +84,14 @@8484 !CryptoCompare.hex(it, nextSigning)8585 } == true8686 if (!longTermChanged && !signingChanged) return null87+ // S5: name the device from LOCAL state. `incoming` is a third-party88+ // issuer's unverified claim about someone else, so preferring it would let89+ // an attacker make its own key rotation read as a rotation on a device the90+ // user trusts more. Fall back to the manifest only when nothing is stored.8791 return SyncAlert(8892 type = SyncAlertType.DEVICE_KEY_ROTATED,89- deviceName = incoming.deviceName.ifBlank { existing.deviceName ?: "paired device" },93+ deviceName = existing.deviceName?.takeIf { it.isNotBlank() }94+ ?: incoming.deviceName.ifBlank { "paired device" },9095 timestamp = nowMs,9196 )9297 }app/src/main/java/com/peachpasswords/sync/VaultSyncAdapter.kt+5 −1
@@ -174,9 +174,13 @@174174 deviceKeyRotationAlert(existing, entry, System.currentTimeMillis())?.let { alert ->175175 syncAlerts?.record(listOf(alert))176176 }177+ // S5: keys and ports propagate through the mesh, the name does178+ // not. A device names itself at pairing; letting a third-party179+ // issuer rewrite that is the rename-then-impersonate vector.177180 registry.upsert(178181 existing.copy(179- deviceName = entry.deviceName,182+ deviceName = existing.deviceName?.takeIf { it.isNotBlank() }183+ ?: entry.deviceName,180184 deviceType = entry.deviceType,181185 longTermPublicKeyHex = entry.longTermPublicKeyHex.lowercase(),182186 signingPublicKeyHex = entry.signingPublicKeyHex.lowercase(),app/src/test/java/com/peachpasswords/sync/SyncAlertTest.kt+25 −0
@@ -98,6 +98,31 @@9898 assertNull(deviceKeyRotationAlert(existing, incoming.copy(signingPublicKeyHex = "bb".repeat(32)), nowMs = 99L))9999 }100100 101+ @Test fun `S5 rotation alert names the device from local state not the manifest`() {102+ // A hostile issuer relabels the rotating device, hoping the alert reads103+ // as a rotation on something the user trusts more.104+ val existing = PairedDevice(105+ deviceId = "browser",106+ deviceName = "Spare Tablet",107+ deviceType = "extension",108+ longTermPublicKeyHex = "aa".repeat(32),109+ signingPublicKeyHex = "bb".repeat(32),110+ pairedAt = 1L,111+ )112+ val incoming = ManifestDeviceEntry(113+ deviceId = "browser",114+ deviceName = "June's Primary Phone",115+ deviceType = "extension",116+ longTermPublicKeyHex = "aa".repeat(32),117+ signingPublicKeyHex = "cc".repeat(32),118+ pairedAt = 1L,119+ )120+121+ val alert = deviceKeyRotationAlert(existing, incoming, nowMs = 99L)122+123+ assertEquals("Spare Tablet", alert?.deviceName)124+ }125+101126 @Test fun sync_alert_queue_is_bounded_to_latest_two_hundred() {102127 val existing = (0 until 199).map { SyncAlert(SyncAlertType.REMOTE_DELETE, timestamp = it.toLong()) }103128 val incoming = listOf(