OMEMO trust lists: steady order, no choice without a key, steady member cards #12

Closed
asxpi wants to merge 0 commits from asxpi/anotherim-js:fix/omemo-trust-list into develop
First-time contributor

Three problems with the OMEMO trust lists, found while reviewing a room member's keys in the 0.5.2 desktop app.

1. Rows moved as their trust was set (4f93f4b). omemo trust_list comes in the backend store's order: devices with a stored row first, then those only on a device list. Setting trust adds or drops that row (Undecided is no row), so the device jumped within the list, under the pointer. The rows are now sorted by device id. Active and inactive devices were already drawn apart.

2. Trust offered for a device whose key has not arrived (0ffa4b5, then 790ebbe). A device on a list whose bundle never came, such as an OMEMO 2 device never met, shows no fingerprint, yet its row offered all three choices.

  • libtacky takes a choice made ahead of the key (PendingTrust), fetches the bundle, and once the key is in, records it with RecordDecision as the key the user verified. The device would then read "Verified" for a key nobody was shown.
  • PendingTrust is in memory only. It isn't saved, trust_list doesn't report it, and it applies only if the bundle is fetched. Don't trust was sent, but the row kept reading Undecided, and a restart forgot it.

The fix:

  • A device with no known key is not settable. It gets no picker and is neither counted nor touched by Set all. In place of the fingerprint it says "Its key has not arrived yet, so there is nothing to decide." (all six language packs).
  • It is also not a key to review (wantsReview, ownDevicesToReview). The bar asked for a review that couldn't be done ("1 new device in … to review"). Its key arriving brings the row's choices, and the bar, back.
  • 0ffa4b5 first only disabled Trust. 790ebbe takes the rest away, for the reasons above.

Refusing a device before its key arrives would be worth having, especially with blind trust on. It needs libtacky to keep and report the pending choice.

3. The room window jumped when a member's keys were set (3bbcbe2). RoomStatus lists members needing attention first, then by JID, so trusting or refusing all of a member's keys moved that member's card. The cards were an unkeyed .map(), so other members were drawn into the places under the pointer, and the page seemed to jump to its end. The window now takes the order once, as it opens (attention first), and keeps it while open. A member who joins later goes at the end. The cards are keyed by JID. Opened again, the window starts from the backend's order.

Checks. tsc and i18n-extract --check pass. The OMEMO tests in omemo.test.ts, muc-omemo.test.ts, key-review-bar.test.ts, encryption-info.test.ts and four more pass (157), with new cases for the order, the key-less device (store, Set all, review, drawing) and the members' cards. All three were checked by hand in a local Flatpak build of 0.5.2 against a real room.

🤖 Generated with Claude Code

Three problems with the OMEMO trust lists, found while reviewing a room member's keys in the 0.5.2 desktop app. **1. Rows moved as their trust was set** (`4f93f4b`). `omemo trust_list` comes in the backend store's order: devices with a stored row first, then those only on a device list. Setting trust adds or drops that row (Undecided is no row), so the device jumped within the list, under the pointer. The rows are now sorted by device id. Active and inactive devices were already drawn apart. **2. Trust offered for a device whose key has not arrived** (`0ffa4b5`, then `790ebbe`). A device on a list whose bundle never came, such as an OMEMO 2 device never met, shows no fingerprint, yet its row offered all three choices. - libtacky takes a choice made ahead of the key (`PendingTrust`), fetches the bundle, and once the key is in, records it with `RecordDecision` as the key the user verified. The device would then read "Verified" for a key nobody was shown. - `PendingTrust` is in memory only. It isn't saved, `trust_list` doesn't report it, and it applies only if the bundle is fetched. Don't trust was sent, but the row kept reading Undecided, and a restart forgot it. The fix: - A device with no known key is not `settable`. It gets no picker and is neither counted nor touched by Set all. In place of the fingerprint it says "Its key has not arrived yet, so there is nothing to decide." (all six language packs). - It is also not a key to review (`wantsReview`, `ownDevicesToReview`). The bar asked for a review that couldn't be done ("1 new device in … to review"). Its key arriving brings the row's choices, and the bar, back. - `0ffa4b5` first only disabled Trust. `790ebbe` takes the rest away, for the reasons above. Refusing a device before its key arrives would be worth having, especially with blind trust on. It needs libtacky to keep and report the pending choice. **3. The room window jumped when a member's keys were set** (`3bbcbe2`). `RoomStatus` lists members needing attention first, then by JID, so trusting or refusing all of a member's keys moved that member's card. The cards were an unkeyed `.map()`, so other members were drawn into the places under the pointer, and the page seemed to jump to its end. The window now takes the order once, as it opens (attention first), and keeps it while open. A member who joins later goes at the end. The cards are keyed by JID. Opened again, the window starts from the backend's order. **Checks.** `tsc` and `i18n-extract --check` pass. The OMEMO tests in `omemo.test.ts`, `muc-omemo.test.ts`, `key-review-bar.test.ts`, `encryption-info.test.ts` and four more pass (157), with new cases for the order, the key-less device (store, Set all, review, drawing) and the members' cards. All three were checked by hand in a local Flatpak build of 0.5.2 against a real room. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The trust list showed the devices in the order the backend sends
them, which is its store's: the devices with a stored row first, then
those only on a device list. Setting trust adds or drops that row
(Undecided is no row at all), so the device moved within the list as
it was set, under the pointer.

