Skip to content

Releases: AgroUPlus/Agro

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 12 Oct 16:52
aab0fdb

New Contributors

Full Changelog: v0.3.0...v0.3.1

v0.3.0

Choose a tag to compare

@Kolbxyz Kolbxyz released this 09 Oct 13:33
df28b7b

Full Changelog: v0.2.0...v0.3.0

v0.2.0 — presence the server cannot read

Choose a tag to compare

@Kolbxyz Kolbxyz released this 04 Sep 18:37
f2acfeb

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, listenAlong and startListenAlong all take a deviceId and resolve through the viewer-aware projection. friends matters as much as the feed does: it seeds the client's presence list on a graph refresh.
  • FRIEND_PRESENCE and LISTEN_ALONG are 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 presenceCiphertexts means 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_ciphertexts and user_device_keys survived 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_payload had 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_after is pub(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.