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
↓ peach-android-98b525b7.tar.gzatlas at this save
files changed3 files
VERSION+1 −1app/build.gradle+1 −1app/src/test/java/com/peachpasswords/CrossPlatformKATTest.kt+49 −0app/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"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 }