Trust

How Empti keeps your files private

What is encrypted, with which keys, what our service can and can't see, and where the limits are. Written for people who want to check before they trust.

Last updated 8 November 2026

In short
  • Every file is split into 4 MiB chunks and each chunk is sealed with AES-256-GCM on your Mac, before anything is uploaded.
  • Your files are locked by a random 256-bit key. Your passphrase protects that key; it never leaves your Mac, and neither does the unlocked key.
  • File names, folders and sizes are in an encrypted list. Our storage only sees random ids and encrypted bytes.
  • Share links and team invites carry their key after the # in the link, which browsers never send to a server.
  • We can still see account details, how much you store, and when. If you lose both your passphrase and your recovery key, nobody can get your files back, including us.

What Empti can and can't see

"Empti" here means our service: the API, the database and the storage bucket. Everything in the right-hand column is encrypted on your Mac with keys the service never holds.

The service can seeThe service can't see
Your email, display name and profile photo, and how you sign in (Apple or Google)Your files' contents
The Macs you're signed in on, by the name each one reports, and when each was last usedFile and folder names, and how folders are organised
How many encrypted objects you store, the size of each, and when each was uploadedWhich object is which file, or what type of file it is
A copy of your vault, locked with your passphrase (so another Mac can open your drive)Your passphrase, your recovery key or your unlocked vault key
For each share link: when it was made, its expiry, whether it has a password, its download limit, file count, total size, and view and download countsWhat a share link contains, its key or its password
Team names, who is in each team and their roles, and each member's public keysTeam keys, private-drive keys, or anyone's private keys
Who commented on which (random) item id and when, who they mentioned, and who reactedWhat a comment says, which file it is on, or which emoji was used
Folders you've given an agent: the label you chose, the agent computer's name and public keys, and which encrypted objects it may readThe folder's real name, what's in it, or its folder key
When you sign in a computer with a code: its name and the IP address it asked from, kept until the code is used or has expired (codes last 10 minutes)Search labels, saved collections or the names of your drives
Network details any web service sees, such as IP addresses and request timesThe secrets of the S3 gateway keys you make, which stay on that computer

Your keys

One random key per vault

When you set up Empti, your Mac makes a vault: one random 256-bit master key. Every other key is derived from it with HKDF-SHA256, each with its own label, so a key made for one purpose can't be used for another.

# made on your Mac; nothing here is sent anywhere unencrypted
master key   = 32 random bytes
file key     = HKDF-SHA256(master, salt: content id, info: "empti-file-v1")
list key     = HKDF-SHA256(master, info: "empti-manifest-v1")
other keys   = HKDF-SHA256(master, info: "empti-labels-v1", …)
vault id     = HKDF-SHA256(master, info: "empti-vault-id-v1"), first 16 bytes

The vault id is a public name for the vault. It lets a Mac check that a recovery key, or the copy kept in your account, belongs to the same vault, without revealing anything about the key.

Your passphrase

The master key is stored only after it has been locked with your passphrase. Empti stretches the passphrase with PBKDF2-HMAC-SHA256, 600,000 iterations and a fresh 16-byte random salt each time it locks the vault, then uses the result to seal the master key with AES-256-GCM. Passphrases must be at least 8 characters.

We chose PBKDF2 because Apple's own crypto libraries provide it and we didn't want to write our own or bundle an unreviewed one. Argon2id would be stronger, since it is memory-hard. The vault records the KDF's name and settings, so a later version can move to Argon2id without breaking existing vaults.

On your Mac

The locked vault (vault.json) sits in Empti's app folder. When you unlock, the master key goes into your Mac's Keychain, in an item marked this device only and readable only after you've logged in to your Mac once since it started. The Empti app and its drive extension share that one Keychain item, so the key is never written to an ordinary file. Lock in Settings removes it, and Empti does the same when the service signs that Mac out.

In your account

When you're signed in, your account keeps a copy of the locked vault, with its vault id. That's how a second Mac, or the empti command, opens your drive with your account and your passphrase instead of your recovery key. The service can't open it, but it does hold it, so the strength of your passphrase matters: see Limits.

