Under the hood

How Buzzdrop works

What happens when you share a file or a note: what runs where, what crosses the wire, and what our server can and can’t see at each moment.

  • We can’t read your files or notes

    They are encrypted before they leave your device. The server only holds ciphertext and never sees the password.

  • One time really means one time

    The server holds half of the key and gives it out once, only to someone who proves the password. Then it destroys it.

  • We do keep some metadata

    A file’s name, timestamps and the IP address it was opened from. What we keep, and for how long.

In brief

The whole flow in six steps, from your side.

  1. Pick a file, or write a note. Either one gets the same treatment.

  2. Set a password. Your browser encrypts the drop before anything is uploaded.

  3. Add an expiry. Optional. When it passes, the drop is deleted, opened or not.

  4. Get one link. It names the drop. On its own, it opens nothing.

  5. Send the link. Send the password another way: a different app, a call.

  6. They prove the password, once. Then the drop downloads once and is deleted from the server. It decrypts on their device.

The flow

Web app or CLI, file or text: it is the same protocol and the same crypto. Pick a view and the steps change to match.

Step 1 Upload, phase 1

Server sees: That an upload is starting

Never sees: The file or note, and the password

Who acts
Your browser, then the server.
What crosses the wire
A request to POST /upload/begin, signed in by your login session, with a CSRF token. The server answers with a fresh file_id and a random 32-byte key share H.
What the server sees
That your account is starting an upload. Nothing about the file or note yet.
Why
The file key needs H, so H has to exist before anything is encrypted. The server keeps it: this is the half of the key it will hand out exactly once. An upload begun and never finished is swept once it is over an hour old, at the next upload or restart.

Step 2 Encrypt where you are

Server sees: Nothing

Never sees: The password, Kp and the content

Who acts
Your browser, in the page’s JavaScript, alone. Nothing crosses the wire.
What happens
You type a password, or have the page generate a 6-word passphrase. The password goes through PBKDF2 with 600,000 iterations; from the result come two keys, Kp for encrypting and a verifier V for proving the password later. The file key is HKDF(Kp ‖ H), so it needs both the password and the server’s share. A random receipt goes in front of the file, and AES-GCM encrypts the lot.
Where the code comes from
Our server sends it with the page. Subresource Integrity protects it in transit and in caches, not against a compromised server.
What the server sees
Nothing. The password and Kp never leave your device.
Why
Ciphertext and H without the password can’t be decrypted, and neither can ciphertext and the password without H. The receipt is how the recipient will later prove they decrypted it.

Step 3 Upload, phase 2

Server sees: Ciphertext, V, and the filename

Never sees: The password, Kp and the content

Who acts
Your browser, then the server.
What crosses the wire
POST /upload with the ciphertext as a multipart file, plus the file_id, V and SHA-256(receipt). It is signed in by your session and CSRF token, and only the account that ran phase 1 may finish it.
What the server sees
The ciphertext and its approximate size, V, the receipt’s hash, the expiry you chose, an optional memo to yourself, and the file’s original filename, in plain text. It stores the ciphertext and binds V to H.
Why
V lets the server check the password later without ever learning it, and it can’t be turned back into Kp.

Step 4 Share

Server sees: Nothing

Never sees: The password, and who you send the link to

Who acts
You.
What crosses the wire
Nothing to us. You send the link one way, and ideally the password another: a different app, a call.
What the server sees
Nothing. The link alone opens nothing: it names the drop, and the drop needs the password.
The one-click link
The success page also offers a link with the password after the #. Browsers never send that part to a server, so we still don’t see the password. But anyone who has that link can open the drop, and chat history, link previews and exports keep a copy. It’s convenient; sending link and password separately is stronger.

Step 5 The recipient proves the password

Server sees: V and an IP address

Never sees: The password

Who acts
The recipient’s browser, then the server. The recipient needs no account, only the link and the password.
What crosses the wire
The browser derives V from the typed password and posts it to POST /release. Never the password.
What the server sees
V, and the recipient’s IP address, which it logs with a successful or wrong attempt. It compares V with the stored one in a single database transaction.
On a match
The server hands over H, once, and wipes H and V from its database. It keeps only a fingerprint of a one-time download ticket, which the browser works out from H.
On a wrong password
One wrong password locks the drop and destroys H, so nobody can open it after that, the recipient included. Self-hosted instances can set their own limit.
Why
This is what makes one time real. Without H the ciphertext is dead bytes, and H comes out exactly once, after the password has been proven, in a transaction only one requester can win.

Step 6 Download once

Server sees: The ticket and an IP address

Never sees: The content, and who the recipient is

Who acts
The recipient’s browser, then the server.
What crosses the wire
GET /download with the ticket. The server streams the ciphertext back.
What the server sees
The ticket, and the recipient’s IP address, which it stores with the time of the download.
Then
The ticket is used up before the first byte goes out, and the ciphertext is deleted from storage once the stream ends, even if it broke off. A drop released but not downloaded within 10 minutes expires the same way.
Why
Only the browser that won the release can claim the ciphertext, and there is no second copy to come back for.

Step 7 Decrypt in the browser

Server sees: That decryption worked

Never sees: The content and the file key

Who acts
The recipient’s browser, then the server.
What happens
The browser builds the file key from Kp (from the password) and H, decrypts, and takes out the receipt. The file is saved as a download.
What crosses the wire
The receipt, to POST /report_decryption, so your list shows the drop as opened and, if you asked for it, you get an email.
What the server sees
The receipt, which only matches its stored hash if the drop was really decrypted. It learns that decryption worked, never the content.

Step 8 Expiry and manual delete

Server sees: H destroyed, ciphertext deleted

Never sees: The content, then or ever

Who acts
The server, or you.
What happens
When a drop’s expiry passes, or you delete it from your list, H is destroyed and the ciphertext is deleted from storage. Expiry is checked whenever the link is opened and on a timer (every five minutes by default). The database overwrites what it deletes, which reduces, but doesn’t rule out, traces of H left in its file.
Why
H never outlives the drop. Even if deleting the ciphertext from storage failed, without H it can’t be decrypted.

8 steps

What the server sees

Everything that reaches us, set against what never does.

  • Content

    Sees Ciphertext and its approximate size

    Never sees The plaintext

  • Password

    Sees V, a one-way fingerprint of it

    Never sees The password, Kp, the file key

  • Name

    Sees The original filename, in plain text. It is stored and shown in the sender’s drop list; the recipient never sees it on a page — the name they save the file under arrives inside the ciphertext.

    Never sees —

  • People

    Sees The sender’s account; the recipient’s IP address

    Never sees Who the recipient is

What we keep, and for how long

  • The ciphertext

    Until opened

    Until it is downloaded, it expires, or the sender deletes it. Then it is deleted from storage.

  • H and V

    Until released

    Until the release succeeds, the drop locks, it expires, or it is deleted, whichever comes first.

  • The drop’s record

    Until you delete it

    The filename (or “Secret Note”), the sender’s account, when it was created, its expiry, when it was opened and from which IP address, whether decryption worked, the receipt’s hash, and the sender’s optional memo and notification address; also the language it was sent in, which the notification is written in. Beside it, what is left of the key share once H and V are gone: the password’s salt, the count of wrong attempts and when it was released. All of it stays in the sender’s list until the sender deletes it; nothing purges it automatically.

  • Server logs

    Operator’s choice

    Every successful or wrong password attempt is logged with the requester’s IP address. How long logs are kept is up to whoever runs the server.

Limits

What Buzzdrop does not protect against. No design can fix these.

  • The recipient can keep a copy. Once a file or note is decrypted, it can be saved, copied or photographed. One time means the key is handed out once, not that the content can’t be kept.

  • Whoever runs the server controls the code. The web app’s encryption code comes from the server, and Subresource Integrity can’t help against the server itself: someone in control of it could serve code that leaks the password, or log V and H. The CLI’s code comes from GitHub Releases instead, but the recipient’s side always runs in a browser.

  • Weak passwords. Someone who steals the database while a drop is waiting to be opened can try passwords against its V offline; the stored files aren’t needed for that. 600,000 PBKDF2 iterations make every guess slow; a long passphrase makes the search hopeless, a short password doesn’t.

  • A one-click link is the key. Whoever has it can open the drop.

  • A wrong guess can destroy a drop. Anyone with the link can spend the attempt and lock it, and then the real recipient can’t open it either.

The full technical account

The keys, requests and server routes behind every step are written up in docs/how-it-works.md, in the Buzzdrop repository.

Read the technical account