[libtacky / JID] Accounts and chats are keyed by exact spelling: -acc/-chat case is not normalized (RFC 7622) #19

Open
opened 2026-11-05 19:33:40 +00:00 by fmodf · 1 comment

Severity: Critical
Found in: libtacky v1.2.0-beta2 (2f42a2f2), anotherim-swift feat/ui-redesign (TestFlight 94/95)
Date: November 5, 2026

Summary

libtacky keys accounts and chats by the exact string the host passes in, without case normalization. Messages from the server, however, are filed in lowercase. When the host spells an address differently from how it was stored, lookups fail or land in a separate, empty conversation.

RFC 7622 says this is the same address. The localpart is enforced with the PRECIS UsernameCaseMapped profile (RFC 8265), which maps it to lowercase. The domainpart is case-insensitive (§3.2). Comparison must happen after enforcement (§3.3). Only the resourcepart (OpaqueString) is case-sensitive. Ivan@another.im and ivan@another.im are one JID. RFC 6122 / nodeprep gave the same result.

Where

-acc lookup is case-sensitive

  • client (tacky/xmpp/client/client.tcl:924-929) → account exists → account_find: SELECT id FROM accounts WHERE bare_jid = $jid (tacky/core/storage.tcl:1639-1642). bare_jid has no COLLATE NOCASE (storage.tcl:110).
  • An account stored as Ivan@another.im is not found by -acc ivan@another.im. Every call fails with Account does not exist: ivan@another.im.

account add stores the address as typed, add_from_uri normalizes it

  • account add stores -acc verbatim (tacky/xmpp/modules/account.tcl:321-363, checked only by jid valid-account, jid.tcl:401-413).
  • add_from_uri uses jid norm [jid bare …] (account.tcl:1703).
  • The per-account store file is named after the raw string (client.tcl:690, 961). On a case-insensitive filesystem two spellings share one file.

libtacky feeds the server's spelling back as -acc

  • After bind, options(-jid) becomes the bound JID (client.tcl:666, 690), which servers normally return in lowercase.
  • That value is then passed as -acc to account store_fast_token / clear_fast_token / fast_token_used (client.tcl:765-784), and to OAuthSignedIn, OAuthTokenRejected and CertificateRejected (client.tcl:695, 792, 798).
  • For an account stored with capitals these calls hit Require and fail. The rotated FAST token is never saved.
  • client account_id (client.tcl:510-515) may create a second, lowercase account row in the per-account store.

Events change spelling at bind

  • emit sends -acc [jid bare $options(-jid)] (tacky/xmpp/modules/client.tcl:660-663).
  • Before bind that is the stored spelling. After bind it is the server's.
  • A host comparing -acc sees two different accounts.

-chat is verbatim on the way in, lowercase on the way back

  • message send files the row under chat_jid $opts(-chat) as given (message.tcl:3971-3973) and emits <New> -jid with that spelling.
  • message history passes -chat through GetLocal unchanged (message.tcl:7436-7480). messagestore compares chat_jid=$jid exactly (messagestore.tcl:641-656).
  • Incoming and MAM messages go through PeerChat: jid norm [jid bare $peer] (message.tcl:1264-1272, 1506-1508, 1593-1600).
  • So message send -chat Bob@x starts a second conversation next to bob@x. Replies never appear in it, and history -chat Bob@x does not see them.
  • Some methods already normalize: clear_history (message.tcl:6546), roster -jid (roster.tcl:293, 318, 343), chatlist (chatlist.tcl:707).

Impact on the client

  • anotherim-swift #9 and #11: an empty duplicate chat with no history, where your own sent message never comes back.
  • anotherim-swift worked around this in the app by lowercasing the bare part of every chat address it passes (ChatID). Because libtacky looks accounts up by exact spelling, that workaround breaks accounts stored with capitals: from build 94, every call from the chat screen fails with Account does not exist. Before it, the app dropped those accounts' live events instead, because events carry the lowercase bound JID.
  • The app cannot fix this cleanly. Accounts already stored with capitals keep their spelling, and their FAST tokens are not saved either way.

