Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HMAC V3 #1380

Closed
6 tasks done
codabrink opened this issue Dec 5, 2024 · 5 comments
Closed
6 tasks done

HMAC V3 #1380

codabrink opened this issue Dec 5, 2024 · 5 comments
Assignees

Comments

@codabrink
Copy link
Contributor

codabrink commented Dec 5, 2024

We have HMAC in v2, we want to bring it to v3.

High level implementation:

  • The HMAC key will be derived from:
    • A random device-synced root secret
    • The message’s respective group’s id
    • The number of 30 day periods since epoch
  • The root key will be changed when an installation is revoked.
  • The root secret will be stored in a new user_preferences table to look something like this:
CREATE TABLE "user_preferences"(
    -- The latest id is the current preference
    id INTEGER PRIMARY KEY ASC,
    -- HMAC root key
    "hmac_key" BLOB,
    -- Nickname (example column) 
    "nickname" TEXT,
    -- Database export path (example column)
    "default_export_path" TEXT
);
  • The key will be device-synced via generic proto sync message with a serialized enum as a payload to allow for easy extensibility for syncing new preferences without altering the protos any further.
  • This generated KDF secret will be used to sign the encrypted payload, generating 3 signatures (current 30 day epoch, and +/- 1 epoch)
  • The notification server will then use these keys to check if it should send a push notification in the same fashion as it did in v2.
  • Ideally there will be no changes to the push notification server.
  • Add a preference update stream to the FFI so that integrators can know when to resubscribe to push-notifications.
@codabrink codabrink self-assigned this Dec 5, 2024
@richardhuaaa
Copy link
Contributor

This looks good to me, love how succinct and clear it is. One thing I've been wondering recently is if rotating the hmac root key only on revocation is frequent enough, but would be good to get feedback from @franziskuskiefer.

The other thing that might be good to describe is the algorithm that determines when a key is generated and rotated, and what happens in race conditions (e.g. key is rotated as you're using it, or if two installations try to generate a new key at the same time). These don't need to be handled perfectly, but would be good to enumerate the worst case scenarios.

@keks
Copy link

keks commented Dec 10, 2024

On the call it sounded like you were planning to sync the root keys using an MLS group that contains all clients/devices of a user. If that is still the plan, you could also export the root key from the MLS key schedule instead of regularly generating new keys and sending them through the group. That would mean that removing a device from the user's group will immediately produce a new key known to all devices. The mechanism is called "Exporter Secrets" (RFC, OpenMLS Docs). For exporting, you need to provide both a label and a context. You could use something like "xmtp hmacv3 root" as the label and use some user id as the context.

I assume you want to use HKDF-Expand for deriving from the HMAC root key. The info argument should contain the group id and epoch, and ideally also some user id. When encoding the group id and epoch for the info argument to the expand function, you need to ensure that you do that injectively. This means that there are no two tuples (user_id, group_id, epoch) and (user_id', group_id', epoch') s.t. encode(user_id, group_id, epoch) == encode(user_id' group_id', epoch'). For example if you just concatenate individual text encodings, ("bob", "team_no1", 23) and ("bob", "team_no", 123) will both produce "bobteam_no123". We want to avoid that happening.

Common strategies to achieve injectivity are to length-prefix all fields, or at least all variable-length fields. You can test whether your encoding is injective by checking that you could unambiguously parse the original data from the encoded version.

Ideally you also add some domain separation. This could be done by encoding ("xmtp hmacv3 group", user_id, group_id, epoch), so future protocol iterations will never use the same string in the derivation.

When using MLS exporter secrets, there is one more thing you could do: instead of first exporting the root key and then deriving the per-group keys from that, you could include all the group-specific information in the exporter context and directly export the per-group keys. This has the theoretical benefit of less keys being stored in multiple places, and maybe less overall complexity. But it would mean that you need to keep track of a lot more keys, and it's not clear to me whether it's really worth it.

Regarding the use of the term user id here: I think I remember xmtp not assigning ids to users. If that is true, the user id could be the installation id of the first device added by the user. Did you have plans for what the group id of the per-user device sync group would be? Sounds like that would also be something like a user id, even though it could be an id that only the user knows about.

I hope this helps, let me know if something is unclear.

@codabrink
Copy link
Contributor Author

Hi @keks, thanks for the comment. How often does the root key from the MLS key schedule change?

@keks
Copy link

keks commented Dec 11, 2024

Good question! I meant to mention this but apparently forgot.

Exported secrets in MLS are tied to the epoch of the group, which changes whenever the group state changes, e.g. because a device/client is added or removed from the group or a group member cycles their own key material. This means that as long as you are in the same epoch, the OpenMLS export_secret function returns the same key (given the same label and context, of course). If the group has changed epochs, a new key will be returned.

More context: Group state changes occur when processing commit messages, which have to be applied in-order. So as long as the new root key is always exported after a commit was applied, there shouldn't be the risk of missing an intermediate root key after having been offline for a while.

@codabrink
Copy link
Contributor Author

This completes the libxmtp portion of #422

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants