Please don't open a public issue for security problems. Email ishaanmanoor1@gmail.com with what's affected, how to reproduce it, and what someone could do with it.
End-to-end encryption (always on for the website and desktop app; on by default for AI assistants)
- Your browser encrypts the file, its name and type, any note and any drawings with AES-256-GCM before uploading. The key is only in the link, after
#k=, which browsers never send to a server. ShareSecure stores files it can't read. - Files are padded to standard sizes (1 KB, 2 KB, 4 KB … 8 MB, 10 MB), so the server only learns roughly how big a file is.
- A link can also need a passcode. Then the link holds only half the key, and the passcode (stretched with 600,000 rounds of PBKDF2) is the other half. Anyone who only has the link can't open the file.
- Files sent to a username have their key sealed to that person's public key (ECDH P-256 + HKDF + AES-GCM). Only their browser can open it.
- Files shared through the hosted assistant connection (
/mcp, ChatGPT) are sealed by the server, which then forgets the key. The server does see the file while it seals it, the link (with its key) goes back to the assistant, so it's in the chat, and the share is tied to your account, like a signed-in upload. - Files shared through the local MCP server (
packages/sharesecure-mcp) are sealed on your own computer with the same code the browser runs, and uploaded and sent with anonymous tokens, exactly like the website does: the server can't tell they came from you. The link goes to your clipboard and your sealed list of shares instead of to the assistant, so the key is never in the chat either. Its connection token is only used for what's tied to your account anyway (picking up tokens, your own sealed boxes, your inbox and file requests), and anything it posts about a share goes a few minutes later, so the timing doesn't point back at it. - Code:
public/sealed.js.
Sign-in never sends your password (based on OPAQUE, RFC 9807; public/opaque.js lists where it differs)
- Your browser blinds the password before anything is sent. The server keeps a record it can check sign-ins against, but it never sees the password, can't learn it from the record, and can't test guesses against a stolen database without its own secret.
- Signing in also produces an export key that only your browser has. It locks your account's private key, which the server stores without being able to use.
- Older accounts send their password one last time when they next sign in, switch over, and the password hash is deleted.
- Deleting your account takes a fresh proof of the password, made the same way.
- New passwords need at least 10 characters and can't be one of the most common ones.
Uploads and sends that can't be tied to you (Privacy Pass-style tokens, public/blindrsa.js)
- While you're signed in, your browser picks up a few tokens a day. The server signs them blinded (RFC 9474 blind RSA), so it never sees the token it signed.
- Uploads and sends spend a token instead of your sign-in. The server can check the token is real and unspent, but it can't tell which account it gave it to. The daily limits (5 uploads, 60 sends) still hold.
- They never fall back to the sign-in. If the wallet is empty, a token is fetched first; if none can be had, the share doesn't happen. The server also ignores a sign-in sent alongside a token, so a careless client can't tag an upload.
- Tokens are picked up a while before they're used (a whole day's uploads at once), so timing doesn't link them either. Nothing signed in is sent right after an upload, and deleting a share uses only its delete key.
- The desktop app works the same way for the copies it puts on the website to send to usernames, and a send an assistant leaves waiting for your approval is posted a few minutes after its anonymous upload.
- Your list of shares is kept on the server sealed to your own key (padded to 4 KB steps), so "Your shares" still works on every device.
Nobody can swap someone's key unnoticed
- Every account has a security code (account menu → Security code): 30 digits from its public key. Compare codes with the people you send to.
- Your browser remembers each person's key the first time you send to them. If the server ever hands out a different one, sending stops and you're told to compare codes.
- Accounts can't replace their key once it's set.
- A public key log (
public/kt.js) records every account's key: an append-only Merkle tree, the same scheme Certificate Transparency uses (RFC 9162). Every key your browser, the desktop app or the local MCP server is given for someone comes with a proof that it's in the log, and is refused without one. Each also remembers the log it last saw and checks the new one only grew from it, so history can't be rewritten under them. Your browser checks the log shows your key for your username each time you open the site, and warns you if it doesn't. - So to read files meant for someone, a server would have to publish a second key for them, where everyone can see it. Anyone can check the whole log with
node scripts/kt-monitor.mjs: it rebuilds the tree, and flags any account that got a different key without being deleted first. A public GitHub Action runs it every day and keeps each day's log on thekey-log-witnessbranch, so the record can't be changed afterwards either. - The log names accounts by a hash of the username, not the name. Like looking up a key, it lets someone check whether a username they guess exists.
Asking for a file
- A file request's link is
/q/<id>#r=<request key>&pk=<your public key>. What you asked for is sealed with the request key, so the server never reads it, and the uploader's browser seals each file's key to the public key in the link, not one the server hands out, so a compromised server can't swap in its own. - What arrives waits in your inbox like any file sent to you, sealed end to end: the server never sees the file, its name or the uploader's note. Uploaders don't need an account and aren't identified.
- A request takes at most 20 files and lasts at most 30 days, and the inbox cap (20 waiting) applies, so a leaked request link can't fill your account.
Links that work once
- The first view takes the link's only view in one database step, so two people opening it at the same moment can't both get it. The file is then erased: every link to it, including copies sent to usernames, since they share its bytes.
- A tombstone stays for 30 days: the link's id, a hash of its delete key, when it was opened and how many times it was tried since. No name, no bytes, no account. Your browser asks about its own links with their delete keys and tells you if someone tried one again, which usually means it was passed on. A reload in the browser that opened it isn't counted.
- The viewer asks before opening, so link previews and scanners that fetch the page don't use up the view. A link that works once can't be downloaded, drawn on or reshared.
Assistants can be tricked; your files can't be sent by one that was
- An assistant reads web pages, emails and files, and any of them can hide an instruction like "send this to @someone". So when an assistant sends a file to someone not on your list, the send waits until you approve it on the website (account menu → Connect an AI assistant). You can instead let assistants send to anyone, or to no one.
- The server only knows which of those three you chose. Your list of people is sealed to your own key, so only your browser and the assistant server on your computer can open and apply it; assistants connected through ShareSecure's servers can't, so they ask every time. A waiting send (which share, to whom, its key and note) is sealed to your key too. Approving one opens it in your browser, which sends it anonymously like any send you make. So the server sees your account store a box it can't read, and later an anonymous send it can't tie to you.
- Only a signed-in session can approve or clear a waiting send, or change the rules, never an assistant's token. Waiting sends last a week at most, and replacing or turning off the token drops them.
- The local MCP server never shares from folders that hold keys or credentials (
~/.ssh,~/.aws,~/.gnupgand similar, and its own key folder), remembers each recipient's key the first time like the browser does, and labels everything in a received file as information from someone else, not instructions.
Code the server can't change
- Every script, pdf.js and mammoth included, is served from this site, and the Content-Security-Policy allows no others. mammoth is also pinned with an integrity hash.
- The desktop app, signed in to an account, runs the website's code from its own copy instead of downloading it, so a compromised server can't change the code that encrypts your files.
- Anyone can check the website serves exactly this repository's code.
npm run verify-sitefetches every file the browser runs from the live site and compares it byte for byte with your checkout, printing each file's SHA-256. A public GitHub Action runs the same check after every deploy and every day, so a changed page would show up there.
The desktop app's server only answers this computer
- It listens on loopback only, so nobody on your network can reach it; links come in through the relay. It also refuses any request whose Host isn't this computer (or the relay), so a web page can't point a domain at 127.0.0.1 (DNS rebinding) to read this computer's relay id and tie you to your links.
- The app's windows can't use the camera, microphone, location or screen capture (Electron allows them unless told not to), and only ever show the app's own pages.
- What a page can ask the app itself is limited by page: only the sign-in page can read or save your saved sign-in, and only the home page can convert files. So even a file that somehow ran code in the viewer couldn't reach your password.
- An assistant can't share your keys or ShareSecure's own data by path. An assistant on this computer can share a file by its path, and a web page or email it read could tell it which. So the desktop app and self-hosted server refuse anything in ShareSecure's data folder (its database, the key that encrypts every file, the linked account's sign-in), the app's own folder,
.env, and key folders (~/.ssh,~/.aws,~/.gnupgand similar), judged by where the file really is, so a link or junction pointing into one doesn't get past. The local MCP server does the same. - One leaked link can't fill your disk. Anyone holding a link can branch it (each viewer gets their own), and through the relay nothing says who's asking, so a link stops branching after 1,000 live copies.
- FFmpeg is checked before it runs. The converter is downloaded only after you say yes, from one fixed release, and refused unless its SHA-256 matches the one in the app's code.
- An assistant connector URL (
/connect/<token>) carries the token in its path, so it passes through Cloudflare and the relay on the way to your computer. The relay keeps no logs, but treat the URL like a password, and replace the token if it leaks.
Everything else
- Links on the website expire after 1 minute to 10 days; on your own computer they can last until you delete them. Expired files are erased.
- Files that aren't end to end (from older versions, or shared by an assistant through ShareSecure's servers) are still encrypted on the server, each with its own key.
- The self-hosted server checks file types by their contents. Files an assistant shares through it have author and editing metadata stripped from PDF and Office files before they're locked; files you share from the page are locked in your browser first, so the server can't strip (or read) them.
- Pages send
noindex,no-storeandframe-ancestors 'none'; only the public guides and comparisons (/guides,/compare) can be indexed, and they hold nothing private. There are no analytics. - Sessions are signed and expire after 30 days. Sign-in attempts are limited per username and per IP address, and IP addresses are only stored as a keyed hash.
A browser can't stop screenshots, screen recorders or someone pointing a phone at the screen, so ShareSecure never claims files are screenshot-proof. A sender can turn on two things per share (both off by default):
- Watermark. The viewer draws a faint repeating mark over the file with who opened it (their username if signed in, otherwise "Guest"), when, and the link's short id. Each person who opens a link gets their own branch of it, so the id points at one opening. It's drawn in the browser after decrypting and laid over the page, never baked into the file, and it's put back if someone deletes or hides it in the page. The server never learns who viewed what: nothing about the viewer is sent anywhere (
public/protect.jsmakes no requests, which the tests check). - Discourage screenshots and printing (view-only shares only). Printing shows a blank page and Ctrl/Cmd+P is blocked, PrintScreen on Windows clears the clipboard and blurs the file for a moment, and right-click, selecting and dragging are off over the file. In the desktop app the window showing the file is left out of screenshots and screen recordings (
setContentProtection), on Windows and macOS; Linux has no way to do this. The desktop app also has a setting (off by default) to do that for every view-only file.
Both settings travel in the share's sealed name for end-to-end shares, so the server can't see or change them; for shares encrypted on the server they're stored encrypted like the name. The existing blackout when the tab is hidden or loses focus stays on for everyone.
These deter casual copying. The watermark is what makes a leak traceable.
npm test includes attacks, each run against the real handlers with a throwaway database:
- A stolen session can't take over an older account (switching to the new sign-in needs the password), can't plant keys to receive someone's files (setting keys needs a sign-in from the last 10 minutes), and can't delete the account (that needs a fresh password proof).
- Replayed or tampered sign-in proofs are refused, and the password never appears in a request or the database.
- Forged, reused or wrong-kind tokens are refused, and the daily limits hold across tokens and signed-in uploads.
- A malicious server can't tag people with their own token key: the apps only accept the key pinned in their code, checked against the key itself.
- Without the right key or passcode, a stored file opens to nothing, and a box moved into another slot or changed in transit fails to open.
- Crafted names can't become HTML or script files, and crafted paths can't make the desktop app serve anything outside its own copy of the site.
- A link that works once gives the file out once even to simultaneous requests, and leaves no copy of the bytes behind.
- A file request refuses anything not sealed in the browser, posts from other sites, and uploads past its limit or expiry, and only the owner can list or close it.
- A swapped key is caught by the key log: a key that isn't in the log is refused, a second key for someone shows up in the log, and a log rewritten after someone looked fails their check.
- A tricked assistant can't send a file to someone off your list without your approval, can't approve its own sends or change its rules, and through the local MCP server never sees a link's key, can't share from key folders, and can't seal a file to a key the server swapped in. On the desktop app and a self-hosted server it can't share the data folder,
.envor key folders by path either, even through a link or junction. - A leaked link can't be branched more than 1,000 times on your own computer.
- A tampered FFmpeg download is refused, and nothing is downloaded until you agree.
- Nothing links you to what you share through the local MCP server: its uploads and sends carry anonymous tokens and no account, the files and copies it makes have no account or sender tag, and the server keeps only boxes it can't open (your rules, your waiting sends, your list of shares).
- Anyone with the whole link (and the passcode, if there is one) can open the file until it expires. For anything that matters, add a passcode, send it to a username, or turn on "Only people signed in".
- The local MCP server keeps your private key on disk once you run
link(in~/.sharesecure, readable by you only on macOS and Linux; on Windows it relies on your user folder's permissions). Runnpx sharesecure-mcp unlinkto remove it. - The site check proves what the server sends to whoever checks, not that it sends everyone the same page: a server targeting one person could still give them different code. The desktop app avoids that (it runs its own copy), and a browser-side check (like WEBCAT) is the next step.
- The website itself is still served by the server. In a browser, you trust the page you load. The desktop app doesn't have this problem.
- The server knows who a file was sent to, and when. It doesn't know who sent it.
- Desktop links share an address. Every link from one computer goes through
/l/<computer id>/, so anyone holding two of your desktop links can tell they came from the same computer, and the relay sees that computer's IP address while it's connected. Website links don't have this. - Timing and IP addresses. With little traffic, a token picked up shortly before an upload (a new browser, or after using up the wallet) could hint at who uploaded. Your IP address does the same for anyone who sees it.
- Your IP address is visible to the hosting provider and your network. Use Tor or a VPN if that matters.
- Someone who can already run programs as you on your computer can get at what the desktop app keeps: its server is started with Electron's run-as-Node mode (so it can outlive the app), which also lets any local program run code as ShareSecure, and the saved sign-in is only as safe as your operating system's key store. Like any desktop app, it can't protect you from malware already on the computer.
- The installers aren't code-signed yet (see SIGNING.md), so a download is only as trustworthy as GitHub and its HTTPS. Updates come from the same releases, checked against the SHA-512 listed in the release's
latest.yml, which guards against a broken download but not against someone who controls the release.
The experimental Unigroth zero-knowledge proofs are gone. The verifier spot-checked 32 of about 549 constraints and trusted a value the prover sent, so proofs could be forged without the secret, and each challenge was tied to a signed-in account. Privacy Pass-style tokens now do what it was meant to do, using a standard scheme (RFC 9474 blind RSA) that's checked against the RFC's own test vectors in tests/. The hash-to-curve step of sign-in is checked against RFC 9380's vectors the same way. These are this project's own implementations, not an audited library, so an outside review is still worth getting.
TOKEN_ISSUER_KEY (an RSA-2048 private key; make one with npm run token-key) turns on anonymous tokens. Without it, uploads and sends use the signed-in session instead. OPAQUE's keys come from ENCRYPTION_KEY, so they need nothing extra.
The same information for users is at /security and /privacy.