Changing your passphrase locks the same master key again under the new one. Your files don't change, and every other Mac and the empti command are signed out, so each has to sign in and unlock with the new passphrase.

Your recovery key

The recovery key is your master key, written out so a person can copy it: the 32 key bytes plus a 2-byte SHA-256 checksum, in base32 without the easily confused letters I, L, O and U, grouped in fours after EMPTI-. The checksum catches typos, and case and spacing don't matter.

If you forget your passphrase, the recovery key lets you set a new one. Before saving anything, Empti checks that the key belongs to your drive: against the vault id in your account, or by opening the first chunk of one of your files. Because the recovery key is the master key itself, changing your passphrase doesn't change it. Keep it somewhere safe and offline.

Files

Chunks

Each file is split into chunks of 4 MiB (the last one can be shorter). Each chunk is sealed on its own with AES-256-GCM (Apple CryptoKit), using that file's key and a fresh random 96-bit nonce. A stored chunk is 12-byte nonce ‖ ciphertext ‖ 16-byte tag, so it is 28 bytes longer than the plaintext.

Each chunk is also bound to where it belongs. The associated data covers the file's content id, the chunk's position and whether it's the last chunk:

"empti-chunk-v1"
  ‖ content id    # 16 bytes
  ‖ chunk index   # 8 bytes, big-endian
  ‖ last chunk?   # 1 byte: 1 for the last chunk, else 0

So a chunk can't be moved to another position, swapped with one from another file, or cut off the end of a file without failing to decrypt. If one byte of a stored chunk is changed, that chunk fails and Empti reports a read error; it never hands an app altered data. The other chunks still open.

Why chunks

Chunks are what let a file open without downloading it. When an app reads part of a file, Empti fetches only the chunks that cover it, checks and decrypts them, and passes on the bytes. The stored object is the chunks back to back, with no header, so the position of any chunk can be worked out from the file's size.

Storage

Each file's content is stored as one object named v/<content id>, where the content id is a random UUID. The name says nothing about the file. Uploads go straight from your Mac to storage using links the service signs (below); the service itself never handles file bytes.

On your Mac

  • The cache (recent files, and files you choose to keep on this Mac) holds the chunks exactly as stored, still encrypted. They're decrypted each time they're read.
  • New files you copy into the drive are kept as they are, unencrypted, in Empti's app folder on your Mac until their upload is confirmed. Then they're removed.

Versions

Saving a file uploads its new content as a new object with a new content id, and so a new file key. The previous content stays, as a version, for 30 days (up to 20 versions per file), and a deleted file stays in Recently deleted for 30 days. The version history is kept inside the encrypted file list, not on the service.

Names, folders and other details

Names, folder structure, sizes and version history live in each drive's file list (manifest.enc), sealed with AES-256-GCM under the list key. The service stores it and hands it to your Macs, but can't read it. Other small details are sealed the same way, each under its own derived key and label:

  • the names of your drives (drives.enc);
  • search labels and saved collections;
  • your team identity keys (identity.enc, see Teams);
  • for each share link, its title and key, so you can copy it again from any of your Macs.

In teams, comments and activity are sealed with the team's key and signed by whoever wrote them (Ed25519). The signature and the sealing both cover where the comment is filed, so a comment can't be moved to another file or passed off as someone else's. The service keeps only an index so it can count, notify and check permissions: who wrote a comment, on which random item id, when, and who it mentions. Activity is deleted after 90 days.

Share links

A share link looks like https://api.getempti.com/s/<id>#<secret>. The secret after the # is 32 random bytes made on your Mac. Browsers don't send that part of a link to any server, so the service never sees it.

S            = 32 random bytes      # the link's #fragment
# password links only:
P            = PBKDF2-HMAC-SHA256(password, 16-byte salt, 600,000)
ikm          = S, or S ‖ P with a password
manifest key = HKDF-SHA256(ikm, info: "empti-share-manifest-v1")
access       = HKDF-SHA256(ikm, info: "empti-share-access-v1")
# the service stores SHA-256(access), never S, P or the manifest key
  • What a link opens. Its sealed list holds the names, sizes and file keys of exactly the files you shared, as they were when you made it. It never contains your master key, and a file key opens only that one version of that one file.
  • In the browser. The recipient's browser derives the keys, opens the list and decrypts each chunk itself with Web Crypto. No Empti account or app is needed.
  • What the service checks. The visitor sends access, which only the secret (and the password) can produce. The service compares its hash, then signs download links valid for 15 minutes, each for one encrypted object in that share. A link can't reach any other object.
  • Passwords. A password is never stored. Guessing one means asking the service, which allows 20 wrong attempts per link per 15 minutes; the service can't guess offline because it doesn't have the secret.
  • Limits. Links expire after 1, 7 or 30 days, with no option for longer. Turning a link off stops new downloads at once; download links already handed out last up to 15 minutes.
  • The page. It's served with a strict Content Security Policy (scripts only from Empti's own share page, connections only to Empti and the storage host), no referrer, no framing, no caching, and a header that stops Cloudflare adding scripts that could read the link.

Teams

Keys

Each person has an identity: an X25519 key pair for receiving keys and an Ed25519 key pair for signing. The private halves are sealed with your personal vault and kept in your account and your Keychain, so all your Macs and the empti command can use them. Only the public halves are published.

A team has its own random 256-bit team key, which works exactly like a personal vault's master key. It reaches each member wrapped to their identity with HPKE (RFC 9180 base mode: X25519, HKDF-SHA256, AES-256-GCM), labelled with the team and key version so a wrapped key can't pass for another team's or another version's. Open team drives derive their keys from the team key. A private drive, which only some members can open, has its own random key wrapped the same way to each person on it, and each new version is signed by the person who made it.

Invites

An invite link works like a share link: its secret is after the #. It seals the team's keys and a fingerprint of the team's history, and it derives a signing key that the joiner must use, so only someone holding the link can join. The service stores only a hash it can check. Invites last 7 days and work once.

An invite by email carries no secret. Instead, the joiner and the approving admin compare a 6-digit code built from both sides' random values and the joiner's keys. The joiner commits to their keys before seeing the admin's random value, so a service that swapped keys would have one chance in a million per attempt, and a failed attempt shows in both apps. Until the joiner has also checked the team code with someone in the team, their Mac reads but seals nothing new.

The signed membership log

Who is in a team, with which keys and roles, is recorded in an append-only log. Every entry (invite, join, add, role change, remove, new key) is signed with the Ed25519 key of the person who made it and chained to the previous one by its SHA-256 hash. Entries are canonical JSON (RFC 8785), so every Mac checks the same bytes.

  • Each Mac checks the log itself, and keeps its own record of the latest point it has verified (in the Keychain, and sealed in your account), which the service can't reset or roll back.
  • Apps trust a team key only through the log. The service can't add a member, get a key it made accepted, or forge an entry.
  • To check that you see the same team and history as someone else, Empti shows codes of eight words (88 bits): a team code that never changes, and a history code for a point in the log. Compare them with the owner or an admin.

When someone leaves

When a member is removed or leaves, the service stops giving them storage links straight away; links they already have last at most 15 minutes. An admin's Mac then makes a new version of the team key, records it in the log and wraps it only to the remaining members. Everything written from then on uses the new key, and lists and comments are sealed again with it.

Older files are not encrypted again. Someone who was removed can still open files they could open before, if they kept copies of the old key or the files. Empti says so when you remove someone.

Sign-in and sessions

Your Empti account and your vault are separate. Signing in lets a Mac reach your encrypted storage; it doesn't unlock anything. You need your passphrase for that.

  • Ways to sign in: Sign in with Apple (the identity token is checked, with a one-time nonce) or Google (an authorisation code with PKCE). A computer without a browser signs in with a code you approve (below). Accounts are never matched across providers by email.
  • Email and password sign-in is built but switched off. If we turn it on: passwords must be 10 to 200 characters, and they're stored as PBKDF2-SHA256 (100,000 iterations, random salt) of an HMAC with a secret pepper held outside the database. After 10 wrong passwords, an email is refused for 15 minutes, and a wrong email takes as long to refuse as a wrong password.
  • Tokens. Each signed-in device gets an access token that lasts one hour (HMAC-SHA256 signed) and a refresh token that lasts 90 days from its last use. Only a hash of the refresh token is stored. Each refresh replaces it, and if an old one is ever used again, the session ends, because a copy is in someone else's hands.
  • Devices. You can see every signed-in Mac and computer and sign any of them out. Its refresh token stops working at once; its access token can last up to the rest of its hour, but changing your vault needs a live session.
  • On your Mac the session is kept in the Keychain.

The empti command

The empti command runs on a Mac or on Linux. It signs in, unlocks and encrypts the same way the app does. See Command line.

  • Signing in. empti login on a Mac signs in with Google in your browser, through a one-time redirect to your own computer. On Linux, or with empti login --device, it prints a code such as BDFH-KMNP instead. You approve it within 10 minutes from a device that's already signed in: in the app (Settings → Account → Sign in a computer) or with empti approve. Both show the computer's name and the address it asked from first, so you can refuse a code you didn't start. The computer polls with a separate 256-bit secret, of which the service stores only a hash. Approving signs that computer in to your account, with a session of its own you can sign out like any Mac. It still needs your passphrase to open any file.
  • Unlocking. empti unlock asks for your passphrase, opens the locked vault from your account on that computer, and keeps the key until empti lock or empti logout. On a Mac it's kept in your login Keychain. On Linux, and with EMPTI_SECRETS=file on a Mac, the session and key are kept in files only your user can read (mode 600). Anyone who can read them as you, or as root, can open your files until you lock. On a shared machine you can skip unlock and pass the passphrase for each run instead.
  • The whole vault, or one folder. A signed-in, unlocked empti holds your whole vault key, so anything that can run it can read and change every file in your drive. For an agent on a server you can give one folder instead, described next.
  • S3 tools. empti serve s3 serves your drives to S3 tools (rclone, the AWS CLI, boto3) on 127.0.0.1 only, and encrypts and decrypts on that computer. The access keys it checks are kept only on that computer, in $EMPTI_HOME/s3-keys.json (mode 600). The secrets are stored in plain text there, because checking an S3 signature needs them. A key can be read-only and limited to one drive or one folder.

Giving an agent one folder

An agent's computer can hold the keys to one folder of your personal drive and nothing else. It never signs in to your account and never sees your vault or passphrase.

# made on your Mac for each folder you give
F            = 32 random bytes, per folder and per version
join token   = EMPTI-JOIN-<folder id>-<t>    # t: 128 random bits
# made on the agent's computer, never leaves it
agent keys   = X25519 + Ed25519 key pairs
proof        = HMAC-SHA256(t, folder id ‖ agent's public keys)
# once you let it in
wrapped F    = HPKE to the agent's X25519 key,
               info: "empti-folder-key-v1|<folder id>|<version>"
pair key     = HKDF-SHA256(t,
               info: "empti-scope-pair-v1|<folder id>|" ‖ X25519 key)
grant        = HMAC-SHA256(pair key, folder id, version, hash of F)
  • Pairing. empti scope create or Settings → Agents prints a join token. The agent runs empti agent join with it, and your Mac checks the proof, then shows the computer's name and key fingerprint. Nothing is wrapped to it until you let it in. The service never sees t, so it can't join as the agent or forge a grant. The agent uses a folder key only if its grant checks, and never goes back to an older version.
  • What the agent can open. A separate list, sealed with F, names only what's inside the folder and carries those files' keys. The folder's own name and everything outside it are left out.
  • What the service lets it do. The agent gets its own session, with 5-minute access tokens, that reaches only that folder's objects. It can't fetch your vault, your identity keys or anything else in your account, and it can never delete a stored file. What it writes is brought into your drive by a computer holding your vault (the app does this every 30 seconds while it runs); overwrites become versions and deletes go to Recently deleted.
  • Revoking (empti scope revoke, or Settings → Agents) takes effect on the agent's next request. Files it already read, it may have kept.

Infrastructure

  • Service: a Cloudflare Worker. Account records, sessions, share settings and team membership are in Cloudflare D1. Encrypted objects are in Cloudflare R2.
  • Storage links. Your Mac never holds storage credentials. The service checks who you are and signs short-lived links (AWS Signature Version 4) for your own objects only: downloads for an hour (15 minutes for team files), uploads for 15 minutes, multipart upload parts for an hour. Every key is checked against the names Empti writes, so a request can't reach another account's files.
  • Email: team invites and billing notices are sent through Resend, with no tracking pixels or rewritten links. Invite emails contain no keys and no file names.
  • Payments: handled by Stripe's hosted checkout and billing portal. Card details go to Stripe, not to Empti. Stripe's notifications to us are signature-checked and refused if older than 5 minutes.
  • Encryption code: Apple CryptoKit and CommonCrypto on the Mac, Web Crypto in browsers and in the service. We don't use our own cipher implementations.

No independent audit has been done yet.

Limits and threat model

Encryption protects what's in your files. It doesn't hide everything, and it can't protect against everything. Here is what it doesn't cover.

  • Sizes and timing are visible. The service sees how many objects you store, their exact encrypted sizes (each is the file's size plus 28 bytes per 4 MiB chunk), and when you upload, download and share. From that, someone could guess roughly how many files you have and how big they are.
  • Your passphrase can be guessed offline by anyone with your locked vault. That includes us, or someone who stole our database. Each guess costs 600,000 rounds of PBKDF2, which isn't memory-hard, so a short or common passphrase could be found. Use a long passphrase you don't use anywhere else. Your recovery key, a random 256-bit key, can't be guessed.
  • Your Mac is trusted. Once unlocked, the master key stays in your Keychain until you lock it, and new files sit unencrypted on your Mac until they've uploaded. Malware running as you, or someone using your unlocked Mac, can read your files. A stolen Mac's access lasts until you sign it out, and in a team, until an admin removes it.
  • Losing both your passphrase and your recovery key loses your files. We can't reset either, and there is no way round it.
  • Anyone with your recovery key has everything. It's the master key, and changing your passphrase doesn't change it.
  • Share links are only as private as the link. Anyone who has the whole link (and the password, if it has one) can open what it shares, and anyone who downloaded a file keeps it after the link ends.
  • The share page comes from our server. A browser decrypts with the code our server sends it, so a compromised service could send a page that leaks the link's secret to it. The Mac app doesn't have this weakness.
  • Team members you remove keep what they had. Files they could open before stay readable to them if they kept copies.
  • Teams need everyone up to date. Members on an older Empti don't check the membership log, and Settings names them. An invite by email trusts our service to show the right team until the joiner has checked the team code. A member can also choose to trust our service instead of the checks for a team, and the rest of the team sees that.
  • Owners and admins are trusted to manage their team. An admin can add anyone, including a key our service controls. Every add is signed and shown to the whole team, but it isn't prevented.
  • Some details are plain to the service: your email, name and photo, team names, member lists and roles, and the index of who commented where.
  • We can refuse service. We, or someone controlling our service, could delete or withhold your encrypted data or stop you signing in. Encryption doesn't prevent that. Keep a separate backup of anything you can't afford to lose.

Reporting a vulnerability

If you think you've found a security problem in Empti, the app, the service, share links or the empti command, please email security@getempti.com with what you found and how to reproduce it. Please don't access other people's data or disrupt the service while testing, and give us a reasonable time to fix it before telling others.

We'll reply to let you know we've received it within 3 working days and keep you updated until it's fixed.

For anything else, see Privacy or contact support.