# 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: . A running instance is at . ``` 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.