3.9 KiB
3.9 KiB
Message Integrity
Signed, append-only message revisions give P2P chat a verifiable history without central message storage. The materialized Message row in local SQLite/IDB is a cache; peers converge via inventory snapshots and revision events.
Responsibilities
- Revision chain — Every create, edit, delete, moderation, or plugin mutation appends a
MessageRevisionwith monotonically increasingrevision,prevRevisionHash, andheadHash. - Dual emit — Outgoing mutations broadcast the legacy event (
chat-message,message-edited,message-deleted) andmessage-revisionso older peers keep working while integrity-aware peers prefer revisions. - Inventory — Sync inventories include
{ id, ts, rc, ac, revision, headHash }. Peers re-fetch when remote revision is newer or the same revision has a different hash (tamper detection). - Signing — Human authors sign revisions with per-user Ed25519 keys. Public keys are registered on the signaling server; private keys stay in browser
localStorage.
Boundaries
| Layer | Owns |
|---|---|
Product client (toju-app) |
Revision construction, merge, verification, P2P broadcast, local persistence |
Signaling server (server) |
PUT /api/users/me/signing-key, GET /api/users/:id/signing-public-key — key directory only, no message storage |
| Electron / mobile persistence | revision + headHash on message rows; revision audit log (IDB store / SQLite meta) |
Plugin API messages may emit unsigned revisions (plugin-edit / plugin-delete) when the actor is a synthetic plugin user.
Key types
Message.revision,Message.headHash— materialized cache fields on the sharedMessagemodel.MessageRevision— wire + persistence audit record (message-revision.models.ts).MessageRevisionType—create,author-edit,author-delete,moderate-edit,moderate-delete,plugin-edit,plugin-delete.ChatEvent.type: 'message-revision'— P2P envelope carrying a fullMessageRevision.
Merge rules
- Valid signed revision with higher
revisionwins over legacy timestamp edits. - Same
revision, differentheadHash→ treat as stale/tampered and re-fetch. - Unsigned revisions (no
signature) are accepted for backward compatibility when verification is skipped. - Legacy peers without
revision/headHashin inventory fall back tots/rc/accomparison.
Client touchpoints
- Domain rules:
message-integrity.rules.ts,message-revision.builder.rules.ts,message-sync.rules.ts - Services:
MessageRevisionService,MessageSigningService - Store:
messages.effects.ts(outgoing dual-emit),messages-incoming.handlers.ts(handleMessageRevision),messages.helpers.ts(inventory + merge) - Plugins:
plugin-client-api.service.tsemits revisions for send/edit/delete
Server API
| Method | Path | Auth | Body / response |
|---|---|---|---|
PUT |
/api/users/me/signing-key |
Bearer | { publicKeyJwk } — stores Ed25519 public JWK on the user row |
GET |
/api/users/:id/signing-public-key |
Public | { publicKeyJwk } — used by peers to verify signatures |
Registration runs automatically after login/register via AuthenticationService.
Degraded-mode behavior
- Outgoing revision signing is best-effort: if
Ed25519signing fails, the client still broadcasts the legacychat-messageenvelope (unsigned revision). - Incoming signed revisions are accepted without cryptographic verification when the sender's public key is not yet registered on the server, so chat is not blocked during key-registration races.
Testing
- Unit:
message-integrity.rules.spec.ts,message-revision.builder.rules.spec.ts,message-revision-signing.rules.spec.ts,message-sync.rules.spec.ts,messages-incoming.handlers.spec.ts - Outgoing revision wiring is covered indirectly through existing message effect tests; add focused specs when changing merge or signing behavior.