98b525b7049e

test: cross-platform KAT for canonicalDeviceManifestBody with null wsPort/relayUrl (v2.9.21)

time
2026-08-01 15:54 UTC · 2 months ago
machine
koh2
parent
3a829674cb88
3 files VERSION+1 −1app/build.gradle+1 −1app/src/test/java/com/peachpasswords/CrossPlatformKATTest.kt+49 −0
VERSION+1 −1
@@ -1 +1 @@1-2.9.201+2.9.21

view file

app/build.gradle+1 −1
@@ -32,7 +32,7 @@3232         minSdk 263333         targetSdk 353434         // Keep the Android code in Gradle; the semantic name comes from root VERSION.35-        versionCode 24016335+        versionCode 2401643636         versionName semanticVersion3737 3838         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

view file

app/src/test/java/com/peachpasswords/CrossPlatformKATTest.kt+49 −0
@@ -129,4 +129,53 @@129129         assertEquals("6a96444e20e8d4c1cee974416acae1c10b3c92886010e54ed94dafb2c3b80ea0", hex(nk1))130130         assertEquals("57af120b0de7acbe7907ec149c5ae870a2dbb74232b65777ba4123f1f7f888f5", hex(nk2))131131     }132+133+    @Test fun device_manifest_v3_canonical_form_omits_null_ws_port_and_relay_url() {134+        // Pinned against peach-browser canonicalise() in device-manifest.test.ts.135+        // Android's manifestEntryMap (DeviceManifestSupport.kt:194) omits wsPort136+        // and relayUrl when null; the browser's canonicalise skips null values137+        // for the same reason (v4.7.4). Both sides must reach the same canonical138+        // bytes — a future regression that re-introduces the null key on either139+        // side will fail this KAT and the browser's mirror test.140+        val body = DeviceManifestBody(141+            manifestVersion = 3,142+            issuerDeviceId = "issuer-1",143+            issuedAt = 1_700_000_000_000L,144+            devices = listOf(145+                ManifestDeviceEntry(146+                    deviceId = "peer-1",147+                    deviceName = "Peer One",148+                    deviceType = "extension",149+                    longTermPublicKeyHex = "aa".repeat(32),150+                    signingPublicKeyHex = "bb".repeat(32),151+                    wsPort = null,152+                    relayUrl = null,153+                    pairedAt = 1_700_000_000_001L,154+                ),155+            ),156+            revoked = listOf(157+                DeviceRevocation(158+                    deviceId = "gone-1",159+                    longTermPublicKeyHex = "cc".repeat(32),160+                    revokedAt = 1_700_000_000_002L,161+                    revokedByDeviceId = "issuer-1",162+                ),163+            ),164+        )165+        val canonical = canonicalDeviceManifestBody(body)166+        // Canonical does NOT mention wsPort or relayUrl at all.167+        assertFalse("wsPort must be omitted when null but got: $canonical", canonical.contains("wsPort"))168+        assertFalse("relayUrl must be omitted when null but got: $canonical", canonical.contains("relayUrl"))169+        // Pinned byte-for-byte — moving a key or changing any escaping fails this.170+        val expected = "{\"devices\":[{\"deviceId\":\"peer-1\",\"deviceName\":\"Peer One\"," +171+            "\"deviceType\":\"extension\",\"longTermPublicKeyHex\":\"" + "aa".repeat(32) + "\"," +172+            "\"pairedAt\":1700000000001," +173+            "\"signingPublicKeyHex\":\"" + "bb".repeat(32) + "\"}]," +174+            "\"issuedAt\":1700000000000,\"issuerDeviceId\":\"issuer-1\"," +175+            "\"manifestVersion\":3," +176+            "\"revoked\":[{\"deviceId\":\"gone-1\"," +177+            "\"longTermPublicKeyHex\":\"" + "cc".repeat(32) + "\",\"revokedAt\":1700000000002," +178+            "\"revokedByDeviceId\":\"issuer-1\"}]}"179+        assertEquals(expected, canonical)180+    }132181 }

view file