Expected

  • Normalize every bare JID argument (-acc, -chat, -jid) with jid norm on entry to the API and in account add, leaving the resourcepart as given.
  • Compare and store bare JIDs only in normalized form.
  • Migrate existing data. Rewrite accounts.bare_jid and messages.chat_jid (and the other per-chat tables) to lowercase, merging duplicate chats. Rename per-account store files.
  • Emit -acc in one stable, normalized spelling.

Steps to reproduce

  1. account add -acc Ivan@another.im … and sign in.
  2. message history -acc ivan@another.im -chat bob@another.im returns Account does not exist: ivan@another.im.
  3. With the account signed in, check the -acc of the events before and after bind: Ivan@another.im, then ivan@another.im.
  4. message send -acc Ivan@another.im -chat Bob@another.im -body hi, then let bob@another.im reply. The reply is filed under bob@another.im, and message history -chat Bob@another.im shows only the sent message.
Severity: Critical Found in: libtacky v1.2.0-beta2 (2f42a2f2), anotherim-swift `feat/ui-redesign` (TestFlight 94/95) Date: November 5, 2026 ## Summary libtacky keys accounts and chats by the exact string the host passes in, without case normalization. Messages from the server, however, are filed in lowercase. When the host spells an address differently from how it was stored, lookups fail or land in a separate, empty conversation. RFC 7622 says this is the same address. The localpart is enforced with the PRECIS `UsernameCaseMapped` profile (RFC 8265), which maps it to lowercase. The domainpart is case-insensitive (§3.2). Comparison must happen after enforcement (§3.3). Only the resourcepart (`OpaqueString`) is case-sensitive. `Ivan@another.im` and `ivan@another.im` are one JID. RFC 6122 / nodeprep gave the same result. ## Where **`-acc` lookup is case-sensitive** - `client` (`tacky/xmpp/client/client.tcl:924-929`) → `account exists` → `account_find`: `SELECT id FROM accounts WHERE bare_jid = $jid` (`tacky/core/storage.tcl:1639-1642`). `bare_jid` has no `COLLATE NOCASE` (`storage.tcl:110`). - An account stored as `Ivan@another.im` is not found by `-acc ivan@another.im`. Every call fails with `Account does not exist: ivan@another.im`. **`account add` stores the address as typed, `add_from_uri` normalizes it** - `account add` stores `-acc` verbatim (`tacky/xmpp/modules/account.tcl:321-363`, checked only by `jid valid-account`, `jid.tcl:401-413`). - `add_from_uri` uses `jid norm [jid bare …]` (`account.tcl:1703`). - The per-account store file is named after the raw string (`client.tcl:690, 961`). On a case-insensitive filesystem two spellings share one file. **libtacky feeds the server's spelling back as `-acc`** - After bind, `options(-jid)` becomes the bound JID (`client.tcl:666, 690`), which servers normally return in lowercase. - That value is then passed as `-acc` to `account store_fast_token` / `clear_fast_token` / `fast_token_used` (`client.tcl:765-784`), and to `OAuthSignedIn`, `OAuthTokenRejected` and `CertificateRejected` (`client.tcl:695, 792, 798`). - For an account stored with capitals these calls hit `Require` and fail. The rotated FAST token is never saved. - `client account_id` (`client.tcl:510-515`) may create a second, lowercase account row in the per-account store. **Events change spelling at bind** - `emit` sends `-acc [jid bare $options(-jid)]` (`tacky/xmpp/modules/client.tcl:660-663`). - Before bind that is the stored spelling. After bind it is the server's. - A host comparing `-acc` sees two different accounts. **`-chat` is verbatim on the way in, lowercase on the way back** - `message send` files the row under `chat_jid $opts(-chat)` as given (`message.tcl:3971-3973`) and emits `<New> -jid` with that spelling. - `message history` passes `-chat` through `GetLocal` unchanged (`message.tcl:7436-7480`). `messagestore` compares `chat_jid=$jid` exactly (`messagestore.tcl:641-656`). - Incoming and MAM messages go through `PeerChat`: `jid norm [jid bare $peer]` (`message.tcl:1264-1272, 1506-1508, 1593-1600`). - So `message send -chat Bob@x` starts a second conversation next to `bob@x`. Replies never appear in it, and `history -chat Bob@x` does not see them. - Some methods already normalize: `clear_history` (`message.tcl:6546`), roster `-jid` (`roster.tcl:293, 318, 343`), chatlist (`chatlist.tcl:707`). ## Impact on the client - anotherim-swift #9 and #11: an empty duplicate chat with no history, where your own sent message never comes back. - anotherim-swift worked around this in the app by lowercasing the bare part of every chat address it passes (`ChatID`). Because libtacky looks accounts up by exact spelling, that workaround breaks accounts stored with capitals: from build 94, every call from the chat screen fails with `Account does not exist`. Before it, the app dropped those accounts' live events instead, because events carry the lowercase bound JID. - The app cannot fix this cleanly. Accounts already stored with capitals keep their spelling, and their FAST tokens are not saved either way. ## Expected - Normalize every bare JID argument (`-acc`, `-chat`, `-jid`) with `jid norm` on entry to the API and in `account add`, leaving the resourcepart as given. - Compare and store bare JIDs only in normalized form. - Migrate existing data. Rewrite `accounts.bare_jid` and `messages.chat_jid` (and the other per-chat tables) to lowercase, merging duplicate chats. Rename per-account store files. - Emit `-acc` in one stable, normalized spelling. ## Steps to reproduce 1. `account add -acc Ivan@another.im …` and sign in. 2. `message history -acc ivan@another.im -chat bob@another.im` returns `Account does not exist: ivan@another.im`. 3. With the account signed in, check the `-acc` of the events before and after bind: `Ivan@another.im`, then `ivan@another.im`. 4. `message send -acc Ivan@another.im -chat Bob@another.im -body hi`, then let `bob@another.im` reply. The reply is filed under `bob@another.im`, and `message history -chat Bob@another.im` shows only the sent message.
Owner

Fixed in libtacky 1.2.0-rc4. Every bare-JID argument of every method is normalised at the API's door (the table of jid-typed arguments is generated from the schema, so nothing is kept by hand): localpart and domain are lowercased and width-mapped, a final dot dropped, a resource and a room's ?join kept as given; what jid valid refuses (the characters RFC 7622 forbids, parts over 1023 bytes, control characters) is an error at the call and nothing is stored or sent. account add stores the normalised address, the client's own JID is normalised before and after the bind, so -acc in events is one spelling throughout and the FAST-token and certificate callbacks find the account; account list is the source of truth. A store written by an older build is migrated once at open: accounts, every per-chat table, the roster, PEP, OTR and the module state are re-keyed, duplicate chats merged without losing a message (the earlier copy of a message kept, the newer read point or draft), store files renamed through a temporary name so a case-insensitive filesystem is safe. No schema-version bump, so rc3 can still open the store. Hosts: from the first event after the upgrade -acc and chat JIDs are the normalised ones.

Fixed in libtacky 1.2.0-rc4. Every bare-JID argument of every method is normalised at the API's door (the table of `jid`-typed arguments is generated from the schema, so nothing is kept by hand): localpart and domain are lowercased and width-mapped, a final dot dropped, a resource and a room's `?join` kept as given; what `jid valid` refuses (the characters RFC 7622 forbids, parts over 1023 bytes, control characters) is an error at the call and nothing is stored or sent. `account add` stores the normalised address, the client's own JID is normalised before and after the bind, so `-acc` in events is one spelling throughout and the FAST-token and certificate callbacks find the account; `account list` is the source of truth. A store written by an older build is migrated once at open: accounts, every per-chat table, the roster, PEP, OTR and the module state are re-keyed, duplicate chats merged without losing a message (the earlier copy of a message kept, the newer read point or draft), store files renamed through a temporary name so a case-insensitive filesystem is safe. No schema-version bump, so rc3 can still open the store. Hosts: from the first event after the upgrade `-acc` and chat JIDs are the normalised ones.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
another-im/libtacky#19
No description provided.