[research] Verification reminder for your OWN unverified sessions #123

Closed
opened 2026-09-17 15:43:35 -04:00 by jared · 3 comments
Owner

Not other people's devices (that is #113 session hygiene / the existing composer warning). This is: you logged in on a new device, never verified it (no cross-signing signature), and nothing ever tells you — so that device can't read backed-up history and shows as a red shield to everyone else.

Research first (correctness matters — wrong prompts here erode trust)

  1. Exact SDK signals: crypto.getUserDeviceInfo / DeviceVerificationStatus.crossSigningVerified for this device (mx.getDeviceId()), cross-signing set-up state (isCrossSigningReady), secret storage state — enumerate every combination and what the correct nudge is (verify via another device / set up cross-signing / restore from backup), reusing the existing flows in components/DeviceVerificationSetup.tsx, BackupRestore.tsx.
  2. Timing: nudge on the next launch after 24 h, not during first-run (the user is mid-onboarding); once per 7 days after that; never on a device that is verified; stop entirely after a dismissal count (3?).
  3. Where: sidebar-bottom banner like the unverified-device warning vs. a dot on Settings → Devices. Recommend one.
  4. Interplay with #110 (key backup banner) so two banners never stack — a single "security" banner with priority ordering.
  5. OIDC/Authelia accounts: confirm the flows behave identically (they should — verification is client crypto, not auth).
    Deliverable: findings + one design in this issue; implementation after review.
Not other people's devices (that is #113 session hygiene / the existing composer warning). This is: you logged in on a new device, never verified it (no cross-signing signature), and nothing ever tells you — so that device can't read backed-up history and shows as a red shield to everyone else. ### Research first (correctness matters — wrong prompts here erode trust) 1. Exact SDK signals: `crypto.getUserDeviceInfo` / `DeviceVerificationStatus.crossSigningVerified` for **this** device (`mx.getDeviceId()`), cross-signing set-up state (`isCrossSigningReady`), secret storage state — enumerate every combination and what the correct nudge is (verify via another device / set up cross-signing / restore from backup), reusing the existing flows in `components/DeviceVerificationSetup.tsx`, `BackupRestore.tsx`. 2. Timing: nudge on the **next launch after 24 h**, not during first-run (the user is mid-onboarding); once per 7 days after that; never on a device that is verified; stop entirely after a dismissal count (3?). 3. Where: sidebar-bottom banner like the unverified-device warning vs. a dot on Settings → Devices. Recommend one. 4. Interplay with #110 (key backup banner) so two banners never stack — a single "security" banner with priority ordering. 5. OIDC/Authelia accounts: confirm the flows behave identically (they should — verification is client crypto, not auth). Deliverable: findings + one design in this issue; implementation after review.
jared added this to the Features 2026-Q4 milestone 2026-09-17 15:43:35 -04:00
jared added the enhancementpriority: mediumsecurityarea: auth-sessionresearch labels 2026-09-17 15:43:35 -04:00
jared self-assigned this 2026-09-17 15:43:35 -04:00
Author
Owner

Findings (2026-09-20, code audit)

1. Signals we already compute, and every combination

# cross-signing published? (useCrossSigningActive = m.cross_signing.master account-data present) this device signed? (crypto.getDeviceVerificationStatus(me, myDeviceId).crossSigningVerified → useDeviceVerificationStatus) key backup on server? (crypto.getKeyBackupInfo() → useKeyBackupInfo) meaning correct nudge
A no — — account never set up cross-signing (fresh account, or reset) "Set up device verification" → DeviceVerificationSetup (bootstraps SSSS + cross-signing + backup in one go)
B yes no yes the case in this issue: logged in elsewhere, never verified here; history is in backup but this device can't unlock it, and it shows a red shield to others "Verify this device" → Settings → Devices → VerifyOtherDeviceTile (request from another device) or "Verify Manually" (recovery key/passphrase → ManualVerification, which also restores the backup)
C yes no no cross-signing exists but no backup (someone turned it off) same as B; verification alone fixes the red shield, history for old rooms stays lost — say so honestly
D yes yes no verified, no backup that is #110's banner, not this one
E yes yes yes healthy nothing, ever
F Unsupported (getDeviceVerificationStatus returns null: crypto not ready / no rust store yet) — — transient on boot nothing; re-evaluate on DeviceListChange (already wired)

