Skip to content

ClaudeMaxUser/DropVault

Repository files navigation

DropVault

Serverless P2P file transfer in the browser — WebRTC + AES-256-GCM, no file data ever touches a server.

WebRTC AES-256-GCM React Vite


What it does

DropVault lets two people transfer files directly between their browsers — no accounts, no cloud storage, no upload limits. Files are encrypted in the browser before they leave, decrypted only at the other end, and no file data ever passes through a server.

The only server involved is Firebase Realtime Database, used purely as a temporary rendezvous to exchange the WebRTC handshake (SDP offer/answer and ICE candidates). Once the connection is established, Firebase is no longer in the loop and the signaling data is deleted.


Technical highlights

End-to-end encryption Each session generates a fresh ECDH (P-256) keypair. The two peers exchange public keys over the DataChannel, then derive a 256-bit AES-GCM session key using HKDF with a domain-separation string (DropVault-AES-Session). Every 64 KB chunk is encrypted with a random IV before it leaves the sender's browser. The server never sees plaintext.

Fingerprint verification Both peers are shown a short fingerprint of each other's public key (first 8 bytes of SHA-256, formatted as XX:XX:XX:XX:XX:XX:XX:XX). Comparing these out-of-band (voice, in-person) defeats a man-in-the-middle who substitutes their own key during the exchange. Incoming requests auto-reject after 30 seconds if left unattended.

Transfer resume Progress is checkpointed to localStorage using a key derived from filename + size + lastModified. If either peer reloads mid-transfer, the transfer picks up from the last confirmed chunk rather than restarting from zero.

Zero server storage Firebase only ever holds SDP and ICE data (a few hundred bytes per session, TTL 24h). No file content, no keys, no metadata.


Architecture

src/
├── App.jsx                      # Root shell — wires hooks to components (~80 lines)
├── firebase.js                  # Firebase init
│
├── lib/                         # Pure utilities, no React dependency
│   ├── constants.js             # RTC config, chunk size
│   ├── crypto.js                # ECDH, HKDF, AES-GCM, fingerprinting
│   ├── signaling.js             # Firebase read/write helpers
│   ├── resume.js                # localStorage checkpoint helpers
│   └── format.js                # Bytes, speed, IDs, error formatting
│
├── hooks/                       # React logic — no JSX
│   ├── usePeerConnection.js     # WebRTC setup, ICE, key exchange, modals
│   ├── useFileTransfer.js       # Chunking, encryption, send/receive
│   └── useLogs.js               # Activity log state
│
└── components/                  # Presentational UI
    ├── Header.jsx
    ├── SendTab.jsx
    ├── ReceiveTab.jsx
    ├── FileItem.jsx
    ├── IncomingItem.jsx
    ├── ConnectModal.jsx          # Fingerprint verification + countdown
    ├── DisconnectModal.jsx
    ├── LogView.jsx
    └── StatBar.jsx

The full connection handshake, key derivation, and chunk packet layout are documented in PROTOCOL.md.


Getting started

Requirements: Node 18+, a browser with WebRTC and SubtleCrypto support (all modern browsers).

npm install
npm run dev
# Production build
npm run build
npm run preview

Firebase setup: Create a Realtime Database, copy your config into src/firebase.js, and set read/write rules that allow unauthenticated access to rooms/ (or scope it as needed). The app writes only SDP/ICE data — there is nothing sensitive in Firebase.


How a transfer works

  1. Each peer gets a short random ID on load (XXXXXXXX).
  2. The sender enters the receiver's ID and clicks Connect. An SDP offer is written to rooms/{receiverId} in Firebase.
  3. The receiver's browser picks up the offer, writes an SDP answer, and ICE candidates are exchanged. Firebase is no longer involved once the DataChannel opens.
  4. Both peers generate ephemeral ECDH keypairs and exchange public keys over the DataChannel.
  5. Each peer derives the same AES-256-GCM session key via HKDF. Both are shown fingerprints to verify out-of-band.
  6. The sender slices files into 64 KB chunks, encrypts each with a fresh IV, and streams them over the DataChannel.
  7. The receiver decrypts and reassembles chunks into a Blob. A Save button triggers a local download — nothing is auto-written to disk.

Security properties

Property Mechanism
Confidentiality AES-256-GCM per chunk; server never sees plaintext
Integrity AES-GCM authentication tag per chunk
Forward secrecy Ephemeral ECDH keypairs, no persistent keys
MITM resistance Out-of-band fingerprint verification
Signaling privacy Only SDP/ICE (no content) passes through Firebase

Limitations: Endpoint compromise, skipped fingerprint verification, and Firebase admin access to SDP data are out of scope. See PROTOCOL.md for the full threat model.


License

MIT


Live demo

Live demo


Troubleshooting — WebRTC connection failures (TURN / relay)

If a connection fails with logs like this:

HH:MM:SS initializing...
HH:MM:SS your peer id: MR02A5DF
HH:MM:SS connecting to: SYY7B4QC
HH:MM:SS ICE gathering: gathering
HH:MM:SS ICE candidate: host
HH:MM:SS ICE candidate: srflx
HH:MM:SS offer sent, waiting for answer...
HH:MM:SS answer received from SYY7B4QC
HH:MM:SS ICE: checking connectivity...
HH:MM:SS WebRTC: connecting
HH:MM:SS WebRTC: failed
HH:MM:SS connection closed
HH:MM:SS data channel closed

then the remote peer is not receiving relay (TURN) candidates. Common causes:

  • TURN servers not configured or credentials invalid/expired.
  • TURN traffic blocked by the network or ISP (ports/protocols blocked).
  • TURN provider rejecting requests (rate limits, expired demo credentials).

Quick checks:

  • Open DevTools Console and look for [DropVault] ICE servers configured: and ICE candidate: logs — you should see relay.
  • Verify VITE_TURN_USERNAME, VITE_TURN_CREDENTIAL, and VITE_TURN_URLS are set in .env.local (see .env.example).
  • Force-test TURN from the browser (replace USER/CREDS):
const pc = new RTCPeerConnection({
    iceServers: [{ urls: "turn:global.relay.metered.ca:80", username:"VITE_TURN_USERNAME", credential:"VITE_TURN_CREDENTIAL" }],
    iceTransportPolicy: "relay"
});
pc.createDataChannel("test");
pc.createOffer().then(o => pc.setLocalDescription(o));
pc.onicecandidate = e => { if (e.candidate) console.log("candidate:", e.candidate.candidate); else console.log("gathering done"); };

If you don't get a relay candidate:

  • Try a different network (mobile hotspot). If it works there, your ISP/network is blocking TURN.
  • Regenerate TURN credentials (e.g., https://www.metered.ca/stun-turn) or run your own coturn.
  • Ensure TURN URLs include TLS (use turns: on 443) for restrictive networks.

Notes:

  • This project includes demo fallback TURN entries, but demo credentials may be rate-limited or expired. For reliable cross-network transfers, use your own TURN credentials or a hosted TURN provider.

About

Serverless P2P file transfer in the browser. WebRTC DataChannel + AES-256-GCM end-to-end encryption, ECDH key exchange with fingerprint verification, and Firebase for SDP signaling only. No file data ever touches a server.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors