feat(backend): mls_key_packages schema with upload and fetch endpoints - #530
Open
Olorunfemi20 wants to merge 1 commit into
Open
feat(backend): mls_key_packages schema with upload and fetch endpoints#530Olorunfemi20 wants to merge 1 commit into
Olorunfemi20 wants to merge 1 commit into
Conversation
feat(backend): mls_key_packages schema and upload/fetch endpoints Devices publish public MLS KeyPackages up front so any group member can add them to a group while they are offline. - mls_key_packages table keyed on devices.id, storing only public key material, with a partial index for claiming the next available package and a (device_id, package_hash) unique index so re-uploads dedupe - POST /devices/:id/mls-key-packages uploads a batch, owner-only, validating base64/size via the shared key validator, rejecting expired packages and capping the per-device inventory at 100 - GET /users/:userId/devices/:deviceId/mls-key-package claims exactly one package inside a transaction using FOR UPDATE SKIP LOCKED, flipping consumed so a package is never handed out twice; 409 on exhaustion - emits mls_key_packages_low to the device and user rooms once the remaining stock reaches the low-water mark, mirroring prekey replenishment Adds docs/mls-key-packages.md covering the data model, both endpoints, the single-use guarantee and the replenishment contract. @
|
@Olorunfemi20 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #365
Summary
Devices can now publish public MLS KeyPackages ahead of time, so any existing group member can add a device to a group without that device being online. This adds the
mls_key_packagestable, the upload and claim endpoints, and the replenishment signal.Data model
New table
mls_key_packages(migrationdrizzle/0001_mls_key_packages.sql, generated by drizzle-kit with its meta snapshot):iduuiddevice_iduuiddevices.id,ON DELETE CASCADEcipher_suiteintegerkey_packagetextpackage_hashtextkey_package, dedupe keyexpires_attimestampconsumedbooleanconsumed_attimestampcreated_attimestampTwo notes on the schema:
deviceId -> user_devices.id. That table no longer exists onmain— feat: conversation mute/archive per-user settings #103/feat: add reusable Avatar component #107 mergeduser_devicesinto the canonicaldevicesregistry, which is whatmessages.senderDeviceId,message_envelopes.recipientDeviceIdandpush_subscriptions.deviceIdall reference now. The FK points atdevices.idto stay consistent with the rest of the schema.(device_id, package_hash)rather than on the package itself. A KeyPackage can be up to 4 KiB, which exceeds the btree index row limit, so the SHA-256 is the conflict target.Only public material is stored. The HPKE init private key and the signature private key stay on the device.
Endpoints
POST /devices/:id/mls-key-packagesBatch upload, owner-only, rejects revoked devices.
{ "cipherSuite": 1, "keyPackages": [ { "keyPackage": "base64-..." }, { "keyPackage": "base64-...", "expiresAt": "2026-12-31T00:00:00.000Z" } ] }Response:
{ "uploaded": 2, "duplicates": 0, "capped": false, "remaining": 2 }Server-side validation:
keyPackageis routed through the existingMlsKeyPackageSchemainsrc/lib/keys.ts(valid base64 decoding to 32-4096 bytes). That schema was already written and carried a comment saying no endpoint consumed it yet; this PR wires it up and updates the comment.expiresAt, when present, must parse as ISO-8601 and be in the future. An already-expired package would only create inventory that can never be claimed.422; below it, an oversize batch is trimmed to the remaining slots andcapped: trueis returned, matching how the one-time prekey endpoint behaves.ON CONFLICT DO NOTHING, so retrying a request that timed out mid-flight reportsduplicatesinstead of inflating the inventory.GET /users/:userId/devices/:deviceId/mls-key-packageClaims exactly one package.
:deviceIdmust belong to:userIdand must not be revoked, otherwise404— the same narrowing the existingkey-bundleroute uses so a caller cannot probe for devices without knowing who owns them.Optional
?cipherSuite=<n>restricts the claim, since a group can only add a device whose package matches the group's cipher suite.{ "deviceId": "uuid", "cipherSuite": 1, "keyPackage": "base64-...", "expiresAt": null, "remaining": 41 }The claim is atomic.
SELECT ... FOR UPDATE SKIP LOCKEDinside a transaction picks the oldest available row and flipsconsumedbefore returning, so two members adding the same device concurrently receive two different packages. This matters beyond bookkeeping: MLS treats a KeyPackage as single-use, and reusing one across two Add proposals would let a compromise of one group's state compromise the other's. Exhaustion therefore returns409rather than re-issuing a used package.consumedis a flag rather than a delete, so the record of which packages were issued stays auditable — the same choicedevice_prekeysmakes.Replenishment
Every claim recounts the device's available stock. At 10 or fewer remaining, the server emits
mls_key_packages_lowwith{ deviceId, remaining, threshold, cap }to two rooms:room:device:<deviceId>so the device tops up immediately if connectedroom:user:<userId>so a sibling device can surface the prompt when the low device is offlineExhaustion (
remaining: 0) is below the watermark by definition, so a409also carries the signal.Tests
src/__tests__/mlsKeyPackages.test.ts— 21 cases covering ownership and revocation rejection, base64 and size validation at both ends of the MLS window, expired-package rejection, in-batch and cross-request dedupe, the cap and batch trimming, atomic claim withconsumedflipped rather than the row deleted, the watermark boundary in both directions, and the exhausted-device path.Full backend suite passes apart from three files that already fail on
mainbecauseweb-push,socket.io-clientandioredis-mockare not installed in the local workspace.eslint srcreports no new problems, and the new files are prettier-clean.Docs
apps/backend/docs/mls-key-packages.mddocuments the data model, both endpoints with their full error tables, why single-use is enforced the way it is, the replenishment contract, and the client sequence.