Open Free and open source — read the code

flamenet-relay

The /e2e/* half of Flamenet Messenger as a standalone service, so the message relay is a standalone service.

It stores public keys and opaque ciphertext. It holds no private key, and there is no path in it that could decrypt an envelope if it wanted to: payload goes into a column and comes back out unread.

It is not an identity provider. Accounts, profiles and the community they are anchored in are the relay's own, and it mints the tokens it verifies against a pinned public key and never calls on the request path.

Running it

swift build -c release
FN_ISSUER_PUBLIC_KEY=<base64 Ed25519 public key> \
FN_RELAY_AUDIENCE=https://relay.flamenet.io \
FN_DB=/var/lib/flamenet/relay.sqlite \
  .build/release/flamenet-relay

It refuses to start without an issuer key. There is no safe default for that value, and a relay that accepts unsigned tokens is worse than one that is down.

Variable Default Notes
FN_ISSUER_PUBLIC_KEY Required. Base64 Ed25519 public key of the token issuer.
FN_RELAY_AUDIENCE Required. The aud this relay accepts.
FN_DB relay.sqlite SQLite file.
FN_HOST / FN_PORT 127.0.0.1 / 8080 Bind address.
FN_WORKDIR inferred Directory containing Resources/ and Public/. Set it in a unit file.
FN_CLOCK_SKEW 60 Seconds of tolerance on a proof timestamp.
FN_BUNDLE_RATE / FN_BUNDLE_RATE_TARGET 120 / 6 Bundle fetches per hour, per requester and per pair.
FN_SEND_RATE 600 Envelopes per hour.
FN_DELIVERED_RETENTION_DAYS 1
FN_UNDELIVERED_RETENTION_DAYS 30 Must stay below the client's retired-SPK retention (45 days).
FN_ATTACHMENTS_DIR attachments Blob directory. Relative paths resolve under FN_WORKDIR.
FN_ATTACHMENT_MAX_BYTES 10485760 10 MiB per blob.
FN_ATTACHMENT_RATE / FN_ATTACHMENT_BYTES_RATE 60 / 200 MiB Per uploader, per hour.
FN_ATTACHMENT_RETENTION_DAYS 30
FN_TURN_SECRET / FN_TURN_HOST unset Unset means GET /e2e/turn answers 503 — calling is off on this instance, which is a different thing from not permitted.
FN_TURN_TTL 7200

Auth

Two modes, deliberately.

Identified routes take Authorization: Bearer <token> plus a proof-of-possession signature. The token is not a bearer credential:

  • scoped to one device (dev), so one stolen from a browser cannot drain the phone's prekey pool or read its inbox;
  • bound to one relay (aud), so a token minted by one instance is not valid at another;
  • bound to the device's IK_sig (cnf), and every request carries X-FN-Timestamp, X-FN-Nonce and X-FN-Signature over method \n path \n timestamp \n nonce \n sha256(body). A stolen token is inert without the key. Proofs are single-use within the skew window.

The issuer's verification key is configuration, never fetched. Fetched over HTTPS, a compromised identity provider could serve a key of its own and mint tokens this relay accepts — which is the exact compromise the split exists to survive. Rotation is a deploy, on purpose.

POST /e2e/messages is different. A sealed submission presents the recipient's delivery key and is otherwise unauthenticated, exactly as the earlier relay had it. Requiring a token there would hand the relay the sender's identity at request time, so sealing would hide the sender from the stored row and from nobody else. Sealed traffic is metered by recipient and by IP, because there is no sender to meter.

Routes

All of SPEC.md §8: devices (§8.1), bundles (§8.2), device list (§8.3), revocation (§8.3a), send and poll (§8.4/§8.5), prekey replenishment (§8.6), sealed attachments (§8.7/§8.8), the group registry (§8.9), and TURN credentials (§8.10).

Two deliberate deviations from what the earlier implementation did, both forced by the same thing — this service is not the identity provider:

  • Group members are ids only, with no name. The earlier server joined a user table for a display name. A relay that caches display names is a relay that leaks them; clients resolve names against the portal, as they already do for buddy lists.
  • Group membership requires a reachable device, rather than get_userdata(). The relay cannot answer "is this a real account". It can answer "can this member receive a single group envelope", which is stricter: fan-out is per device, so a member with none is a silent black hole.

What this does not fix

Stated here because a rewrite implies it fixed everything it touched.

  • Key transparency is still absent. This service is still the sole distributor of prekey bundles; the split reduced its power over key distribution by exactly zero. It is a precondition for federation, not a follow-on.
  • Group rosters are not implemented here yet, and remain server-visible where they are.
  • Profiles are server-visible.
  • IdP compromise still buys envelope deletion, prekey drain and traffic observation. A registration lock stops device registration, not everything.
  • Web client delivery. A separate origin removes third-party scripts; it does not change who ships the code. On the web the relay still serves the client, so "defends against a compromised relay" holds on native and not in the browser.

Tests

node ../test/relay.mjs boots this binary and drives it over real HTTP against SPEC.md §8 — including the negative cases, which are the point: a forged proof, a replayed proof, a token for another relay, a token signed by an unpinned issuer, and a token used against a device it was not scoped to.

It skips rather than fails when the binary is not built, so the JS suite still runs without a Swift toolchain. 60 assertions, including a path-traversal probe against the attachment id and a cross-implementation check that the coturn REST credential this mints matches one computed independently in Node.