Encryption
What happens to a file between your upload and your link, explained once in plain words and once with the actual parameters.
The short version
Encrypted on arrival
Every file is encrypted before it is stored anywhere. You do not have to do anything.
The key is in the link
Your URL carries it, after # on private links and in ?key= on embed and raw links.
We keep no copy we can open
Lose the link and the file is gone for good, unless a key pair of yours sealed a copy of the key. What we can see is right below.
What we can see
Encryption covers the contents of your files. It does not cover everything an upload leaves behind, and a service that says otherwise is hiding the rest. This is the full list of what our server can read about an upload.
| What | Readable by us | Why, and for how long |
|---|---|---|
| The file itself | Not as stored | Stored encrypted under the key in your link, which we do not keep. It is readable on our server for the moment it is uploaded and encrypted, and again for each request to an embed or raw link, whose key arrives in the address. Private links are decrypted in the browser instead, see where the key travels. |
| The key in your link | No | Not stored. If you set up a key pair, a copy is kept sealed to your public key so the dashboard can rebuild your links; only your private key opens it. |
| Name, type, size and dates | Yes | Kept as long as the upload, so the site can list, serve, expire and count it. The same goes for its view and download counts, and for the folder and tags you give it. |
| The link preview image | Yes | For private links only. A small image made at upload time so Discord and others can show something: blurred by default, a full thumbnail, or none. It is encrypted under a password kept beside it, so our server can open it, and only it. Choose None under Settings › Uploads in the dashboard and nothing is made. |
| A fingerprint of the file | Yes | A SHA-256 hash of the file as it arrived and as it was stored. It recognises the same file anywhere on the site: to refuse one that was removed before, and to find the other copies when one is reported. A hash cannot be turned back into the file, but whoever holds the same file can compute it and see whether it is here. |
| The address it came from | Yes, 30 days | The IP address of the upload, kept to answer abuse reports and lawful requests and nothing else. An hourly job clears it once the upload is 30 days old. |
How it works
On upload
- A random password is generated, 22 Base64URL characters
- A random 16 byte salt is created for this one file
- HKDF-SHA256 derives a 256 bit key from password and salt
- The file is cut into 64 KB blocks
- Each block is encrypted with AES-256-GCM and sealed with its own tag, which also covers its position and whether it is the last one
- A short check value is stored with it, so a wrong password is recognised as wrong
- Only the encrypted blocks are stored
On retrieval
- Someone opens the link, which carries the password
- The version, salt and counter are read from the file header
- The same key is derived again from the password
- Each block is checked against its tag before its contents are used
- Decryption happens on the fly, in memory. Jumping around in a video only fetches and checks the blocks you jumped to
- The decrypted file is never written back to storage
Where the key travels
This is the part worth being precise about, because it differs per link type. The key is never stored on our side in any of these cases, but it does not always stay in your browser either.
Embed and raw links
/aB3dK9.png?key=…
The key sits in the query string, so it reaches our server with every request. It has to, because the server is what decrypts the file to serve a preview or the raw bytes. It is used for that one response and never written to the database.
Be aware of what a query string is, though. Unlike a fragment it is a normal part of the address, so it travels wherever the address does. Our access logs do not record query strings and the key is not written to the database, but that only covers our end: it still lands in the browser history of everyone who opens the link, and it passes through whatever sits between them and us. If the file is one you would rather we could not open at all, the link type below is the one to use.
Private links
/p/aB3dK9.png#key=…
Everything after # is a fragment, and browsers
never send fragments to the server. The encrypted file is fetched and decrypted in your browser.
The key never leaves your machine. A crawler cannot open the file either, so the link
preview comes from the separate image described under
what we can see,
or there is none if you turn it off.
The dashboard gallery
Also fully local. We hold an encrypted copy of each file password that only your private key can open, and the unwrapping happens in your browser. See viewing your uploads below.
Why this holds up
Standard cipher
AES-256-GCM from Node's own crypto library, with a unique nonce for every block. There is no home-made crypto here.
Per-file salt and IV
Two identical files produce completely different ciphertext, so the stored files cannot be compared. The fingerprint listed above is the deliberate exception.
One key per file
No master key exists. Every upload has its own, so one key that gets out opens exactly one file.
Parameters
| Cipher | AES-256-GCM over 64 KB blocks |
| Key derivation | HKDF-SHA256 |
| Derived key | 256 bits (32 bytes) |
| Salt | 128 bits (16 bytes), random per file |
| Base nonce | 96 bits (12 bytes), random per file. Each block takes its own nonce from this plus its block number, so no two blocks share one |
| Key check | 128 bits (16 bytes), HMAC-SHA256 over a fixed string. Reveals nothing about the file, and lets a wrong password fail with an error instead of producing noise |
| File password | 22 Base64URL characters, about 132 bits of entropy |
| Password wrapping | X25519 and HKDF-SHA256, then AES-256-GCM, only when you set up a public key |
| Per-block tag | 128 bits (16 bytes), covering the block, its number and whether it is the last. That is what makes altering, reordering or truncating a stored file fail instead of pass |
| Not covered | The file name, type and size, the preview image and the fingerprint. All of them are listed under what we can see |
Stored format
The leading version byte is 0x05
for everything written today. Blocks are a fixed size, so the block holding any given byte can be
worked out by dividing: that is what lets a video player fetch and check the middle of a file
without reading what comes before it.
Making a link stop working
A key that cannot be taken back is only half of sharing something. The other half is being able to decide up front how long it stays useful. Two settings do that, and they work independently: whichever runs out first ends the link.
Expire after
From one hour to thirty days, or never. Once the time is up the file is deleted, bytes first and record second, by a job that runs every hour. Nothing is served in the meantime.
Download limit
Up to a hundred, or once and then gone. When the last one is used the link stops working immediately and the file is queued for deletion.
How a download is counted
- Per visitor, not per request. Opening a video and dragging through it makes dozens of requests for pieces of the same file. That is one person watching one file, and it counts once. Without this rule no limit under about fifty would be usable for video.
- A wrong key costs nothing. On embed and raw links the count is only taken once decryption has succeeded, so somebody trying a mangled link cannot burn through an allowance that was not meant for them.
- On private links it counts fetches. The key is in the fragment and never reaches us, so the only thing we can observe is that the encrypted file was handed over. We cannot tell whether it then opened.
Setting them
Both have an account default under Settings › Uploads in the dashboard, and both can be set per upload, which wins over the default. ShareX and the desktop client send them as ordinary form fields:
curl -X POST https://imageserver.pw/api/v1/upload \
-H "X-IMAGESERVER-AUTH-KEY: your-user-key" \
-F "[email protected]" \
-F "expires=24h" \
-F "maxDownloads=1"
maxDownloads
accepts 1, 5, 10, 25, 100 or unlimited.
Sending it explicitly is how a single upload overrides an account default in either direction.
Viewing your uploads in the dashboard
Because we do not keep file passwords, a plain upload list could not show you anything but filenames. The way around that is a key pair of your own: we store each file password encrypted to your public key, and your browser unwraps it with the private key you never hand over.
Setting it up
Supported key formats
- X25519 keys: public as PEM SPKI, private as PKCS#8 PEM
- OpenSSH formats such as
ssh-ed25519are not accepted. Convert to PEM, or let the dashboard generate the pair for you
Only applies going forward
A wrapped password is stored at upload time, so only files uploaded after you set up the public key show up decrypted here. For anything older you still need the original share link.
When it fails
- No key set: the upload is older than your key pair, so no sealed copy of its key exists. Only its original share link opens it
- Key error: the loaded private key does not open this upload's sealed key, usually because the upload belongs to a key pair you had before
- Fetch error: the encrypted file could not be downloaded, which is usually the network
- Decrypt error: the key opened but the file did not decrypt with it. Reload once; if it stays, tell us on Discord
The private key stays with you
We only ever see your public key and the wrapped passwords. Lose the private key and those files cannot be recovered by anyone.
Sharing stays the same
Share links still carry their own key in the URL. Anyone holding the full link can open the file, key pair or not, so pass them on deliberately.
Common questions
Can staff open my files?
Not from what we store. Without the password from your link the stored bytes are noise, and we keep no copy of that password. Your file does pass through our server readable at the moment you upload it, which is where this differs from end-to-end encryption, and the preview image of a private link is readable by design. Everything else we can read is listed under what we can see.
What if I lose the link?
Then the file is gone, irreversibly, unless you had a key pair set up when you uploaded it. There is no recovery path and no support ticket that changes that.
Is AES-256 actually enough?
Yes. Brute forcing a 256 bit key is not a matter of patience, it is physically out of reach. The weak points of a service like this are elsewhere: where the key travels and what is kept beside the file, which is why both are spelled out on this page.
Why HKDF, when password hashing usually means scrypt?
Because this is not a password anybody chose. scrypt, bcrypt and Argon2 exist to make a guessable secret expensive to guess, and the cost they add is the whole point. The key in your link is 132 bits from a random number generator, which is already beyond guessing, so a slow derivation would buy nothing and charge for it: every seek in a video paid for it again, and your dashboard paid for it once per thumbnail. HKDF is the right tool for a secret that is already random.
Can I encrypt files myself before uploading?
Nothing stops you, and the result is stored and encrypted again like any other file. Note that we then cannot render a preview, since to us it is just bytes.
Does encryption slow things down?
Not noticeably. The key derivation happens once per request and the cipher itself runs in hardware on any modern CPU.
Try it on a real file
Upload something from the homepage and look at the link you get back. The part after the
# is the whole story.