Existing surfaces: the sidebar already shows the red shield tab (UnverifiedTab.tsx) for case B/C and an orange one for other unverified devices, and Settings → Devices already explains the two paths (Verification.tsx). So "nothing ever tells you" is slightly off — it is a 24 px icon with a tooltip and no words, which is why nobody acts on it.

2. Timing

Use the session's creation time (the sessions store keeps it; add createdAt if it doesn't survive a reload — it's one field on the persisted session). Rules: show only when now − createdAt ≥ 24 h and status is B or C (never A during onboarding: the setup dialog is already offered at Settings → Devices, and a first-run nudge collides with the E2EE onboarding), re-show at most every 7 days, stop after 3 dismissals — all three counters in localStorage under lotus-verify-nudge-<deviceId> (per device on purpose; cleared by clearPlaintextCaches on logout). Verified → the record is deleted so a later reset starts fresh.

3. Where

Recommendation: a sidebar-bottom banner (same slot and style as the existing unverified-device icon, but with words: "Verify this device — so you can read older messages and others see you as trusted · Verify · Not now") rather than a dot on Settings → Devices. A dot is what we have today in effect, and it doesn't convert. Mobile: the same banner above the composer's room list.

4. One security banner (#110)

Fold both into a single SecurityBanner with a priority list: verify-device (B/C) > key-backup (#110's D) > nothing. One banner slot, one dismissal store keyed by banner id, so they never stack and B's dismissal doesn't hide D later.

5. OIDC / Authelia

Identical: everything above is client-side rust-crypto state (device signatures, SSSS, backup). The only OIDC-specific wrinkle is account-management redirects for resetting cross-signing (accountManagementActions.crossSigningReset is already handled in Verification.tsx); the nudge never needs it.

Design (one, for review)

  • hooks/useVerifyNudge.ts: pure decision shouldShowVerifyNudge({ status, crossSigningActive, sessionCreatedAt, record, now }) (unit-tested) + the localStorage record.
  • components/SecurityBanner.tsx rendered once in the sidebar footer, choosing between verify-device and key-backup (#110) content by priority; "Verify" opens Settings → Devices (via settingsRequestAtom, like #159); "Not now" bumps the dismissal count.
  • Remove nothing: the shield icon stays for the always-on state, the banner is the timed nudge.
    ~150 lines + tests. Waiting for a go before building (per the issue: implementation after review).
## Findings (2026-09-20, code audit) ### 1. Signals we already compute, and every combination | # | cross-signing published? (`useCrossSigningActive` = `m.cross_signing.master` account-data present) | this device signed? (`crypto.getDeviceVerificationStatus(me, myDeviceId).crossSigningVerified` → `useDeviceVerificationStatus`) | key backup on server? (`crypto.getKeyBackupInfo()` → `useKeyBackupInfo`) | meaning | correct nudge | |---|---|---|---|---|---| | A | no | — | — | account never set up cross-signing (fresh account, or reset) | **"Set up device verification"** → `DeviceVerificationSetup` (bootstraps SSSS + cross-signing + backup in one go) | | B | yes | **no** | yes | the case in this issue: logged in elsewhere, never verified here; history is in backup but this device can't unlock it, and it shows a red shield to others | **"Verify this device"** → Settings → Devices → `VerifyOtherDeviceTile` (request from another device) or "Verify Manually" (recovery key/passphrase → `ManualVerification`, which also restores the backup) | | C | yes | no | no | cross-signing exists but no backup (someone turned it off) | same as B; verification alone fixes the red shield, history for old rooms stays lost — say so honestly | | D | yes | yes | no | verified, no backup | that is #110's banner, not this one | | E | yes | yes | yes | healthy | nothing, ever | | F | `Unsupported` (`getDeviceVerificationStatus` returns null: crypto not ready / no rust store yet) | — | — | transient on boot | nothing; re-evaluate on `DeviceListChange` (already wired) | Existing surfaces: the sidebar **already** shows the red shield tab (`UnverifiedTab.tsx`) for case B/C and an orange one for other unverified devices, and Settings → Devices already explains the two paths (`Verification.tsx`). So "nothing ever tells you" is slightly off — it is a 24 px icon with a tooltip and no words, which is why nobody acts on it. ### 2. Timing Use the session's creation time (the `sessions` store keeps it; add `createdAt` if it doesn't survive a reload — it's one field on the persisted session). Rules: show only when `now − createdAt ≥ 24 h` and status is B or C (never A during onboarding: the setup dialog is already offered at Settings → Devices, and a first-run nudge collides with the E2EE onboarding), re-show at most every 7 days, stop after 3 dismissals — all three counters in `localStorage` under `lotus-verify-nudge-<deviceId>` (per device on purpose; cleared by `clearPlaintextCaches` on logout). Verified → the record is deleted so a later reset starts fresh. ### 3. Where Recommendation: a **sidebar-bottom banner** (same slot and style as the existing unverified-device icon, but with words: "Verify this device — so you can read older messages and others see you as trusted · Verify · Not now") rather than a dot on Settings → Devices. A dot is what we have today in effect, and it doesn't convert. Mobile: the same banner above the composer's room list. ### 4. One security banner (#110) Fold both into a single `SecurityBanner` with a priority list: `verify-device` (B/C) > `key-backup` (#110's D) > nothing. One banner slot, one dismissal store keyed by banner id, so they never stack and B's dismissal doesn't hide D later. ### 5. OIDC / Authelia Identical: everything above is client-side rust-crypto state (device signatures, SSSS, backup). The only OIDC-specific wrinkle is account-management redirects for *resetting* cross-signing (`accountManagementActions.crossSigningReset` is already handled in `Verification.tsx`); the nudge never needs it. ## Design (one, for review) - `hooks/useVerifyNudge.ts`: pure decision `shouldShowVerifyNudge({ status, crossSigningActive, sessionCreatedAt, record, now })` (unit-tested) + the localStorage record. - `components/SecurityBanner.tsx` rendered once in the sidebar footer, choosing between verify-device and key-backup (#110) content by priority; "Verify" opens Settings → Devices (via `settingsRequestAtom`, like #159); "Not now" bumps the dismissal count. - Remove nothing: the shield icon stays for the always-on state, the banner is the timed nudge. ~150 lines + tests. Waiting for a go before building (per the issue: implementation after review).
jared added the needs-human-review label 2026-09-20 13:49:50 -04:00
Author
Owner

Design review: screenshots of the real client (#110 + #123 together)

These are screenshots of the actual Lotus Chat client, running a prototype on a local branch (security-nudge) that isn't pushed or deployed.

  • Real accounts: the test accounts are in real states on a local test homeserver. One is fresh. The other had cross-signing and key backup set up through the real Settings flow on device 1, then logged in again as device 2.
  • One faked value: the 24-hour grace period was skipped by back-dating this device's "first seen" record.

One nudge, three states (priority order)

  1. Verify this device. Cross-signing exists but this device isn't verified. It comes first because verifying with the recovery key also connects the backup.
  2. Connect this device to your key backup. A backup exists but this device isn't using it. (Same strip; not screenshotted, as it's hard to produce on demand.)
  3. Set up key backup. There's no backup at all, including accounts that never set up cross-signing. The setup flow does both.

The primary button opens Settings → Devices (the existing flows). Not now snoozes it.

verify device
verify device dark
setup backup
setup backup dark

"Set up" opens Settings → Devices, where Device Verification → Enable sets up verification + backup:
after set up

Phone: the full text wraps.
phone

Rules (from the research above)

  • Grace period: nothing in a device's first 24 h (onboarding).
  • Snooze and limit: Not now snoozes that nudge for 7 days; after 3 dismissals it stops for good. A different nudge isn't affected.
  • Healthy devices never see it. Verified on device 1: after setting up, nothing shows.
  • One strip at a time: it waits until startup sync has settled (never under "Connecting…"), and outage, maintenance and connection strips (#124) take priority.
  • Local only: the snooze and dismissal record is per device, in local storage, and wiped on logout.
  • Can't break the app: the banner sits inside an error boundary.
  • Placement: the design above suggested the sidebar. That sidebar is the 64-px icon rail, too narrow for words, so the nudge uses the same top strip as the status banner. The red shield icon in the rail stays as it is.

Found and fixed while making these

  • App crash on this branch: the first version crashed the app on load. It was mounted outside the Matrix client context. It's now mounted inside, plus the error boundary.
  • Status strip on phones: it now wraps its text instead of cutting it off when there's no Details button. That affects the #124 strip too, and only for the better.

Your call

Wording, the 24 h / 7 days / 3 dismissals numbers, and anything about the look. Once you've approved it I'll add end-to-end tests and open the PR.

## Design review: screenshots of the real client (#110 + #123 together) These are **screenshots of the actual Lotus Chat client**, running a prototype on a local branch (`security-nudge`) that isn't pushed or deployed. - **Real accounts:** the test accounts are in real states on a local test homeserver. One is fresh. The other had cross-signing and key backup set up through the real Settings flow on device 1, then logged in again as device 2. - **One faked value:** the 24-hour grace period was skipped by back-dating this device's "first seen" record. ### One nudge, three states (priority order) 1. **Verify this device.** Cross-signing exists but this device isn't verified. It comes first because verifying with the recovery key also connects the backup. 2. **Connect this device to your key backup.** A backup exists but this device isn't using it. (Same strip; not screenshotted, as it's hard to produce on demand.) 3. **Set up key backup.** There's no backup at all, including accounts that never set up cross-signing. The setup flow does both. The primary button opens **Settings → Devices** (the existing flows). **Not now** snoozes it. ![verify device](https://code.lotusguild.org/attachments/48692952-ec13-47a5-9992-d7f8c8537827) ![verify device dark](https://code.lotusguild.org/attachments/dadbc288-bcff-46da-a67f-cd3a1a058c74) ![setup backup](https://code.lotusguild.org/attachments/64320234-9bf5-49d0-876b-3cff9efbdb1f) ![setup backup dark](https://code.lotusguild.org/attachments/eeda702a-5bc7-49d3-a8aa-d55e5593b83f) **"Set up" opens Settings → Devices, where Device Verification → Enable sets up verification + backup:** ![after set up](https://code.lotusguild.org/attachments/04aac001-396d-4871-bcdb-dfc79bb99c5e) **Phone:** the full text wraps. ![phone](https://code.lotusguild.org/attachments/9ec3f8d8-cb26-4939-8f28-f03f15c15477) ### Rules (from the research above) - **Grace period:** nothing in a device's **first 24 h** (onboarding). - **Snooze and limit:** **Not now** snoozes that nudge for **7 days**; after **3** dismissals it stops for good. A different nudge isn't affected. - **Healthy devices** never see it. Verified on device 1: after setting up, nothing shows. - **One strip at a time:** it waits until startup sync has settled (never under "Connecting…"), and outage, maintenance and connection strips (#124) take priority. - **Local only:** the snooze and dismissal record is per device, in local storage, and wiped on logout. - **Can't break the app:** the banner sits inside an error boundary. - **Placement:** the design above suggested the sidebar. That sidebar is the 64-px icon rail, too narrow for words, so the nudge uses the same top strip as the status banner. The red shield icon in the rail stays as it is. ### Found and fixed while making these - **App crash on this branch:** the first version crashed the app on load. It was mounted outside the Matrix client context. It's now mounted inside, plus the error boundary. - **Status strip on phones:** it now wraps its text instead of cutting it off when there's no **Details** button. That affects the #124 strip too, and only for the better. ### Your call Wording, the 24 h / 7 days / 3 dismissals numbers, and anything about the look. Once you've approved it I'll add end-to-end tests and open the PR.
Author
Owner

Shipped in #258 (merged, live on chat.lotusguild.org since 01:05 UTC 2026-10-01). It's one device-security strip for this device. In priority order:

  1. Verify this device.
  2. Connect this device to your key backup.
  3. Set up key backup.

Timing:

  • nothing in a device's first 24 h;
  • Not now snoozes it for 7 days;
  • it stops after 3 dismissals;
  • never on a healthy device.

Placement: it waits until sync has settled, and outage, maintenance and connection strips take priority.

Tests: 4 unit tests, plus end-to-end tests for the grace period, Set up key backup opening Settings → Devices, and Not now surviving a reload. Verify this device was checked by hand on a second device (screenshots above).

Shipped in #258 (merged, live on chat.lotusguild.org since 01:05 UTC 2026-10-01). It's one device-security strip for this device. In priority order: 1. **Verify this device.** 2. **Connect this device to your key backup.** 3. **Set up key backup.** **Timing:** - nothing in a device's first **24 h**; - **Not now** snoozes it for **7 days**; - it stops after **3** dismissals; - never on a healthy device. **Placement:** it waits until sync has settled, and outage, maintenance and connection strips take priority. **Tests:** 4 unit tests, plus end-to-end tests for the grace period, *Set up key backup* opening Settings → Devices, and **Not now** surviving a reload. *Verify this device* was checked by hand on a second device (screenshots above).
jared closed this issue 2026-09-30 21:05:57 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#123