fix(crypto): escrow the THREAD reveal too — the live sealed surface
Found during live verification of the escrow: I had added the fallback only to
decryptPartnerAnswer, the DAILY path. Daily answers are schemaVersion 2 —
couple-key encrypted, gated by rules, not ECIES-sealed. The sealed system's live
surface is question THREADS. So the escrow healed the path that doesn't need it
and left the one that does still failing on a second device: exactly the bug,
untouched, behind a fix that looked complete.
Worth naming the pattern: the exploration notes said this plainly ("the live
blast radius today is question threads"), and I still wired the fix to the
first call site I read. Reading the map is not the same as following it.
decryptPartnerThreadAnswer now takes the same escrow fallback. Mutation-checked:
reverting it to a bare loadPrivateKey() fails exactly the new test.
Diagnostics from the live run removed. The escrow write itself is confirmed
accepted by the DEPLOYED rules — an instrumented run showed "existing=true" on
a device that had published one on an earlier Home load, which also proves the
idempotence guard skips correctly.
This commit is contained in:
parent
a216d1b20a
commit
2f16d1e09c
|
|
@ -200,7 +200,13 @@ class SealedRevealManager @Inject constructor(
|
|||
recipientUserId = userId
|
||||
) ?: return null
|
||||
|
||||
val myPrivateKey = userKeyManager.loadPrivateKey() ?: return null
|
||||
// Same escrow fallback as the daily path — and this is the one that matters most: question
|
||||
// threads are the LIVE sealed surface (daily answers are couple-key/schemaVersion 2), so a
|
||||
// second device loses thread reveals first. Healing only the daily path would have fixed the
|
||||
// dead one and left the real one broken.
|
||||
val myPrivateKey = userKeyManager.loadPrivateKey()
|
||||
?: restorePrivateKeyFromEscrow(userId, coupleId)
|
||||
?: return null
|
||||
val oneTimeKey = releaseKeyEncryptor.unwrapFromSender(
|
||||
keyboxB64 = keybox,
|
||||
recipientPrivateKey = myPrivateKey,
|
||||
|
|
|
|||
|
|
@ -83,4 +83,26 @@ class PartnerAnswerResultTest {
|
|||
assertEquals(SealedRevealManager.PartnerAnswerResult.KeyUnavailable, decrypt())
|
||||
io.mockk.coVerify { crashReporter.recordException(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the THREAD reveal also falls back to escrow — it is the live sealed surface`() = runTest {
|
||||
// Threads are where sealed answers actually run today (daily answers are couple-key), so a
|
||||
// second device loses THESE first. The escrow fallback originally went only on the daily
|
||||
// path — healing the dead one and leaving the real one broken.
|
||||
coEvery { releaseKeyDataSource.readReleaseKeyForThread(any(), any(), any(), any()) } returns "keybox:v1:x"
|
||||
every { userKeyManager.loadPrivateKey() } returns null
|
||||
coEvery { deviceKeyDataSource.getPrivateKeyEscrow("me") } returns null
|
||||
|
||||
val result = manager.decryptPartnerThreadAnswer(
|
||||
coupleId = "c1",
|
||||
threadId = "t1",
|
||||
partnerId = "partner",
|
||||
userId = "me",
|
||||
encryptedPayload = "sealed:v1:whatever"
|
||||
)
|
||||
|
||||
// Null (not a crash), and the escrow was consulted rather than giving up on the local key.
|
||||
assertEquals(null, result)
|
||||
io.mockk.coVerify { deviceKeyDataSource.getPrivateKeyEscrow("me") }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue