328 lines
16 KiB
Markdown
328 lines
16 KiB
Markdown
# 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.
|