Sign inSign up

darktrees/blind-sync

By darktrees

•Updated 2 days ago

Blind sync server for the BlindSync app: it relays your devices' ciphertext and never holds a key.

Image
0

3.6K

darktrees/blind-sync repository overview

⁠BlindSync server

The sync server for BlindSync: a small, end-to-end-encrypted, account-free app for a household. It relays ciphertext between your devices. It has never held a key, so it can't read anything it stores.

The app is The Mycelium⁠. It runs in a browser and installs as a PWA, and it holds several things in one place:

  • Next Dose: medication doses, and when the next one is due.
  • Tally Stick: shared expenses, split and settled.
  • Stock Level: how many days of supply are left in each place you keep stocked.
  • Family Manager: a shared family calendar with tasks and reminders.
  • Rep Sheet: workout logging.
  • Voice Notes: record, transcribe and summarise.
  • Loan Book: personal loans between people.

The app keeps a copy of its data on the device and works offline. It syncs through the BlindSync server you give it at setup, and this image is that server.

⁠What it does

  • An encrypted document store. Each shared thing (a person you track, an expense group, a family) is a group of opaque, versioned documents. Writes are checked against the version the device last saw, so two phones editing at once don't overwrite each other.
  • Live updates. Devices hold a WebSocket per group and are told about a change as it lands. A device that was away catches up from a cursor.
  • Invites. A link carries a sealed invite. The server applies its expiry and use count, and can't open it.
  • Encrypted files. Recordings and attachments are uploaded as encrypted chunks. The key, the name and the file type stay inside the encrypted documents.
  • Notifications (optional). Web Push wakes a phone in a pocket when its group changes, and the server keeps the clock for reminders a PWA can't keep itself. A reminder's name is sealed by the phone that scheduled it, and every push is padded to one length.
  • Calendar invites by email (optional). Family Manager can send an event to someone's own calendar app as a standard iMIP invite, through an SMTP relay you configure.

⁠What the server sees

  • Ciphertext, group ids and sizes. No keys, no names, no content. The identity behind your data is a 24-word recovery phrase that never leaves your devices. There are no accounts and no passwords.
  • When things happen. Write times, when a reminder is due, and the IP address of each request (used for rate limits).
  • Request logs are redacted. Group, invite, document and file ids are logged only as a short non-reversible hash, since a group id is what grants access to it.
  • One exception: calendar email. An invite sent by email has to be readable by the recipient's mail client, so it passes through the server and your SMTP relay in plain text. It's off unless you configure it, and the app says so before sending.

⁠Running it

services:
  blind-sync:
    image: darktrees/blind-sync:latest
    env_file: .env
    volumes:
      - blind-sync-data:/data
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped
    mem_limit: 512m
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - "no-new-privileges:true"

volumes:
  blind-sync-data:

The database and the encrypted files both live in /data, so back up that volume.

The app is served over HTTPS, so a browser will only talk to a server that is HTTPS too. Put it behind a reverse proxy with a trusted certificate, then enter that address in the app when it asks for a server, for example https://sync.example.com.

⁠Exposing it

There's no login, by design. A group id is a capability: anything that knows one can fetch that group's ciphertext, and can't read it. To limit what a stranger can cost you, every storage, connection and rate cap has a finite default.

Keep it on a private network such as Tailscale or WireGuard if that suits your household. If you put it on the internet:

  • Publish the port only to the reverse proxy, as in the example above.
  • Set CORS_ALLOWED_ORIGINS to the app address you use. An origin left out shows up as "Can't reach your server" in the app, while /health still loads in a browser tab.
  • Set ALLOW_PRIVATE_NETWORK=false.
  • Keep TRUST_PROXY=true only behind a proxy that sets X-Real-IP. Without one, a client could pick its own address and step around the rate limits.
  • Allow client_max_body_size of at least 2 MB on the proxy for encrypted file chunks.
⁠Configuration

Put these in .env beside the compose file. Keep the VAPID private key and the SMTP password out of the compose file itself.

VariableDefaultWhat it's for
CORS_ALLOWED_ORIGINS*The browser origins allowed to call the API, comma-separated
ALLOW_PRIVATE_NETWORKtrueLets a public app page reach a server on a private address. Turn it off on a public server.
TRUST_PROXYtrueTake the client address from the proxy's X-Real-IP
MAX_TOTAL_BYTES2 GBAll documents across all groups
MAX_GROUP_BYTES8 MBDocuments in one group
MAX_TOTAL_BLOB_BYTES20 GiBAll encrypted files. Size it to the disk.
MAX_GROUP_BLOB_BYTES512 MiBEncrypted files in one group. Raise it for long recordings.
RATE_LIMIT_RPS / RATE_LIMIT_BURST10 / 200Per-address request rate. Opening the app costs about four requests per group at once, so don't set the burst low.
MAX_WS_PER_IP64Live connections per address. A device holds one per group.
VAPID_PRIVATE_KEYemptyTurns on notifications and reminders
VAPID_SUBJECTemptyA mailto: or https: address for push services to contact you
MAIL_ENABLEDfalseTurns on calendar invites by email
SMTP_HOST / SMTP_PORTempty / 587Your SMTP relay. Port 465 is implicit TLS; any other port uses STARTTLS.
SMTP_USER / SMTP_PASSemptyRelay credentials, if it needs them
MAIL_FROMemptyThe organiser address invites are sent from. Keep it fixed: calendar apps match updates to it.

Set any cap to 0 for no limit. Everything else has a working default.

Generate the VAPID key with the image itself:

docker run --rm darktrees/blind-sync -genvapid

Changing the key later cuts every phone off from notifications until the app re-subscribes, which current versions do on their own.

GET /health answers {"status":"ok"}, and the image has a built-in health check. When notifications don't arrive, start from Settings → Notifications → Check notifications in the app. It tests every link and sends a real push to that phone. Then check docker logs for push.

⁠Data and attribution

The server ships no data and fetches none. Everything it stores came from your own devices. It contacts two kinds of outside service, and only when you configure them:

ServiceUsed forTerms
Browser push services (run by the browser's maker)Delivering notificationsThe browser maker's terms. Each push is encrypted to the device, and padded.
Your SMTP relayCalendar invites by emailYour own provider's terms

The binary is built with these open-source libraries, each under its own licence: chi⁠ and go-chi/cors⁠ (MIT), Gorilla WebSocket⁠ (BSD-2-Clause), golang.org/x/time⁠ (BSD-3-Clause), modernc.org/sqlite⁠ (BSD-3-Clause, a translation of SQLite, which is public domain) and the Go⁠ standard library (BSD-3-Clause).

⁠Licence

BlindSync is source-available under the PolyForm Noncommercial License 1.0.0⁠. Personal, hobby, research, educational and charity use are all welcome. Commercial use is not permitted without a separate licence. The full terms are at /LICENSE in the image.

⁠Source

The code lives on a private forge. It's available on request for non-commercial use; get in touch through the contact link on the darktrees Docker Hub profile⁠. To ask about commercial use, use the same link.

Tag summary

Content type

Image

Digest

sha256:4eeb90998…

Size

5.2 MB

Last updated

2 days ago

docker pull darktrees/blind-sync