Sort the rows by device id when a list arrives. Active and inactive
devices were already drawn apart.

Assisted-by: Claude:opus-5-5
A device on a contact's device list whose bundle never came (an
OMEMO 2 device never met) shows no fingerprint, yet its row offered
Trust. libtacky takes a trust given before the key (PendingTrust),
fetches the bundle and, the key in, records it with RecordDecision as
the key the user verified: the device would read "Verified" for a key
nobody was shown.

Trust waits for the key (trustable()): the picker's tick is disabled,
with "Its key has not arrived yet, so there is nothing to trust." in
place of the fingerprint, and setTrust and Set all skip such a row for
Trust. Undecided and Don't trust need no key and stay. A row still
waiting as Undecided no longer keeps Set all from reading as all
trusted, and is not a key to review (wantsReview, ownDevicesToReview),
since the bar's review could only end in Don't trust; its key arriving
brings it and the bar back.

The new string is in all six language packs.

Assisted-by: Claude:opus-5-5
A room's status lists its members needing attention first, then by
JID, so trusting or refusing all of a member's keys moved that
member's whole card, and the cards were an unkeyed map, so other
members were drawn into the places under the pointer: the page seemed
to jump to its end.

The window takes the order once, as it opens, and keeps it: attention
first on opening, nothing moving while it is open, a member who joins
later at the end. The cards are keyed by JID. Opened again, it starts
from the backend's order.

Assisted-by: Claude:opus-5-5
The previous commit left Undecided and Don't trust on such a row, on
the thought that a device could be refused before its key came. But
libtacky keeps a choice made ahead of the key (PendingTrust) in memory
alone: it is not saved, trust_list does not report it, and it applies
only if the bundle is fetched. Don't trust sent, and the row went on
reading Undecided; a restart forgot it.

So a device with no known key is not settable: no picker, not counted
or touched by Set all, with "Its key has not arrived yet, so there is
nothing to decide." in place of the fingerprint. The trustable()
helper and the picker's disabled-tick mode go. A refusal ahead of the
key needs libtacky to keep and report the pending choice.

Assisted-by: Claude:opus-5-5
Owner

Reviewed and merged into develop by hand with merge commit 83eb1d19, released in 0.5.6 (v0.5.6 = 33b27872, https://dev.another.im/another-im/anotherim-js/releases/tag/v0.5.6). The forge does not mark a hand merge as merged, so this PR is closed here.

Reviewed and merged into develop by hand with merge commit 83eb1d19, released in 0.5.6 (v0.5.6 = 33b27872, https://dev.another.im/another-im/anotherim-js/releases/tag/v0.5.6). The forge does not mark a hand merge as merged, so this PR is closed here.
ann closed this pull request 2026-10-16 08:50:02 +00:00
ann referenced this pull request from a commit 2026-10-27 13:09:52 +00:00
The remaining Lows of the final anotherim-js audit of 2026-10-27 (/data/another-im/audit-2026-10-27-final-anotherim-js.md): XEP-0439 actions are not drawn in rooms (#12: rowActions(m, room), message-row passes showNick, which both callers set from the chat's groupchat; libtacky refuses the press there and any occupant could draw buttons; whether action-selected goes out encrypted in an OMEMO chat is F2's libtacky half), a quick response chip shows the value it sends after its label whenever the two differ but for case and spacing (#13: XEP-0439 sends the value and only shows the label, so the new shownValue keeps the XEP and the reader sees the words before they go), isReportable is false for a room occupant's private chat since Blocking keeps bare JIDs and would block and report the whole room (#17, the safe one of the two choices), a vCard's tel: link carries data-ask and the link guard asks "Call {number}?" / "This opens {number} in your phone or calling app." before the opener in the shells and in a tab alike (#18), the desktop shell's openable hands a TEL: link on as tel: (#21, now a Cow), a gateway's auto-approved contact is added with no roster name instead of the subscribe nick (#19: their nick is their word, the name drawn then comes from their vCard or nick as theirs), and localHost, the page's sticker gate, also refuses a single-label host and names under .local, .localdomain, .home.arpa, .internal, .lan, .home, .intranet, .corp and .private, with the browser edition's remaining gap (a public-looking name resolving to a private address) documented at the function (#20). New strings in en, ru, uk, et and es; CHANGELOG Unreleased has a Security block naming the audit ids. Tests that failed before the fix (9 of them): quick-responses (rowActions in a room, shownValue, the chip showing "Large one Large"), report-spam (PM not reportable), requests (gateway contact added without a name), transfers (localHost local names), os-links (the tel question), links-cov (a data-ask tel opens through the question in a tab), vcard-profiles (tel link has data-ask); links.rs a_tel_scheme_in_upper_case_is_handed_on_in_lower_case. make -o audit check STRICT=1 clean (2110 keys), make test-ui 384 files 4246 passed 8 skipped, make test-shell ok, WebKit smoke 6769 ok with the contact info and profile card screens, its 4 FAILs the stale Android lock-line checks b1bdb10a fixed on develop.
ann referenced this pull request from a commit 2026-10-27 13:19:39 +00:00
Final audit 2026-10-27 H2, M6 and #12 (libtacky final M4, M7, L7, L9), on libtacky 34506021 (already inside the e6897acb develop pins), which takes a room's disappearing-messages timer only from the room or a moderator, gives a room PM its own timer, raises another's timer under a minute to 60 s, refuses groupcall join into a room on a service the inviting chat would not use and refuses select_action on a row that came encrypted: the app asks a room's timer by its room@service?join id with muc my_role and my_affiliation, and a moderator (role moderator, or owner or admin) gets Disappearing messages in the room's header menu and on the timer mark, Off first, the menu saying only moderators can change it, while anyone else keeps the plain mark; a PM's timer is asked and kept under room@service/nick (state/ephemeral.ts timerJid, key keeps the nick's case); the timer notice names the occupant by nick from room/nick and the room by its name (service-row.ts serviceActor, used by chat-view.ts) instead of the room for everyone; a group-call invite whose call room is not the chat's own room shows "Call room on {host}" and Join asks "Join this call?" naming the server, which the inviter chose, before joining (call-invite-row.ts); a row that came encrypted draws no XEP-0439 action buttons (quick-responses.ts rowActions). The generated api.ts carries the schema's new words; four new strings in en, ru, uk, et and es; CHANGELOG Unreleased Fixed has the Security lines. Tests: ephemeral.test.ts 4 new (a moderator offered the room's timers with Off and the room's chat id sent, an owner or admin moderates and a participant gets nothing, a room, a PM and a 1:1 with the room's address are three timers, the notice names the occupant), group-calls.test.ts 2 new and the in-room join confirming, quick-responses.test.ts an encrypted row, each failing before (7 failed on the old sources); the WebKit smoke's room group call screens check the server line and confirm the Join dialog. In the container, rebased on develop 827bf34f (rowActions keeps F4's room rule beside the encrypted one): make check STRICT=1 clean, make test-ui 385 files 4252 passed 8 skipped, make test-shell ok, the room group call smoke screens all ok (the whole webkit-smoke had no failures before the rebase).

Pull request closed

Sign in to join this conversation.
No description provided.