End-to-end encrypted, peer-to-peer folder sharing in the browser.
folder.zone · Security Model · Cryptographic Design · Self-Hosting
folder.zone enables real-time folder sharing directly from your browser. Select a folder, receive a link, share it. Recipients browse and download files immediately: no upload step, no cloud storage, no accounts.
All data is encrypted client-side before transmission. The server facilitates peer discovery and connection signaling but never receives encryption keys. In relay fallback mode, the server forwards only ciphertext it cannot decrypt.
The server is assumed fully compromised. An attacker with complete server access can:
- Read all server-side state (room IDs, connection metadata)
- Store and analyze all relay traffic
- Observe WebRTC signaling messages (SDP offers, ICE candidates)
- Perform traffic analysis on message timing and sizes
| Asset | Mechanism |
|---|---|
| File contents | AES-256-GCM authenticated encryption |
| File integrity | HMAC-SHA256 with per-transfer key derivation |
| File paths | Encrypted within authenticated payload |
| Observable | Notes |
|---|---|
| Room identifiers | Required for peer discovery |
| Connection timing | When peers join/leave |
| Transfer volumes | Approximate, via ciphertext sizes |
| Peer count | Number of participants per room |
Encryption keys are transmitted exclusively via URL fragment. Per RFC 3986 §3.5, the fragment component is never sent to the server:
https://folder.zone/#<room_id>:<key_base64url>
└────────────────────────┘
Client-side only
Link security is equivalent to key security. Treat shared links as secrets.
| Operation | Algorithm | Source |
|---|---|---|
| Authenticated encryption | AES-256-GCM | Web Crypto API |
| Key derivation | HKDF-SHA-256 | Web Crypto API |
| Message authentication | HMAC-SHA-256 | Web Crypto API |
| Random generation | CSPRNG | crypto.getRandomValues |
Each message is encrypted with a fresh 96-bit initialization vector:
async function encrypt(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12))
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
)
return iv || ciphertext // 12-byte IV ‖ ciphertext ‖ 16-byte tag
}GCM mode provides authenticated encryption—tampering is detected via the 128-bit authentication tag.
File transfers include end-to-end integrity verification. A unique HMAC key is derived per transfer via HKDF to prevent replay:
async function deriveHMACKey(sessionKey, transferNonce) {
const ikm = concat(exportKey(sessionKey), transferNonce)
const keyMaterial = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveKey"])
return crypto.subtle.deriveKey(
{ name: "HKDF", hash: "SHA-256", salt: transferNonce, info: encode("file-hmac") },
keyMaterial,
{ name: "HMAC", hash: "SHA-256", length: 256 },
false,
["sign", "verify"]
)
}Verification flow:
- Sender computes
tag = HMAC(derivedKey, fileContents) - Sender transmits
(nonce, tag)with completion message - Receiver re-derives HMAC key from
(sessionKey, nonce) - Receiver verifies
tagbefore accepting file
Binary messages use a compact header for efficient WebRTC transport:
┌──────────┬───────────┬───────────┬───────────┬──────────┬──────────────┐
│ Type (1) │ Index (4) │ Total (4) │ PathLen(2)│ Path (n) │ Chunk (≤64K) │
└──────────┴───────────┴───────────┴───────────┴──────────┴──────────────┘
└─────────────────────────────────────────────────────────────────┘
Encrypted before transmission
64KB chunks stay within WebRTC DataChannel limits (~256KB) while enabling streaming without full file buffering.
┌─────────────────────────────────────────────────────────────────────────┐
│ HOST BROWSER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
│ │ File System │──│ Encrypt │──│ Chunk │──│ Transport │ │
│ │ Access API │ │ AES-256-GCM │ │ 64KB │ │ WebRTC / WS │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────┬───────┘ │
└─────────────────────────────────────────────────────────────│──────────┘
│
┌─────────────────────────────────────────┴─────┐
│ SIGNALING SERVER │
│ │
│ • Room management and peer discovery │
│ • WebRTC signaling relay (SDP, ICE) │
│ • Encrypted blob relay (fallback mode) │
│ • Zero knowledge of plaintext or keys │
│ │
└─────────────────────────────────────────┬─────┘
│
┌─────────────────────────────────────────────────────────────│──────────┐
│ PEER BROWSER │
│ ┌───────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Transport │──│ Reassemble │──│ Decrypt │──│ Verify HMAC │ │
│ │ WebRTC / WS │ │ Chunks │ │ AES-256-GCM │ │ Download │ │
│ └───────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
WebRTC DataChannel (primary)
- Direct peer-to-peer via STUN hole-punching
- No server bandwidth consumption
- Lowest latency
WebSocket Relay (fallback)
- Activated after 10s WebRTC timeout
- Handles symmetric NAT, restrictive firewalls
- Identical encryption—server sees only ciphertext
- Bun v1.0+
git clone https://github.com/symbolicsoft/folder.zone
cd folder.zone
bun run server/server.jsIncludes Dockerfile and fly.toml for Fly.io:
fly launch
fly deployFor horizontal scaling, configure Upstash Redis for room affinity:
fly secrets set UPSTASH_REDIS_REST_URL=<url>
fly secrets set UPSTASH_REDIS_REST_TOKEN=<token>| Variable | Description | Default |
|---|---|---|
PORT |
HTTP/WebSocket listen port | 3000 |
UPSTASH_REDIS_REST_URL |
Redis endpoint for room tracking | — |
UPSTASH_REDIS_REST_TOKEN |
Redis authentication | — |
| Resource | Limit | Scope |
|---|---|---|
| File downloads | 60/min | Per peer |
| File uploads | 30/min | Per peer |
| Max file size | 2 GB | Per file |
| Max path depth | 10 | Directories |
| WebSocket messages | 300/min | Per connection |
| Relay bandwidth | 100 MB/min | Per connection |
Beta software. The implementation uses standard cryptographic primitives via Web Crypto API, but the protocol has not undergone independent security audit.
Not recommended for:
- High-risk threat models requiring audited security
- Adversaries capable of compromising your browser
- Scenarios where link interception is likely
For sensitive applications, consider OnionShare or similar audited tools.
AGPL-3.0-or-later
Public deployment of modified versions requires source disclosure. See LICENSE.