Skip to content

AdamsTechnologies/Encryptilock_Server

Repository files navigation

Encryptilock Sync Server

A tiny, zero-knowledge sync server for the Encryptilock password manager. Run it on a NAS / home server (or any box with Docker) to sync your vault across devices. It stores only opaque ciphertext blobs plus an auth verifier — it can never decrypt a vault, and neither can anyone who steals its disk.

  • Stack: Dart + shelf + SQLite — compiles to a single self-contained binary.
  • Stores: the encrypted blob, the public account salt, a verifier (a hash), a key epoch, and a monotonic version. Nothing else.
  • Concurrency: optimistic, via If-Match / 412.

This is the primary "host it yourself" transport for Encryptilock sync. The app also supports WebDAV/Nextcloud/S3 and serverless LAN-QR, but this server is the recommended option.


TL;DR — deploy with Docker

# on the server box, in an empty dir holding this project's files:
mkdir -p tls data
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
  -keyout tls/key.pem -out tls/cert.pem \
  -subj "/CN=encryptilock-sync" \
  -addext "subjectAltName=IP:192.168.1.50,DNS:encryptilock.lan"   # ← your server IP

docker compose up -d --build           # builds the image, runs the container
docker compose ps                      # -> Up (healthy)

openssl x509 -in tls/cert.pem -noout -fingerprint -sha256   # ← pin this in the app

Then point each device at https://<server-ip>:8443, Trust certificate (confirm that fingerprint), and register/enroll. Full walkthrough below.


1. Quick start (local, for testing)

Needs the Dart SDK.

dart pub get
dart test                       # unit + integration tests
PORT=8443 DB_PATH=./sync.db dart run bin/server.dart   # plain HTTP, no TLS
curl http://localhost:8443/healthz                     # -> ok

Add TLS_CERT_PATH / TLS_KEY_PATH to serve HTTPS directly (see below).


2. Deploy with Docker (recommended)

The repo ships a Dockerfile (multi-stage → small debian-slim runtime) and a docker-compose.yml. The compose defaults to direct self-signed TLS, which is ideal for a home/LAN server with no domain.

2.1 Prerequisites

  • Docker Engine + Compose plugin on the server box:
    curl -fsSL https://get.docker.com | sudo sh
    sudo usermod -aG docker "$USER" && newgrp docker
  • A static/reserved LAN IP for the box (set a DHCP reservation in your router).

2.2 Get the files on the box

Copy this server/ directory to the box (it's self-contained — only it is needed to build). A common layout is one dir per container, e.g. ~/docker/encryptilock_server/:

# from your dev machine (Git-Bash/WSL), copy the contents (incl. the hidden
# .dockerignore, which keeps data/, tls/ and *.db out of the image build):
tar czf - -C server --exclude=.dart_tool --exclude=data --exclude='*.db*' . \
  | ssh you@192.168.1.50 'mkdir -p ~/docker/encryptilock_server && tar xzf - -C ~/docker/encryptilock_server'

2.3 Make a TLS cert (self-signed, with your IP in the SAN)

cd ~/docker/encryptilock_server
mkdir -p tls data
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
  -keyout tls/key.pem -out tls/cert.pem \
  -subj "/CN=encryptilock-sync" \
  -addext "subjectAltName=IP:192.168.1.50,DNS:encryptilock.lan"
openssl x509 -in tls/cert.pem -noout -fingerprint -sha256   # save for pinning

On Windows openssl, use -subj "//CN=…" (double slash). The subjectAltName must include your server's IP or the app's hostname check fails.

2.4 Build and run

docker compose up -d --build
docker compose ps                 # STATUS -> Up (healthy) after ~10–30s
docker compose logs -f            # expect: server.start … "tls":true … "port":8443

The docker-compose.yml bind-mounts ./data (the SQLite store — visible + easy to back up) and ./tls (your cert, read-only), and has a healthcheck.

Port already in use? Map a free host port — change ports: ["8443:8443"] to e.g. ["8444:8443"] (keep PORT: "8443" inside), or set PORT and the mapping to the same new value and update the healthcheck port to match. The cert is keyed to the IP, not the port, so it stays valid.

2.5 Firewall

sudo ufw allow from 192.168.1.0/24 to any port 8443 proto tcp   # LAN-only

2.6 Verify

curl -k https://localhost:8443/healthz        # on the box -> ok
curl -k https://192.168.1.50:8443/healthz     # from a LAN device -> ok

2.7 Point the app at it

On each device → Settings → Cross-Device Sync → Transport = Encryptilock server (REST) → Server URL https://192.168.1.50:8443Trust certificate (confirm the fingerprint from 2.3) → Register this device (first device) or login screen → Join existing account (others).

2.8 Updating later

# re-copy the source over the dir (your tls/ and data/ are preserved), then:
docker compose up -d --build --force-recreate

--force-recreate ensures the container swaps to the freshly built image even though the :latest tag is unchanged (otherwise Compose may keep the old image).

2.9 Backups

The whole server state is one SQLite file in ./data:

cp ./data/encryptilock_sync.db* ~/backups/   # cron it; contains only ciphertext

Losing it only loses the server copy — every device still holds its own vault.

2.10 Troubleshooting

Symptom Cause / fix
address already in use on up Another service holds the port → map a different host port (2.4).
Container Restarting (255) docker compose logs — a startup crash. Usually a missing/bad tls/cert.pem, or the /data mount isn't writable.
Edited the Dockerfile but nothing changed docker compose build --no-cache && docker compose up -d --force-recreate.
App says "certificate changed" You regenerated the cert → re-pin the new fingerprint in the app.
App can't reach it Firewall (2.5); confirm curl -k https://IP:PORT/healthz from the device's network.

3. Behind a reverse proxy (real domain, no pinning)

If you already run Caddy / Traefik / Nginx Proxy Manager, serve plain HTTP internally and let the proxy terminate TLS with a real CA cert — then the app needs no fingerprint pinning, just the https://sync.example.com URL.

In docker-compose.yml: remove the TLS_* env + the ./tls mount, put the container on your proxy network. Caddy example:

sync.example.com {
    reverse_proxy encryptilock-sync:8443
}

The proxy forwards the client IP via X-Forwarded-For, which the rate limiter honors.


4. Configuration (environment variables)

Var Default Meaning
PORT 8443 listen port
DB_PATH /data/encryptilock_sync.db (image) SQLite file path
TLS_CERT_PATH / TLS_KEY_PATH serve HTTPS directly; omit both to terminate TLS at a reverse proxy
RATE_LIMIT_THRESHOLD 5 failed auths before lockout
RATE_LIMIT_BASE_SECONDS 1 base backoff
RATE_LIMIT_MAX_SECONDS 900 max backoff

5. API (JSON over HTTPS; blobs are base64url strings in the JSON body)

Method & path Auth Notes
GET /healthz liveness
GET /v1/account/{lookup} – (pre-auth) { accountSalt, keyEpoch } or 404
POST /v1/account/{lookup} register { accountSalt, authSecret, keyEpoch }{ accountSalt, verifierServerSalt, keyEpoch }; 409 if exists
GET /v1/vault/{lookup} Bearer { blob, version, keyEpoch }; empty vault → version:"", blob:null
PUT /v1/vault/{lookup} Bearer + If-Match body { blob, keyEpoch }{ version }; 412 on version mismatch; If-Match: "" = create-only
POST /v1/account/{lookup}/rotate Bearer + If-Match master-password change: { accountSalt, authSecret, keyEpoch, blob }, atomic swap; 412 on version mismatch
GET /v1/account/{lookup}/devices Bearer { devices: [{ deviceId, label, lastSeen }] } (best-effort registry)
DELETE /v1/account/{lookup}/devices/{deviceId} Bearer remove a device from the registry

Auth: send the base64url authSecret as Authorization: Bearer <authSecret> (or X-Auth-Secret: <authSecret>). The server recomputes sha256(authSecret ‖ verifierServerSalt) and constant-time compares it to the stored verifier. Failed auth is rate-limited per account + per IP with exponential backoff (429 + Retry-After).

If-Match carries the version as an ETag, e.g. If-Match: "3". If-Match: "" means "create only". rotate is also conditional — on a concurrent push the client pulls + merges, then retries.

Device registry (best-effort): a client may send X-Device-Id (+ optional X-Device-Label) on register/auth; the server records it and refreshes last_seen. Removing a device is best-effort — its authSecret keeps working until the next master-password change (which re-keys the account). True cutoff = rotate.


6. Zero-knowledge guarantees

  • The password, vault key, and plaintext never reach the server.
  • authSecret is HKDF-derived from the vault key and cannot decrypt anything; the server stores only sha256(authSecret ‖ verifierServerSalt).
  • Blobs are never decrypted, parsed, inspected, or logged. Logs contain only sizes, versions, timestamps, account lookups, and outcomes — never blob bytes or secrets.

7. Develop & test

dart pub get
dart test                      # unit + integration (in-memory + SQLite)
dart run bin/server.dart       # PORT/DB_PATH/TLS_* from the environment

# End-to-end smoke test against a running instance:
BASE_URL=https://localhost:8443 INSECURE=1 ./scripts/smoke.sh

About

A simple api to serve your encrypted db to your devices across your network

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors