Skip to content

e2ee(pair): design §4.4's revoke-then-re-pair recovery is unreachable from the app UI #920

Description

@RonenMars

What happened

During Group D's device-hardware row 6 testing (key/pin survival, revoke-then-re-pair), a paired E2EE device was revoked server-side (revoked_at set). The app correctly surfaced the break — "Unauthorized — the server rejected the API key for /api/info", no crash, no plaintext fallback — good behavior on that front.

Streamer design.md §4.4 states the intended recovery path explicitly:

Re-pairing a revoked device is allowed and clears revoked_at ... The same D_priv still identifies the device, so the existing row is restored rather than duplicated.

The streamer side implements exactly this: devices.repository.ts's register() looks up the row by e2ee_static_pub (byE2eeStaticPubStmt) and, when it finds one, runs repairStmt — an UPDATE that clears revoked_at on the existing row rather than inserting a new one.

The gap

The app has no way to reach that recovery path.

  • Re-scanning the same pairing URL while the revoked server is still locally listed is blocked client-side with "Server already added — delete that server first if you want to add it again," regardless of the server's revoked state.
  • The only reachable UI path is delete → re-add. But removeServer (stores/servers.ts) calls clearDeviceStaticKey(serverId), wiping D_priv from SecureStore. editServer does the same (stores/servers.ts:487-488).
  • So a re-add after delete always presents a fresh, random static key — serverIdFromUrl-keyed storage means the phone can never present the same e2ee_static_pub again for that server, and the streamer's repairStmt path is therefore structurally unreachable from this app.

Evidence (reproduced on hardware)

Cabled physical iPhone, streamer 1.70.6, scratch-isolated rig. Revoked a paired device (60d2ed51...) server-side, confirmed via GET /api/devices. Deleted the server in-app, re-added via a fresh pairing link to the identical URL. Server-side result: a genuinely new device row (668b05df..., revokedAt: null) was created; the original revoked row stays revoked forever, orphaned (compounds threadbase-streamer#744's dead-row accumulation — every revoke-then-recover attempt through the app leaves one more permanently-dead row rather than reusing one).

Full trace: tb-e2ee-program/tracks/D/PLAN-D.md §13, row 6 (Group D, device evidence track).

Suggested follow-up (not scoped here)

A re-authorize affordance that keeps D_priv: re-scanning an existing (or revoked) server's QR/link should present the stored static key rather than routing through the destructive delete flow, so the app can actually exercise the streamer's own §4.4 recovery path. clearDeviceStaticKey should probably stay reserved for an explicit "forget this device" action distinct from ordinary remove/edit, or the block/gate on re-scanning an already-known URL needs a "revoked — re-authorize?" branch instead of a hard "already added" refusal.

Filed per Group D / D1's "no code changes — file an issue and report" rule.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions