From e6913ae60e430383743dc006ef505009314bd795 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Fri, 4 Sep 2026 16:22:01 -0400 Subject: [PATCH] =?UTF-8?q?PARITY:=20history=20at=20rest=20was=20already?= =?UTF-8?q?=20built=20=E2=80=94=20the=20third=20stale=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marked ⚪ and implemented the whole time. Every read goes to disk: `Session.history` calls `Vault.history`, which decrypts the record. There is no in-memory copy to be fooled by. Checked against iOS's `E2EHistoryStore` rather than assumed. Same cipher, same posture, two honest differences recorded rather than papered over: - 🟢 Android binds the RECORD NAME into the AAD, so a sealed blob cannot be moved between slots. iOS keeps one snapshot file and does not need that — but would, the day it splits. - iOS's `.completeFileProtection` is a per-file assertion. Android's equivalent is Credential-Encrypted storage, a property of the volume rather than of this app's request. Neither favours one platform overall, and both clients agree on the behaviour that matters: a plain sign-out KEEPS history, and erasing the device destroys it — on Android by wiping every record and deleting the Keystore key, so what is left is unopenable as well as gone. ⚠️ Also recorded: `Vault.history` prunes expired messages on the way out, so disappearing messages are enforced on READ rather than by a timer nothing runs — and it rewrites the record only when something actually expired, so a read is not a write. Services (AIM/ICQ) retired rather than left as a gap: not a goal for this app (owner, 2026-09-04). That leaves only the two post-quantum rows, and the note above them already says why they are not a gap in effect: iOS's PQ files are inert because CryptoKit exposes ML-KEM on iOS 26 and the app targets 16, so neither client does post-quantum anything today. Closing it properly is one decision for both clients, not an Android task. Co-Authored-By: Claude Opus 5 --- PARITY.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/PARITY.md b/PARITY.md index 24a54f5..760b629 100644 --- a/PARITY.md +++ b/PARITY.md @@ -113,7 +113,7 @@ From `E2E/E2EInbox.swift`, which is the real feature list. | Trust state, verification, identity-change acceptance | ✅ pinned in the vault, surfaced on the thread | | Key-log enforcement and head gossip | ✅ opt-in enforcement, gossip detects a fork | | Clear thread | ✅ keeps the session, so the chat still works | -| History at rest (encrypted, device-only key) | ⚪ | +| History at rest (encrypted, device-only key) | ✅ **this row was stale** — sealed per thread under a Keystore key, read straight from disk; see below | | Presence (read and set) | ✅ shown from the relay, never inferred | | Buddy list, add by username | ✅ | | Remove, block, unblock | ✅ | @@ -121,7 +121,35 @@ From `E2E/E2EInbox.swift`, which is the real feature list. | Buddy commands | — retired; not a feature any more (owner, 2026-08-25) | | Voice calls (WebRTC, DTLS-SRTP, TURN) | ✅ P2P-first; ⚠️ no TURN deployed | | Notifications list, read state | ✅ | -| Services (AIM/ICQ via `flamenet-oscar`) | ⚪ | +| Services (AIM/ICQ via `flamenet-oscar`) | — retired; not a goal for this app (owner, 2026-09-04) | + +🔴 **History at rest was marked ⚪ and it has been built the whole time — the +third stale row in this file.** Every read goes to disk: `Session.history` calls +`Vault.history`, which decrypts the record. There is no in-memory copy to be +fooled by. + +What Android does, against what iOS's `E2EHistoryStore` does: + +| | iOS | Android | +|---|---|---| +| Cipher | AES-256-GCM | AES-256-GCM | +| Key | Keychain, `ThisDeviceOnly` | Android Keystore — never leaves the process at all | +| Bound to its slot | one file, nothing to confuse | 🟢 **the record NAME is the AAD**, so a blob cannot be moved between slots | +| Shape | one snapshot for everything | one record per thread | +| Off backups | excluded from iCloud | `allowBackup="false"`, and a Keystore key is unbackupable anyway | +| At rest, locked | `.completeFileProtection` | Credential-Encrypted storage: `filesDir` is unreadable until the first unlock after boot | +| Plain sign-out | keeps history | keeps history | +| Erase this device | `eraseLocalData()` | `forgetEverything()` **and** the Keystore key is deleted | + +⚠️ Two honest differences rather than a claim of equivalence. Android's +per-record AAD is a binding iOS's single file does not need; iOS's +`.completeFileProtection` is a per-file assertion, while Android's equivalent is +a property of the storage volume rather than of this app's request. Neither gap +favours one platform overall. + +⚠️ `Vault.history` prunes expired messages on the way out, so disappearing +messages are enforced on READ rather than by a timer nothing runs. It rewrites +the record only when something actually expired — a read is not a write. ✅ Calls are built, on `io.github.webrtc-sdk:android` — the one third-party library in this app, and unavoidable: the alternative is hand-rolling ICE, -- 2.54.0