Please report vulnerabilities privately through GitHub's private vulnerability reporting, not in a public issue or pull request.
Include what an attacker can do, the steps to reproduce it, and the commit or version you tested. You should get a first reply within a week.
typstry is pre-1.0. Security fixes go into a new release and into nightly (the image tag and the CLI build that follow main). Older releases do not get backports, so upgrade to the newest release. Please check that the issue still exists in it or on main.
The documentation describes how the encryption works. In particular, these are in scope:
- A server, or anyone who can read its data directory, learning the contents of documents or uploads.
- A view link being used to make edits that other clients accept.
- Getting at a project's content or key without its share link.
- A removed member reading data written after their removal, or following the group to learn the new links.
- A client, or the server, passing itself off as another member, or adding a member without an external commit made with the link.
- Getting the server to replace a project's links without the owner key.
- Code execution or file access outside the project through the CLI, the server, or a compiled Typst document.
- The roster is authenticated. Every device has its own key, which signs the device's member key in each project. Every update is signed with that member key, and the names shown for authors and cursors are the members' own profiles, which only members can read.
- Only the project's owner, the person who created it, can remove members or reset the links. The group state names the owner by a tag derived from the person and the project's first document key, so it differs between projects. Every member checks owner rights against the devices that person's graph lists as valid, so an editor or the server cannot grant them to itself. The server enforces this as well: it replaces a project's links only for a rotation signed with the project's owner key, which only the owner's devices hold and which changes with every rotation. Devices take the current owner key from the server, and from a commit only when the group accepts it as a rotation. They keep every owner key they made or received, whichever one the server names, so a server that misreports the key can stall rotations but cannot make a device drop the real one.
- Removing a member (or resetting the links) switches the project to a new key generation, and the members who stay get the new links automatically. The removed device cannot read what is written afterwards. An owner device whose keyring lags behind the group fetches the current one before it rotates or resets, so it never drops a newer generation.
- Presence is sealed under a key of the current group epoch, so a removed member stops seeing cursors with the next epoch.
- Each device has its own key, and a person is a graph of signed entries that add and revoke devices. No secret is shared between a person's devices, except the owner key of each project they own and the link to their personal space. The owner keys also travel in the personal space, so every device of the person holds the key of every project the person owns. A device is shown as that person only if its leaf key is signed by a device the graph lists as valid, and a commit can never swap a leaf's identity for another one. Only an external join with the link replaces the leaf, which is how a device that switched persons moves its leaf over. Members merge a person's graph only from leaves bound to one of that person's devices, so a revoke that reaches any one of the person's leaves applies to all of them. Removing a person removes all of their devices in one commit.
- A revoked device loses its owner rights as soon as a leaf in the group carries the revoke. The owner's devices that hold the owner key remove every revoked device they see, of any person, and replace the links.
- Each person has a personal space per server: a space like any project, whose members are only that person's devices and whose document is the list of the person's projects with their links. Pairing hands the new device its link and owner key inside the grant sealed under the pairing code, so it only ever comes from one of the person's own devices. Revoking a device removes it from the personal space first, so it stops seeing projects added afterwards, and then resets the links of every project in the list that the person owns.
- A recovery key is a device key spelled as 17 words. It can only add and revoke devices, and its revokes outrank those of devices added after it was created. It is never accepted as a member of a project and never counts as the owner, but it can open the owner key of each project the person owns. Each device's leaf also carries the link to the person's personal space, sealed to the person's recovery keys, so recovering through the link of any one project the person is in brings back the personal space and with it the list of all their projects and the owner keys of those they own.
These are not vulnerabilities:
- A link grants access until it is reset or its holder is removed. Anyone holding a share link can read the project, and anyone holding an edit link can change it.
- The app only lets you delete your own comments, but that is a rule of its UI, not of the encryption: comments are document data like the text, so any editor can delete or resolve any comment. Who wrote a comment is attributed like any other update.
- A person's graph and the device keys are sealed under a key derived from the project's first document key, so that every member checks them the same way even while its keyring lags behind a rotation. Anyone who once held a link to the project, a removed member included, can read the person ids of later members together with the server, and can test a guessed person against the owner tag.
- Revoking a device only removes it from a project once one of the owner's devices that holds the owner key opens that project. In projects someone else owns, the revoked device keeps its access until then.
typstry devices revokeand the browser's devices dialog list the projects they could not reach. - A device you pair can read the links of every project in your list, including projects added later, and the owner key of every project you own, until it is revoked. A stolen device can therefore race you to reset the links of any of your projects, not only those it had opened. Revoke it quickly: revoking resets the links of every project you own. Links to projects someone else owns stay valid for it until their owner removes it or resets them.
- Before it is revoked, a device can also write entries into the list: links that open some other space under a project's name, or owner keys that are valid but of no use. Devices skip entries dated more than a day ahead, file a link that turns out to open another space under the space it opens, take owner keys from the list without ever pushing out a key the list does not also hold, so planted or stale entries cannot evict a key a device holds nowhere else, and only ever use an owner key the server names, but such entries can make the list less useful until they are replaced. Revoking only resets your project links once your personal space has been secured, since new links written into a list the revoked device can still read would be useless.
- Recovering picks, among the personal spaces the person's devices point to, one the recovery key holds the owner key of, preferring the one most devices point to. Until it is revoked a stolen device is the person, so it can make a space of its own that passes these checks and point to it. If it is the only device with a pointer, or ties with the one honest device left, recovery may pick its space and keep the person's list there. The recovered device still removes the lost devices from that space.
- A device trusts the personal space it was handed, and checks that space's document kind only after merging it. A forged binding would therefore receive the device's project list, but forging one takes a compromised pairing, which hands over the person anyway. A project link that turns out to open some other kind of space is refused before anything local is uploaded.
- Between losing a device and revoking it, the device can do anything its person can, including acting as the owner and flagging the person's other devices as recovery keys, which stops them from being used in projects.
- The server accepts any rotation signed with the current owner key, even one the members reject. A stolen owner device that still holds that key can reset the links and lock the others out. Once the owner has rotated in a project the stolen key is useless there, but if the thief rotates first, the owner loses the links of that project for good and has to share a new copy. Revoke stolen devices quickly.
- Two devices of the same rank that revoke each other are both revoked, since nobody can tell which of them was stolen. A revoked device can force this against the device that revoked it at any later time, which costs the person that device too. A recovery key outranks the devices added after it was created, and a device added before it can at most tie with it, so revoke stolen devices with the recovery key. If the person's only recovery key is stolen, its revokes beat those of all their devices and the thief can take the person over. If a tie leaves the owner without devices, the project has no owner until the recovery key adds a new device. A holder of one of a person's keys can also make all of that person's devices show as unverified by writing more entries into the graph than fit, since members refuse to guess which copy is right.
- A graph holds at most 48 entries and 8 KiB. Every device added, every revoke and every change of name or color uses one entry. The last 8 entries and 2.5 KiB are reserved for revokes and for entries signed by a recovery key, so a stolen device that fills the graph cannot stop the person from revoking it or recovering. This holds against a device that fills the graph in passing, not one that sets out to fill the reserve too: it can spend the reserve on revokes, or add a recovery key of its own and spend it on that key's entries, since any valid device may add a recovery key and the graph cannot tell a thief's from the person's. A person who has used up the rest cannot add devices or change their profile anymore, except through a recovery key, and once the reserve is used up as well, not even revokes fit.
- After a group reset, a revoked owner device that rejoins before any up-to-date device of the owner is judged against its own outdated graph and counts as the owner until a device that knows the revoke rejoins.
- Members cannot tell whether a person is the one they expect. The fingerprint shown next to each person and device is there to compare out of band, and nothing pins it yet.
- Recovering with the recovery key needs a link to at least one project the person is in, since the graph and the sealed link to the personal space are only stored on devices and in the projects. A person who somehow ends up with two personal spaces on one server (say, two devices that were never paired both made one) is not merged back into one. Its last word is a checksum that catches most but not all typos. A wrong key only fails to find the person.
- A device that opens a link before it is paired joins as a person of its own, and members see that person and its fingerprint until the device pairs and joins again as the person it joined. The prompt that offers this switch trusts the user's answer about which device already belongs to them.
- Device names are local labels. Pairing tells both devices each other's name, and they never reach other people. The name is chosen by the joining device and proves nothing. Both devices show the new device's key fingerprint, so approve only if they match. A pairing link opened in the browser waits for a click before it connects.
- Pairing uses SPAKE2 from the
spake2crate, which has not had an independent audit. A short code gives an attacker on the relay one guess per attempt (1 in 65,536 for two words), and the device that shows or enters the code for the person gives up after one failed attempt. Anyone who guesses a channel number can make a pairing attempt fail, but cannot learn or change what is exchanged. - Every device that opens a link, view links included, joins the group and stays a member until it is removed. Removing anyone rotates the links for everyone who stays.
- A removed member keeps everything they already had. Document content has no forward secrecy: a link holder can derive the keys of every earlier generation, and the full history is in the document anyway.
- A newly paired device of the owner gets the owner keys through the personal space once it has synced, and until then only after another device that holds the key opens the project and hands it over.
- If every device of the owner is lost and there is no recovery key, the owner key is lost with them and nobody can remove members or reset the links anymore. Editing keeps working. Create a recovery key, pair a second device, or back up the CLI config directory and each folder's
.typstry/to avoid this. - Any editor can still reset a stalled group. The reset changes neither the links nor the owner key, and it carries the owner tag over. A member that knew the project refuses a reset that rewrites the keyring it saw or changes the owner tag, and it refuses any keyring that does not extend the one it holds, after rotations too, and the group stays stalled for that member until someone handles it by hand. Documents keep syncing meanwhile. A device joining for the first time after a malicious reset trusts whichever owner that group names, and a device that lost its state cannot tell a rewritten keyring.
- A removed editor working with the server could inject updates backdated to the old generation. They would still be signed by, and so attributed to, that editor.
- A compromised server can withhold or roll back updates, and it can stall the group (stop commits from being delivered). Editing keeps working, but removals and new links cannot reach other members until it behaves.
- Any link holder can stall the group with a commit the others reject, until an editor resets the group. Documents keep syncing meanwhile. A commit from an editor that removes anyone counts as such a commit.
- A join can be crafted so that only some members reject it. That splits the group: those members stop following while the rest carry on, until an editor resets the group. Without the server validating commits, MLS cannot prevent this.
- Retired links still let a member who was offline across a rotation read the commit log and follow it to the new links. A group reset, or the server pruning the commit log (by age, by count or by size, and retired links expire with it), drops those messages, and such a member then needs a new link out of band. A person's own devices also find the newest link they know of in the personal space.
- The group's cryptography runs as wasm on the browser's main thread, so a large group can briefly stall the UI.
- The server cannot tell a personal space from a project by its contents, but it can guess one from its size, its traffic and a roster that never grows beyond one person's devices.
- The server sees space ids (one per project, and one per person for the personal space) and share-link tokens, record headers, blob sizes, connection metadata, and the group's metadata: how many members it has, when commits happen, their epochs and sizes. It also sees each space's owner public key and that it changes with every rotation.
- The server serves the app's JavaScript, so it could serve a modified client.
- The browser keeps its keys unencrypted. Link secrets and the binding to the personal space are in
localStorage(typstry:key:*,typstry:space). The device key, the MLS group state and the owner keys are in IndexedDB, and so are the documents themselves, in plaintext. Anything that can read the browser profile, a cross-site scripting bug in the app, or a malicious browser extension gets all of them. The CLI keeps its keys the same way, in files only your user can read, in its config directory and each folder's.typstry/. - A server reachable from the internet lets anyone create projects and upload blobs, since there are no accounts. The server enforces a storage quota per project and a ceiling on open connections; limits per client address (creating projects, uploading, pairing, open connections) are left to the reverse proxy, and the provided
compose.prod.yamlsets them up in Traefik. Without them one visitor can use up the instance. None of this is access control. See Who can use your instance and Abuse limits. - Share tokens (the part of a link before
#) and pairing claims travel in URL paths and query strings. The server never logs them, but a reverse proxy's access log records them unless it is turned off or strips paths and queries, so configure it that way (see Behind another reverse proxy; the Traefik setup incompose.prod.yamlkeeps its access log off). A token alone decrypts nothing, but it gives its holder what the server sees of that project. - The browser loads Typst's default fonts from
cdn.jsdelivr.netand imported packages frompackages.typst.org. Those hosts see your IP address, that typstry is in use, and which packages your documents import. A build can serve the fonts from a mirror instead (see What the browser loads from elsewhere). - Presence updates carry no counter, so the server can replay a member's earlier cursor and name within the same group epoch. Clients order presence by its own timestamps, so a replay shows at most a stale cursor until the member's next update or until it times out.