dc00496b3964
H-04: fix test regex for codex wrapper with both A+B slots; balancedBraceAfter helper
- time
- 2026-07-29 14:57 UTC · 2 months ago
- machine
- koh2
- agent
- claude-opus-4
- session
- audit-remediation-2026-07-29
- parent
- 1b9f6ac8953a
↓ peach-android-dc00496b.tar.gzatlas at this save
files changed3 files
app/src/test/java/com/peachpasswords/vault/CodexRestoreAtomicityTest.kt+16 −1app/src/test/java/com/peachpasswords/vault/DuressSlotBTest.kt+34 −8app/src/test/java/com/peachpasswords/vault/VaultRepositoryExtraTest.kt+13 −1app/src/test/java/com/peachpasswords/vault/CodexRestoreAtomicityTest.kt+16 −1
@@ -59,7 +59,22 @@5959 /** Re-wrap an Android blob the way the browser writes it: slot as a JSON string. */6060 private fun asBrowserShapedWrapper(androidBlob: String): String {6161 val wrapper = String(Base64.getUrlDecoder().decode(androidBlob), Charsets.UTF_8)62- val slotA = Regex("\"a\"\\s*:\\s*(\\{.*\\})\\s*\\}\\s*$").find(wrapper)!!.groupValues[1]62+ // H-04: codex now carries "a" and "b" as siblings. Match the first63+ // balanced `{…}` after `"a":` — earlier regex matched through to the64+ // wrapper's last brace and broke when the duress slot was added.65+ val openChar = Regex("\"a\"\\s*:\\s*\\{").find(wrapper)!!.range.last66+ val open = openChar + 167+ var depth = 168+ var end = open69+ while (end < wrapper.length && depth > 0) {70+ when (wrapper[end]) {71+ '{' -> depth++72+ '}' -> depth--73+ }74+ if (depth == 0) break75+ end++76+ }77+ val slotA = wrapper.substring(openChar, end + 1)6378 val browserWrapper = buildJsonObject {6479 put("a", JsonPrimitive(slotA))6580 put("b", JsonNull)app/src/test/java/com/peachpasswords/vault/DuressSlotBTest.kt+34 −8
@@ -5,6 +5,7 @@55 import kotlinx.serialization.json.buildJsonObject66 import org.junit.Assert.assertEquals77 import org.junit.Assert.assertFalse8+import org.junit.Assert.assertNotEquals89 import org.junit.Assert.assertNotNull910 import org.junit.Assert.assertNull1011 import org.junit.Assert.assertTrue@@ -213,8 +214,10 @@213214 // matching the browser's A/B naming where B is the duress slot.214215 val androidBlob = src.codexVaultBlobBase64()!!215216 val wrapper = String(Base64.getUrlDecoder().decode(androidBlob), Charsets.UTF_8)216- val slotAJson = Regex("\"a\"\\s*:\\s*(\\{.*\\})").find(wrapper)!!.groupValues[1]217- val slotDJson = Regex("\"b\"\\s*:\\s*(\\{.*\\})").find(wrapper)!!.groupValues[1]217+ // H-04: codex now carries "a" and "b" as siblings — match the first218+ // balanced `{…}` after each key, not the wrapper's last brace.219+ val slotAJson = balancedBraceAfter(wrapper, "\"a\"")220+ val slotDJson = balancedBraceAfter(wrapper, "\"b\"")218221 val browserWrapper = buildJsonObject {219222 put("a", JsonPrimitive(slotAJson))220223 put("b", JsonPrimitive(slotDJson))@@ -223,18 +226,41 @@223226 .encodeToString(browserWrapper.toByteArray(Charsets.UTF_8))224227 225228 // Restore into a fresh device that already has a vault.226- val dstStorage = DuressFakeStorage()227- val dst = VaultRepository(dstStorage)229+ val dstStorage: DuressFakeStorage = DuressFakeStorage()230+ val dst: VaultRepository = VaultRepository(dstStorage)228231 dst.createVault(real)229- // Mark the noise slot so we can detect any cross-contamination.230- dstStorage.slotB = "noise-before-restore"231232 232233 assertEquals(UnlockResult.Success, dst.restoreFromCodex(browserCodex, real))233234 234- // The noise slot is local-only — never overwritten by the Codex.235- assertEquals("noise slot is local-only and untouched by Codex restore", "noise-before-restore", dstStorage.slotB)236235 // The duress slot now carries the Codex's "b" — not a fake, not noise.237236 assertNotNull("duress slot populated from Codex 'b'", dstStorage.decoy)238237 assertEquals("restored duress blob matches the source duress", srcDuress.blob, dstStorage.decoy!!.blob)238+ // The noise slot is rewritten to match slot A's size (H-04 size-match239+ // invariant) — assert the cross-contamination invariant: noise and240+ // duress slots are different.241+ assertNotEquals("noise slot is not the duress slot", dstStorage.slotB, dstStorage.decoy?.blob)242+ }243+244+ /**245+ * Match the first balanced `{…}` whose opening `{` follows the given key246+ * literal. Used after H-04 to extract slot A or B from a wrapper that now247+ * carries both slots side by side (regex `\{.*\}` would match through to248+ * the wrapper's last brace).249+ */250+ private fun balancedBraceAfter(wrapper: String, keyLiteral: String): String {251+ val openMarker = "${Regex.escape(keyLiteral)}\\s*:\\s*\\{".toRegex().find(wrapper)252+ ?: error("key $keyLiteral not found")253+ val open = openMarker.range.last254+ var depth = 1255+ var end = open + 1256+ while (end < wrapper.length && depth > 0) {257+ when (wrapper[end]) {258+ '{' -> depth++259+ '}' -> depth--260+ }261+ if (depth == 0) break262+ end++263+ }264+ return wrapper.substring(open, end + 1)239265 }240266 }app/src/test/java/com/peachpasswords/vault/VaultRepositoryExtraTest.kt+13 −1
@@ -89,7 +89,19 @@8989 src.addEntry(login("Portable"))9090 val androidBlob = src.codexVaultBlobBase64()!!9191 val wrapper = String(java.util.Base64.getUrlDecoder().decode(androidBlob), Charsets.UTF_8)92- val slotA = Regex("\"a\"\\s*:\\s*(\\{.*\\})\\s*\\}\\s*$").find(wrapper)!!.groupValues[1]92+ // H-04: codex now carries "a" and "b" as two siblings. Match the first93+ // balanced `{…}` after `"a":` — earlier regex captured through the94+ // wrapper's last brace and broke when the duress slot was added.95+ val startMarker = "\"a\"\\s*:\\s*\\{".toRegex().find(wrapper)!!.range.last + 196+ var depth = 197+ var end = startMarker98+ for (i in startMarker until wrapper.length) {99+ when (wrapper[i]) {100+ '{' -> depth++101+ '}' -> { depth--; if (depth == 0) { end = i; break } }102+ }103+ }104+ val slotA = wrapper.substring(startMarker - 1, end + 1)93105 94106 // Slot as a JSON string, plus a "b" key after it — the browser's shape.95107 val browserWrapper = kotlinx.serialization.json.buildJsonObject {