DataTransfer/README.md
2026-10-15 20:09:11 +02:00

328 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DataTransfer
Send messages and files **end-to-end encrypted** between two browsers using a one-time code. No account, no
installation, and nothing is stored on the server: it only relays encrypted data while both browsers are connected.
DataTransfer is free and open source ([0BSD license](LICENSE)). Source code:
<https://git.sithis.ch/Sithis/DataTransfer>. A running instance is at <https://transfer.sithis.ch>.
```
Browser A (creator) Server (relay) Browser B (joiner)
─────────────────── ────────────── ──────────────────
Create connection ── code ──────────► pairs the two browsers ◄──────── Join with code
key exchange ◄────────────────── relays public keys ──────────► key exchange
encrypt ─ chunks ─────────────────────► relays, never stores ──────────► decrypt, save
```
## Features
- **End-to-end encryption in the browser.** The server never sees keys or plaintext.
- **One-time connection codes** (10 characters, 50 bits of randomness), shared out of band (phone, messenger, …).
- **Safety code.** Both users compare it over another channel; if both codes match, nobody intercepted the connection.
- **Messages and files**, files up to 100 MB each, transferred in 256 KB chunks with progress display.
- **Nothing stored.** The server keeps only in-memory connection state; received files stay in the recipient's
browser until they're saved.
- **Abuse protection:** per-network limits, rate limits, a self-hosted proof-of-work bot check, connection timeouts
and transfer quotas.
- **Security headers:** strict Content Security Policy, no third-party content, no tracking.
- **Recipient protection:** received file names are sanitized, and risky file types (programs, scripts, macros, …) show
a warning.
- **Legal pages** for Switzerland (imprint, privacy policy, terms of use, abuse contact), filled from configuration.
- **Open source:** the footer and the legal pages link to the source code repository (`Legal:SourceCodeUrl`).
- **Queue:** when the server is full, new connections wait in a queue and are created automatically.
## How it works
### Architecture
| Part | Technology | Responsibility |
|---|---|---|
| `DataTransfer` | ASP.NET Core 10, SignalR | Hosts the app, pairs browsers, relays frames, enforces limits, serves the legal pages |
| `DataTransfer.Client` | Blazor WebAssembly, WebCrypto | The whole user interface, key exchange, encryption, file handling |
All state on the server lives in memory (one instance, no database). A restart ends all connections.
### Encryption
1. The creator gets a code from the server and shares it. The joiner enters the code.
2. **Commit-then-reveal key exchange** with ephemeral ECDH P-256 keys:
1. the joiner sends a hash commitment to its public key;
2. the creator reveals its public key;
3. the joiner reveals its public key, and the creator checks it against the commitment.
A relay in the middle therefore has to fix its fake keys before it learns the real ones, and can't search for keys
that produce matching safety codes.
3. Both browsers derive one **AES-256-GCM key per direction** from the shared secret, salted with the code and both
public keys. Private keys are non-extractable and discarded after the agreement.
4. Every frame carries a **sequence number inside the ciphertext**, so replayed, reordered or dropped frames are rejected.
5. The **safety code** (4 × 5 digits) is a hash of both public keys.
As with every web application, the server delivers the code that runs in the browser. The end-to-end encryption
protects against a curious or compromised relay only as long as the server delivers honest code.
### Limits (defaults)
| Limit | Default |
|---|---|
| File size | 100 MB per file, up to 20 files per selection |
| Per connection and direction | 1,000 messages/files, 1 GB of files |
| Hub calls per browser | 10 per second (paces file transfers to at most about 2.5 MB/s) |
| Unjoined code expires after | 3 minutes |
| Connection closes after | 10 minutes without transfers, 2 hours at the latest |
| Data relayed per connection | 2 GiB |
| Simultaneous connections | 100, plus a queue of 50 |
| Per network address (IPv4 or IPv6 /56) | 3 connections or queued requests, 20 open browsers, 60 new connections per minute |
## Project structure
```
DataTransfer.slnx
LICENSE BSD Zero Clause License (0BSD)
Dockerfile Container image (multi-stage build)
DataTransfer/ Server
Program.cs Startup and middleware pipeline
Hubs/ SignalR hub and per-client rate limit
Services/ Connection registry, cleanup, options
Models/ Connection (pairing of two browsers)
Security/ Address limits, bot check, CSP/security headers, forwarded headers
Components/ Layout, legal pages, error pages
wwwroot/ CSS, bot-check workers, JS initializer (e-mail protection)
DataTransfer.Client/ Browser app (Blazor WebAssembly)
Pages/Link.razor The transfer page
Services/ Session state machine, encryption, frames, file handling
Shared/ Contracts shared with the server
```
## Requirements
- [.NET 10 SDK](https://dotnet.microsoft.com/download) for building and running locally.
- [Docker](https://docs.docker.com/get-docker/) for building and running the container.
- A current browser (Chrome, Edge, Firefox, Safari). The page must be opened via **HTTPS** or `localhost`; otherwise
browsers don't provide the encryption API.
## Build and run locally
```bash
# Build
dotnet build DataTransfer.slnx
# Run (Development) – http://localhost:5133
dotnet run --project DataTransfer --launch-profile http
# or with HTTPS – https://localhost:7260
dotnet run --project DataTransfer --launch-profile https
```
Open the URL in two browser windows, for example a normal and a private window: create a connection in one, join it
in the other.
### Publish without Docker
```bash
dotnet publish DataTransfer/DataTransfer.csproj -c Release -o ./publish
cd publish
ASPNETCORE_URLS=http://0.0.0.0:8080 dotnet DataTransfer.dll
```
On Windows PowerShell, set the variable with `$env:ASPNETCORE_URLS = "http://0.0.0.0:8080"` instead.
## Configuration
Settings live in `DataTransfer/appsettings.json`. Every setting can be overridden with an environment variable, using
`__` (double underscore) between section and key, and an index for list entries:
```bash
AllowedHosts=transfer.example.ch
Transfer__MaxConnections=50
ForwardedHeaders__KnownNetworks__0=172.16.0.0/12
Legal__Address__0="Example Street 1"
```
Keep personal details (the `Legal` section) out of shared code: use environment variables or an
`appsettings.Production.json` that you don't commit or publish.
| Setting | Default | Meaning |
|---|---|---|
| `AllowedHosts` | `localhost;127.0.0.1;[::1]` | Host names the server answers to. **Set it to your domain**, otherwise requests get HTTP 400. |
| `Transfer:InactivityTimeout` | `00:10:00` | A connection closes after this period without transfers. |
| `Transfer:JoinTimeout` | `00:03:00` | A new code expires unless someone joins within this period. |
| `Transfer:MaxConnectionLifetime` | `02:00:00` | Every connection closes after this period. |
| `Transfer:MaxConnections` / `MaxQueueLength` | `100` / `50` | Simultaneous connections, and requests waiting for a free slot. |
| `Transfer:MaxRelayedBytesPerConnection` | `2147483648` | Bytes a connection may relay (both directions) before it closes. |
| `Transfer:MaxConnectionsPerAddress` | `3` | Connections plus queued requests per network address. |
| `Transfer:MaxClientsPerAddress` | `20` | Open browsers (hub connections) per network address. |
| `Transfer:MaxConnectAttemptsPerMinute` | `60` | New hub connections per network address and minute. |
| `Transfer:MaxInvocationsPerSecond` | `10` | Hub calls per browser and second; further calls are delayed. |
| `BotCheck:Enabled` | `true` | Proof of work before a connection is created. |
| `BotCheck:MaxNumber` / `BusyMaxNumber` | `200000` / `1000000` | Difficulty normally, and while the server is at least `BusyThreshold` (80 %) full. |
| `ForwardedHeaders:KnownProxies` / `KnownNetworks` | empty | Reverse proxies whose `X-Forwarded-For` is trusted (IP addresses / CIDR networks). |
| `ForwardedHeaders:ForwardLimit` | `1` | Number of proxies in front of the app. |
| `Kestrel:Limits:MaxConcurrentConnections` / `MaxConcurrentUpgradedConnections` | `2000` / `1000` | Caps on open TCP connections and WebSockets. |
| `Legal:*` | placeholders | Operator details for the legal pages (see below). |
### Legal pages
`/imprint`, `/privacy`, `/terms` and `/abuse` are filled from the `Legal` section:
| Key | Content |
|---|---|
| `OperatorName` | Your full name, or the company name. |
| `Address` | Postal address, one entry per line. |
| `Email`, `AbuseEmail` | Contact and abuse addresses. |
| `Phone`, `CompanyId`, `ResponsiblePerson` | Optional; shown only if set. |
| `HostingProvider` | The hosting company, or **empty** if you run the server yourself. |
| `HostingLocation` | Country of the server. |
| `AccessLogRetentionDays` | How long your server or reverse proxy keeps access logs; `0` if none. |
| `PlaceOfJurisdiction` | Your municipality of residence, or the company's registered office. |
| `LastUpdated` | Date of the last change to the legal texts. |
| `SourceCodeUrl` | Public repository of the code you run (http or https). The footer ("Open source"), imprint, privacy policy, terms and abuse page link to it. Leave empty to hide these links. |
The `appsettings.json` in this repository contains only `[…]` placeholders. Put your real details into an
`appsettings.Production.json` next to the app (or mount it into the container) and never commit it; it is listed in
`.gitignore`. Outside Development, the server logs a warning at startup while any placeholder remains. The texts are
templates based on Swiss law (revFADP, CO, CPC) and not legal advice.
Protection against harvesting bots:
- The name, postal address, phone number, responsible person, place of jurisdiction and e-mail addresses are written
into the HTML only in encoded form (`Components/Layout/ProtectedData.cs`). The browser decodes them with JavaScript
(`wwwroot/DataTransfer.lib.module.js`), so bots that read the HTML find nothing usable.
- The legal pages are marked `noindex, noarchive`, so search engines don't list or cache them.
## Docker
The `Dockerfile` builds the app with the .NET 10 SDK and runs it in the minimal `aspnet:10.0-noble-chiseled` image
(no shell, non-root user). The container listens on **port 8080** and runs in the `Production` environment.
### Build the image
```bash
docker build -t datatransfer .
```
For an arm64 server, such as a Raspberry Pi, or for both architectures at once:
```bash
docker buildx build --platform linux/arm64 -t datatransfer .
docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.ch/datatransfer:latest --push .
```
### Start the container
```bash
docker run -d --name datatransfer --restart unless-stopped \
-p 8080:8080 \
-e AllowedHosts=transfer.example.ch \
-e ForwardedHeaders__KnownNetworks__0=172.16.0.0/12 \
datatransfer
```
With your legal details in a separate file instead of the image:
```bash
docker run -d --name datatransfer --restart unless-stopped \
-p 8080:8080 \
-e AllowedHosts=transfer.example.ch \
-e ForwardedHeaders__KnownNetworks__0=172.16.0.0/12 \
-v "$(pwd)/appsettings.Production.json:/app/appsettings.Production.json:ro" \
datatransfer
```
Useful commands:
```bash
docker logs -f datatransfer # follow the log
docker stop datatransfer # stop (ends all connections)
docker start datatransfer # start again
# Update: rebuild with the latest base images and replace the container
docker build --pull -t datatransfer .
docker rm -f datatransfer
docker run -d ... # same command as above
```
### With HTTPS (Docker Compose and Caddy)
Browsers only allow the encryption on HTTPS pages, so put a reverse proxy with a certificate in front of the
container. [Caddy](https://caddyserver.com) fetches Let's Encrypt certificates automatically, forwards WebSockets and
the original `Host` header, and sets `X-Forwarded-For`.
`docker-compose.yml`:
```yaml
services:
datatransfer:
build: .
image: datatransfer
restart: unless-stopped
environment:
AllowedHosts: transfer.example.ch
# Trust Caddy, which reaches the app from Docker's internal network.
ForwardedHeaders__KnownNetworks__0: 172.16.0.0/12
# volumes:
# - ./appsettings.Production.json:/app/appsettings.Production.json:ro
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
```
`Caddyfile`:
```
transfer.example.ch {
reverse_proxy datatransfer:8080
}
```
Start, follow the log and update:
```bash
docker compose up -d --build
docker compose logs -f datatransfer
docker compose build --pull && docker compose up -d
```
For the certificate, the domain must point to your server, and ports 80 and 443 must be reachable from the internet
(port forwarding on your router if you host at home). The app container itself doesn't publish a port; only Caddy is
reachable.
## Deployment checklist
- [ ] HTTPS via a reverse proxy; only ports 80/443 reachable from the internet.
- [ ] `AllowedHosts` set to your domain.
- [ ] The reverse proxy is trusted (`ForwardedHeaders:KnownProxies` or `KnownNetworks`) and forwards the original `Host`
header and WebSockets.
- [ ] The `Legal` section is filled in, and `AccessLogRetentionDays` matches your reverse proxy's access log settings.
- [ ] Rebuild the image regularly (`--pull`) to get security updates of .NET and the base image.
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| "End-to-end encryption is not available" | The page isn't opened via HTTPS (or `localhost`). |
| Every request returns HTTP 400 | The host name isn't in `AllowedHosts`. |
| Everybody gets "Too many connections from your network" | The reverse proxy isn't trusted, so all visitors share its address. The log shows *"Ignored X-Forwarded-For from … which is not a trusted proxy"*; add the proxy to `ForwardedHeaders:KnownProxies` or `KnownNetworks`. |
| Creating or joining hangs or fails behind a proxy | The proxy doesn't forward WebSockets, or it rewrites the `Host` header (the server rejects connections whose `Origin` doesn't match the host). |
| Log warning *"Failed to determine the https port for redirect"* | Harmless when HTTPS is handled by the reverse proxy. |
| Log warning about Data Protection keys not being persisted (container) | Harmless for this app. Mount a volume at `/home/app/.aspnet` to keep the keys across container updates. |
| Log warning *"…still contain placeholders…"* | Fill in the `Legal` section. |
| Header and footer show, but the transfer area stays empty | `_framework/blazor.web.js` returns 404: the image was built without Blazor's web assets. Keep `-p:RequiresAspNetWebAssets=true` on the `dotnet restore` line in the `Dockerfile` and rebuild. |
## License
DataTransfer is free and open source under the [BSD Zero Clause License (0BSD)](LICENSE): you may use, copy, modify
and distribute it for any purpose, with or without fee, and without any conditions (not even attribution). It comes
without any warranty.