A secure file sharing platform built with the MERN stack and Supabase Storage — with an optional client-side, zero-knowledge encryption layer on top of its token-based sharing model.
Cryptex allows users to upload files, organize them into folders, generate unique share tokens, and control access through public/private visibility settings. File contents are stored in Supabase Storage while metadata is managed through MongoDB. Any upload can also be encrypted entirely in the browser before it's sent — in that mode, the server only ever stores ciphertext and never sees the decryption key.
- AES-256-GCM encryption performed entirely client-side via the Web Crypto API
- Content, filename, and MIME type are all encrypted — not just the bytes
- The decryption key is generated in the browser and only ever travels inside a URL fragment (
#key=...), which browsers never send in HTTP requests — the server, its logs, and Supabase never see it - Encrypted files are always private, can't be renamed (the name is ciphertext), and can't be made public
- Trade-off, by design: if a share link is lost, the file is permanently unrecoverable — there is no server-side reset, because there's nothing on the server that could reconstruct the key
- A folder can be created as an encrypted vault — every file added to it is encrypted under one shared key, generated once in the browser at vault creation
- One link (
?token=...#key=...) unlocks the entire vault, rather than needing a separate key per file - Each file still gets its own random IV even under the shared key — required for AES-GCM's safety guarantee regardless of key reuse
- Vaults are always private and can never be made public, enforced server-side
- Uploading into a vault and uploading a standalone encrypted file use the exact same client-side encryption code — only which key gets used differs
- Known limitation: an existing plaintext (or differently-encrypted) file can't be added into a vault after the fact — vault membership is decided at upload time, not by moving files in later
- Upload files securely (with real magic-byte content verification for unencrypted uploads)
- Download files
- Preview supported files
- Rename files
- Delete files
- Create folders
- Organize files into folders
- Rename folders
- Delete folders
- Download an entire folder as a ZIP
- Unique share token generated for every file and folder
- Access shared content without exposing database IDs
- Easy and secure sharing mechanism
- Expiring tokens, with one-click regeneration to rotate a leaked token
- Public files/folders
- Private files/folders
- Toggle visibility anytime (encrypted files are private-only — see above)
- MongoDB stores metadata
- Supabase Storage stores actual files (ciphertext, for encrypted uploads)
- Express API handles uploads and access control
git clone https://github.com/Zephyrex21/Cryptex_File_Sharing.git
cd cryptexnpm installCreate a .env file and add the required values.
npm run devnpm startCreate a .env file in the root directory.
Example:
# MongoDB Atlas URI
MONGO_URI=
# Supabase Project URL
SUPABASE_URL=
# Supabase Service Role Key
SUPABASE_SERVICE_KEY=
# Supabase Storage Bucket Name
SUPABASE_BUCKET=
# Backend Port
PORT=3000
# Owner key — required to rename, delete, or re-share any file/folder.
# Picked up silently: visit /app?ownerkey=<this value> once and the server
# sets a cookie, no prompts or UI involved from then on. Leave unset and the
# server generates one at startup and prints it (and the ready-to-open link)
# to the console; set it explicitly to keep the same key/link across
# restarts. See "Security Considerations" below.
OWNER_KEY=| Method | Endpoint | Description |
|---|---|---|
| POST | /api/files/upload |
Upload file |
| GET | /api/files |
Get all files |
| GET | /api/files/:id |
Get file details |
| GET | /api/files/:id/download |
Download file |
| GET | /api/files/:id/preview |
Preview file |
| PATCH | /api/files/:id |
Rename file 🔒 |
| PATCH | /api/files/:id/visibility |
Change visibility 🔒 |
| DELETE | /api/files/:id |
Delete file 🔒 |
| GET | /api/files/token/:token |
Access via token |
🔒 = requires the owner key — a cookie set silently via
/app?ownerkey=...once (or anX-Owner-Keyheader for scripts/curl), see "Environment Variables". A private/encrypted file's:idroutes above also require its own valid?token=(or the owner key) if it's not public — see "Security Considerations".
POST /api/files/uploadaccepts an optional encrypted-upload contract: multipart fieldsencrypted=true,iv,encryptedName,encryptedNameIV,encryptedMimeType,encryptedMimeTypeIValongsidefile(sent as ciphertext,application/octet-stream). Seecontrollers/fileUpload.jsandpublic/js/app.jsfor the client/server contract.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/folders |
Create folder |
| GET | /api/folders |
Get folders |
| GET | /api/folders/:id |
Folder details |
| PATCH | /api/folders/:id |
Rename folder 🔒 |
| PATCH | /api/folders/:id/visibility |
Change visibility 🔒 |
| DELETE | /api/folders/:id |
Delete folder 🔒 |
| POST | /api/folders/:id/files |
Add file to folder 🔒 |
| DELETE | /api/folders/:id/files/:fileId |
Remove file from folder 🔒 |
| GET | /api/folders/token/:token |
Access via token |
Standard upload:
- User uploads a file.
- File is stored in Supabase Storage.
- Metadata is stored in MongoDB.
- A unique token is generated.
- Users can share the token to provide access.
- Visibility settings determine whether content appears publicly.
Encrypted upload:
- User toggles "End-to-end encrypt" before uploading.
- The browser generates a random AES-256-GCM key and encrypts the file's content, filename, and MIME type — nothing plaintext leaves the device.
- Only ciphertext is uploaded and stored in Supabase; MongoDB stores ciphertext blobs for the name/type too, not the real values.
- The file is forced private and given a token, same as above.
- The share link is built as
.../app?token=<token>#key=<key>— the#key=fragment is never transmitted to the server by the browser, so this is the only place the key exists outside the uploader's own session. - Whoever opens that link decrypts the file locally in their browser; the server is never asked to, and couldn't if it were.
- Sensitive credentials are stored in environment variables.
- Rename, delete, visibility changes, and folder membership all require the owner key (
OWNER_KEY). It's picked up with zero UI: open/app?ownerkey=<key>once from your browser and the server sets a long-lived, HttpOnly cookie (unreadable by page JS, sent automatically after that) — no button, no prompt, nothing to re-enter. Scripts/curl/Postman can use anX-Owner-Keyheader instead. Reading a public item, or a private item via its own valid token, never needs either. - Supabase Service Role Key remains server-side.
- Private, unencrypted files/folders can only be read through their own current, unexpired share token (or the owner key) — this is enforced on every access path, including direct-by-id requests, not just the token-lookup endpoint. A file/folder's
_idis not itself a secret (it's returned alongside the token in several responses), so it's the token check that matters, and it's now checked everywhere the id is. - Encrypted files are the one exception to the point above: their content is ciphertext either way, so the server has nothing meaningful to gate — the client-side AES key is the real (and only) protection, exactly as the zero-knowledge design intends. An encrypted vault folder's own name/metadata is still plain text, though, and still requires its token like any other private folder.
- Regenerating a share token immediately invalidates the old one everywhere it's checked, including direct-by-id access — not just the token-lookup route.
- Token-lookup routes (
GET /files/token/:token,GET /folders/token/:token, andGET /folders/:id— the other way a private folder's token gets checked) are all rate-limited against brute-forcing. - QR codes for share links are generated entirely client-side (see
public/js/vendor/qrcode.js). This matters specifically for encrypted items, whose link includes the decryption key as a#key=...fragment — that fragment is never sent to any server, by anyone, for any reason, including rendering its own QR code. - Uploaded file content is verified against its declared type using magic-byte signatures, not just the client-reported MIME type (see
verifyMagicBytesincontrollers/fileUpload.js). - Unencrypted uploads get a best-effort malware pre-screen via VirusTotal's hash-lookup API — see
utils/malwareScan.jsfor exactly what this does and doesn't catch (it's a known-hash lookup, not a full scan, and fails open if VirusTotal is unreachable or no API key is configured). - Encrypted uploads are zero-knowledge by construction, not just policy: the AES key never appears in any request body, response, log line, or database row this server controls. This also means magic-byte content verification is skipped for encrypted uploads — the server has no plaintext to check, which is the expected cost of not being able to read the file at all.
- Known limitation: encryption happens at upload time only. There's currently no way to encrypt a file that's already been uploaded in plain — that would require downloading, encrypting, and re-uploading it, which isn't implemented yet.
- Known limitation: encrypted files are excluded from folder ZIP downloads (bulk decryption would need a folder-level key exchange, which is a separate future feature, not a silent omission).
- Known limitation:
controllers/linkPreview.js's SSRF guard re-validates the target IP on every redirect hop, but there's a narrow window between that check and the actual fetch where a DNS answer could theoretically change (DNS rebinding). Closing this fully needs pinning the validated IP for the actual connection instead of letting the HTTP client re-resolve it, which isn't implemented yet. - Upload, folder/link creation, and the public gallery/listing endpoints are intentionally left open (no owner key) — this app is designed as a personal drop-box where anyone with the link can contribute, not a walled garden. Lock those down too (wrap them in
requireOwnerfrommiddleware/auth.js) if that's not the model you want.
Built by Saurabh Raj Shekhar using MERN Stack and Supabase Storage.
This project is licensed under the ISC License.