Repository navigation
Releases: AgroUPlus/Agro
Release list
v0.3.1
New Contributors
- @dependabot[bot] made their first contribution in #60
Full Changelog: v0.3.0...v0.3.1
v0.3.0
v0.2.0 — presence the server cannot read
The server half of Wanda v1.5.0-beta.5. Deployed and running; user_version 43.
Tagged at Cargo version 0.2.0, the first release since v0.1.0.
Sealed social presence
A handoff can be end-to-end encrypted: the client seals the track metadata and the server relays an envelope it cannot open, with track_title and artist_name carrying a placeholder.
That envelope is sealed to the account's own vault key, so only its other devices can read it — which is what a handoff is for. It also meant the social feed, which reads those same plaintext columns, showed every sealed session to every friend as "Private Session" regardless of show_now_playing. Encrypting a session and being seen by friends were mutually exclusive.
handoff_presence_ciphertexts resolves that. It holds the same metadata sealed once per friend device public key from user_device_keys, exactly as drop_note_ciphertexts holds a note, and each viewer is handed only the copy addressed to the device it asks from. The server stores N ciphertexts and no key to any of them.
friendsNowPlaying,friends,listenAlongandstartListenAlongall take adeviceIdand resolve through the viewer-aware projection.friendsmatters as much as the feed does: it seeds the client's presence list on a graph refresh.FRIEND_PRESENCEandLISTEN_ALONGare sent per recipient rather than broadcast. A single fan-out would hand every friend the first one's ciphertext — bytes none of the rest can open. The listen-along loop was already per-listener, for the pairwise LAN token.- Omitting
presenceCiphertextsmeans leave the stored copies alone, so a heartbeat does not re-seal metadata that has not changed. An empty set clears them. Only a track change pays for a new set.
This changes what the server can read, never who is allowed to look. show_now_playing and incognito are enforced exactly as before, on rows the server cannot decrypt, and the read-time freshness check (NOW_PLAYING_STALE_AFTER_SECS) is unchanged.
Copies are released when the session goes stale, when a device withdraws its key, when a friendship ends, and when an account is deleted.
The migration bug this release exists to not ship
migrate applies MIGRATIONS by index and skips by stamp, so an entry's version is its position. The encrypted_payload column was originally added beside the other handoff migrations rather than appended, which moved user_device_keys from version 41 to 42.
Any database already stamped 41 would have seen 41 <= 41, skipped the new entry permanently, and re-run the device-key registry instead. The column would simply never exist, and every query naming it would fail at runtime. Only freshly created databases would have worked.
No test could see it: they all build from scratch, and a fresh database applies the whole list in order whatever order it is in.
Fixed by appending, and pinned so it cannot recur — SHIPPED records a digest per index and migrations_are_append_only compares against it. The digests are recorded rather than derived; recomputing both sides from the same list would only ever agree with itself.
Retention and deletion
drop_note_ciphertextsanduser_device_keyssurvived account deletion. Nothing deleted from either, no cascade was declared, and a recreated account inherited the public keys of the devices the old one registered. Both now go, with the drop ciphertexts removed before the drops they are keyed to.- Sealed presence copies have their own short TTL anchored to the five-minute now-playing staleness window, not the 30-day handoff row TTL. Past that point nothing can read them and they are only being stored.
Hardening
encrypted_payloadhad no length bound — only the global 2 MB body limit. It is bounded now, as is each sealed copy, with a cap on how many one handoff may carry.replay_afterispub(crate)so the boundary suites can assert it: what a socket is sent is half of what one account can learn about another, and a frame carrying somebody else's ciphertext is invisible to any query-level assertion.
Still plaintext, deliberately
Cross-account popularity (db_popularity.rs) groups across accounts and no per-recipient sealing permits that. It is opt-in, and it is now the one documented plaintext path — see SECURITY.md.
Tests
410 pass. New coverage: a friend receives only its own copy; a device with no copy gets the placeholder rather than someone else's; a keyless asker gets nothing; the visibility flag still wins over a stored copy; a heartbeat preserves and an empty set clears; unfriending and key withdrawal drop copies; the sweep removes stale ones; oversized payloads are refused; both fan-out frames carry only the recipient's copy; and what a stolen database would show of a live sealed session.
Not verified
The two-account end-to-end — two devices, a real friendship, this server — has not been run. The wire format between client and server has never been exercised against a live pair.