End-to-end encryption
Firefly encrypts all message content on the device. The server stores and forwards ciphertext only.
| Conversation | Protocol |
|---|---|
| 1:1 chats | Signal protocol (X3DH-style prekeys with post-quantum KEM prekeys, Double Ratchet) via libsignal |
| Groups | MLS via mls-rs, ciphersuite P256_AES128 |
1:1 chats (Signal protocol)
Section titled “1:1 chats (Signal protocol)”Each device publishes prekey bundles. To start a session with someone, a device fetches one of their bundles per device and encrypts to each.
| Request | Description |
|---|---|
POST /user/preKeyBundles |
Upload your bundles (firefly.PreKeyBundleEntries). |
GET /user/preKeyBundles?other=<username> |
Fetch bundles for all of a user’s devices. |
GET /user/preKeyBundles?ids=<address_ids> |
Fetch bundles for specific addresses. |
GET /user/preKeyBundles?id=<address_id>&onlyIds=true |
List IDs of your remaining bundles. |
DELETE /user/preKeyBundles?addressId=<id>&ids=<ids> |
Delete used bundles. |
POST /user/conversation?other=<username>&settings=<n> |
Start a conversation. |
Messages are sent with the uploadUserMessage RPC,
encrypted separately for each recipient device and for your own other devices.
Message requests
Section titled “Message requests”If a recipient has disabled message requests (MessageRequestSettings.requests_disabled),
only users they have accepted can message or call them. Others receive error 403.
Groups (MLS)
Section titled “Groups (MLS)”Key packages
Section titled “Key packages”Each device keeps a pool of single-use MLS key packages on the server so others can add it to groups while it is offline. The reference client keeps 32 available.
| Request | Description |
|---|---|
POST /group/keyPackages?address=<address_id> |
Upload packages (firefly.GroupKeyPackages). Package IDs are int32 values chosen by the client. |
GET /group/keyPackages?address_id=<id>&device_id=<id> |
List your own packages. |
GET /group/keyPackages?other=<username>&all=true |
Fetch one package per device of another user. |
DELETE /group/keyPackages?address=<id>&ids=<ids> |
Remove stale packages. |
Creating a group
Section titled “Creating a group”- Create an MLS group locally with the Firefly group extension.
POST /groupwithfirefly.Group { name, description, settings, state }, wherestateis the MLSGroupInfo.- The server returns the group’s numeric
id.
Adding members
Section titled “Adding members”- Fetch the invitee’s key packages:
GET /group/keyPackages?other=<username>&all=true. - Validate each package: its credential must belong to the invitee, be signed by Firefly, and not be expired; skip devices already in the group.
- Create a commit that adds the packages and assigns the invitee a role.
POST /group/commitAndWelcomewithfirefly.GroupCommitAndWelcome(max 4 MiB).- Apply the commit locally only after the server accepts it.
The invitee receives a groupInvite over the real-time connection
(or from GET /group/invites?address=<id>), joins with the Welcome message, and reports
its position with POST /group/member?address=<id>.
Other changes
Section titled “Other changes”Removing members, changing roles or channels, and key updates are ordinary commits sent
with POST /group/commit. The server returns the commit’s assigned id and epoch.
Firefly acts as an MLS external observer: it validates every commit against the group
rules before distributing it. If two members commit for the same epoch, only the first is
accepted; the server rejects the other with 400, and that member must process the accepted commit and retry.
Sending messages
Section titled “Sending messages”Application messages are sent with the uploadGroupMessage RPC. Receivers reject
application messages from epochs other than the current one, so process all pending
commits first.
Roles, channels, and permissions
Section titled “Roles, channels, and permissions”Every group carries a Firefly extension (MLS extension type 65001) describing:
- Roles, each with a permission bitmask.
- Channels, with optional per-role overrides.
- Members and their roles.
- Default permissions for members without a specific role.
| Permission | Bit |
|---|---|
SeeMessage |
1 |
PinMessage |
2 |
AddMessage |
4 |
ManageChannel |
8 |
ManageRole |
16 |
ManageMember |
32 |
ManageGroup |
64 |
The default is SeeMessage | AddMessage. Changes use custom MLS proposals:
| Proposal | Type |
|---|---|
UpdateRole |
30006 |
UpdateUser |
30007 |
UpdateChannel |
30008 |
UpdateRoleInChannel |
30009 |
Proposals are permission-checked by every member and by the server.
Recovery and history
Section titled “Recovery and history”- Re-adds: if a device loses its group state, it asks to be re-added
(
POST /group/reAdd). Another member claims the request (POST /group/reAdd/claim) and re-adds it with a fresh key package. - History sharing: when enabled for a group (
POST /group/history/sync?id=<id>&enabled=true), existing members can share encrypted history with new devices or members using the history RPCs. Chunks are encrypted with keys that are distributed only through MLS. - Encrypted backups: 1:1 conversation history can be backed up to the CDN under
client-side encryption with
/backups/direct.
Plan limits
Section titled “Plan limits”Group size, upgrades, and channels depend on the group owner’s
Lupyd plan. Operations beyond the plan return 403 with an
explanation.