Social Home household OS · federated · open

Security model

What is sealed, what is signed, what the relay can still see — and what we haven't solved yet.

This page is the plain-language twin of the project’s principles: every guarantee below is written first for the person who runs the household, and then — in the “Under the hood” blocks — for the person who wants to check it against the code. If a word is new, it is in Words we use.

One rule, stated honestly

Nothing leaves your household readable. The shopping list, the book-club chat, the photo of dinner, the dentist appointment — all of it is sealed before it leaves your home and only opened in the home it was sent to.

The rule is about the wire and about every machine in between: the relay, the network, and other members’ servers that pass a post along.

Under the hood

Encryption is server-to-server: your household’s server seals, the receiving household’s server opens. There is no per-device key and no claim of device-to-device secrecy.

Who it protects you from

Someone who……gets
runs the GFS you usesealed envelopes, routing data and timing — never a word of content
watches the networksealed, padded traffic between known addresses
was removed from a spacenothing posted after they left — the space got a new key
finds a stray invite linkinto the space, if it was a GFS link — treat those like keys; a Local link does nothing for a stranger
has root on your home’s servereverything — it is your server; protect it like the rest of your home network
is an admin of a space you are ineverything in that space, same as any member — admins aren’t a backdoor

The last two rows are the honest ones: Social Home protects your household from the outside, not from itself.

Encrypted home to home

Every message, post and calendar event is sealed in an envelope in your home and signed, so the receiving household can check it really came from you and that nobody changed it on the way. Direct messages work the same way: sealed from your home to theirs.

Under the hood
  • Envelopes: AES-256-GCM. Signatures: Ed25519 over the envelope.
  • The only plaintext on the wire: event_type, from_instance, to_instance, space_id, epoch.

It fails closed

There is no “encrypted / not encrypted” switch and no fallback to sending in the clear. If a space can’t be sealed, the message stays home. An envelope with a bad signature, or one that arrives far too late, is dropped — no exceptions, no “trust this one anyway” mode.

Under the hood
  • No plaintext fallback; no trusted-instance mode.
  • Timestamp window ±300 s; replay cache 24 h.
  • Any signature failure drops the envelope before it is parsed further.

Keys and rotation

Your household gets its own identity key the first time it starts. When you pair with another household, the two of you agree a shared secret over a QR code or a short spoken code, so nobody can slip in between.

Every space has its own key, and that key changes every time the membership changes. When someone leaves the book club, the club gets a new key; they can’t read anything posted after that. When an admin is removed, the key that signs admin decisions changes too.

The keys on your disk are wrapped under a master key. If you installed the add-on, they travel with your normal Home Assistant backup; on a standalone install you download a Recovery Kit and keep it somewhere safe.

Under the hood
  • Identity: Ed25519. Opt-in hybrid Ed25519 + ML-DSA-65 signatures (needs liboqs).
  • Pairing: X25519 + HKDF-SHA256, authenticated by the spoken code.
  • Space content: per-epoch AES-256-GCM key, rotated on every membership change. Authority key rotated on admin revocation.
  • At rest: keys wrapped under a KEK (key-encryption key). Recovery Kit .shrk = scrypt + AES-256-GCM, for standalone installs; the Home Assistant backup covers add-on installs.
  • Residual: key exchange is X25519 only — not yet post-quantum. Hybrid signatures do not make the key exchange post-quantum.

What a relay sees

The GFS (Global Federation Server) — the relay that helps households find each other — carries the posts of a global space to households you aren’t paired with: followers, and members who joined with a GFS link. Paired households get them directly or over the mesh. It carries sealed envelopes, padded to a few fixed sizes, and fans them out. It stores no content: if a household is offline it keeps the sealed envelopes for a day, then lets them go. See GFS in the glossary.

ModeWhat the relay learns
Trusted (default)that someone in the space posted, and when — never which household
Strict (opt-in)the same, and it can’t tell which households are allowed to write
Private space, GFS onwhich households belong to the channel — never the space’s name, id, key or content

Only the GFSs the owner picked for a space are involved. In every mode the relay still sees the sender’s IP address, the timing, the size bucket and which households receive a space’s posts. A plain membership sync tells the GFS nothing.

Under the hood
  • Routing metadata only: space_id, event_type, size bucket, timing, subscriber set, source IP.
  • When the host household relays a post, the request names no household at all: {space_id, event_type, payload} on a cookie-less session, and only to a GFS that has proved anonymous_publish — otherwise nothing is sent.
  • When a member publishes for itself (so the host needn’t be online), it signs with a shared per-epoch writer key in both modes, so the GFS can’t tell which member posted. It sends to every GFS on the owner-signed list (at most 8), over a cookie-less session pinned to the listed address, after that GFS proves it holds the listed key. In strict mode writers also subscribe in the same shuffled batch as followers.
  • Private spaces: private_gfs off by default; when on, an opaque 128-bit channel id on the GFSs the owner picks.
  • Padding buckets: 1 / 4 / 16 / 64 / 128 KiB for member publishes, plus a 191 KiB bucket for envelope relay.
  • Offline recipients: queued 24 h, at most 2000 envelopes or 64 MiB per recipient. Your household retries with backoff: 5 s, 30 s, 2 min, 10 min.

Mesh and calls

If two member households can’t reach each other directly, a post can hop through other members’ servers. Each hop is sealed for the final recipient, so the servers in between carry it but can’t open it.

Voice and video calls run directly between the participants. When a direct connection isn’t possible, a TURN server — a relay just for calls — passes the stream through, and it only ever sees encrypted media.

Under the hood
  • SPACE_ROUTED forwarding: at most three hops, sealed to an ephemeral X25519 key of the recipient.
  • Calls: WebRTC with DTLS-SRTP between participants; a TURN fallback relays ciphertext only.

Apps

Apps from the catalog run in a sealed box inside Social Home. An app can’t phone home, can’t load anything from the internet, and can’t be swapped for a different version behind your back. Apps travel directly between households; the GFS is not involved.

Under the hood
  • Bundles sha256-pinned; 1 MiB cap.
  • Sandboxed iframe (allow-scripts only) with connect-src 'none'.
  • Distributed peer-to-peer between paired households.

The small things

  • A space pinned on the GFS map has its location rounded to about 11 m — the street, not the front door.
  • Posts can’t load images from other websites, so nobody can plant a tracking pixel in your feed.
  • Link previews are fetched by the author’s household only, never by every reader. A household admin can turn them off.
  • Push notifications carry only the title; the content waits until you open the app.
  • Protected accounts for minors are refused outright from the Bazaar, public spaces, public Moments, public highlight links and API tokens — see Family safety.
Under the hood
  • GPS truncated to 4 decimals (~11 m).
  • CSP img-src 'self' data: blob: blocks external images in user content.
  • Link previews: fetched by the author’s household, SSRF-guarded, admin-disableable.
  • API and WebSocket responses are minimised to what the view needs.
  • min_age is checked on every path that seats a member.

How we keep it true

Every rule on this page has tests behind it, and those tests run before every release. If one fails, the release doesn’t ship — there is no “we’ll fix it next time”. Changes to security- sensitive code get a second, deliberately adversarial review, and every change to a principle gets a dated sign-off in the principles file, with whatever it leaves unsolved written down next to it.

What we don’t have yet: automated scanning of third-party dependencies. That is on the list, not in the pipeline.

Under the hood
  • 68 protocol test files tagged security; they block a release regardless of coverage numbers.
  • CI: pytest with 90 % branch coverage, the security tests as a separate step, ruff, mypy, eslint, tsc, vitest.
  • Sign-offs live in docs/principles.md.
  • No CodeQL, bandit or dependency scanning yet.

Known residuals

The things that are true today and that you should know before you trust us with the group chat:

  • A GFS the space’s owner picked, but your household never connected to, sees your IP address when you post there. Because anonymous writers can’t be told apart, a GFS can’t ban one household; the owner removes a writer by rotating the space key.
  • In every mode the relay sees your IP address, the timing and the size bucket of each envelope, and which households receive a space’s posts.
  • Key exchange is not yet post-quantum; only signatures have an opt-in post-quantum variant.
  • There is no automated dependency scanning yet.

Reporting a problem

If you find a hole, please tell us privately rather than in a public issue: open a report under GitHub Security Advisories in the social-home-io/socialhome repository.