Security and threat model
YAFL encrypts every file on your own machine before it leaves. The key that decrypts it travels only in the URL fragment, which browsers never send to a server, and it is separate from the API key your agent uses. This page describes what that protects, names the cryptographic primitives so the claims can be checked, and states what it does not protect against. YAFL has not had a third-party security audit.
What is and is not protected
| Question | Answer |
|---|---|
| Can the YAFL app server read my file? | No. The app server never receives the file bytes; object storage receives only ciphertext. |
| Can it read the filename or content type? | No. The metadata is encrypted separately, under the same scheme. |
| Does the app server carry the file bytes? | No. Bytes go from your machine straight to object storage over a pre-signed URL. |
| Can whoever holds the link read the file? | For a standard link, yes: the link contains the key. A password-protected link also needs the password. Either way, treat the link as the secret. |
| If my agent's API key leaks, are my files exposed? | No. The API key is not the decryption key. See below. |
| Does the server know who I am and when I sent something? | Yes. It knows your account email, the size, and the timing. This is the metadata YAFL does see. |
| Has the crypto been independently audited? | No. Solo project, unaudited. The design is public here so you can judge it yourself. |
The cryptography, precisely
Every claim in this section is implemented in one small shared module (packages/crypto) that runs unmodified in both the Node process (the MCP server and CLI) and the browser, through the same WebCrypto crypto.subtle API. There is no second, divergent implementation to get out of sync.
- Cipher: AES-256-GCM. One cipher. There is no algorithm-negotiation step and no cipher menu, because agility is a place for bugs to hide, not a feature.
- Key: a fresh 256-bit key per file, generated from the platform CSPRNG (
crypto.getRandomValues), neverMath.random. Keys are never reused across transfers. - Nonce/IV: a fresh 12-byte IV is generated inside the encrypt function on every call and can never be passed in from outside. This is deliberate: threading an IV in from a caller is exactly how GCM nonce reuse happens, and this design makes that mistake unrepresentable. The IV is prepended to the ciphertext so decryption can recover it without a side channel.
- Integrity: GCM's authentication tag is verified on decrypt. A tampered or corrupted blob throws; it never decrypts to silent garbage.
- Metadata: the filename and content type are encrypted into their own separate AES-GCM blob, with their own fresh IV. The server stores that blob and cannot read it.
Where the key lives, and why that is the whole design
A YAFL link looks like https://yafl.dev/t/{id}#{secret}. The part after the # is the URL fragment, and browsers do not transmit the fragment to any server. It is never in an HTTP request, never in a server log, never in the database.
There are two link shapes. A standard link carries the raw AES key in the fragment. A password-protected link carries only a random salt in the fragment, and the AES key is derived on the recipient's machine from that salt plus the password (which the server also never sees), so the link alone cannot decrypt without the password.
That is the trust model: object storage has the ciphertext, and the fragment carries the decrypting secret (the key itself, or a salt that needs the password), and that secret never reaches the server. When a recipient opens the link, the secret is read from the fragment by code running on their own machine, and decryption happens there.
The direct consequence, and the thing to internalise: for a standard link, the link is the key. Anyone who gets the whole link can read the file. So the honest security boundary is not "is it encrypted" (it is) but "who can see the link" (and, for a password link, the password).
The two-key split
There are two different secrets, and conflating them is the most common misunderstanding.
- The API key authorises your agent to create and manage transfers. It is a
yafl_-prefixed token with roughly 190 bits of entropy, stored on the server only as a SHA-256 hash, looked up by hash, and compared in constant time. You can hold one per agent or per machine, and revoke any one of them; a revoked key is dead on the very next request. - The decryption key is the per-file AES key. For a standard link it lives only in the link fragment; for a password-protected link it is derived on the recipient's machine from the fragment's salt plus the password. Either way it never touches the server.
So if the box your agent runs on is compromised and its API key is stolen, the attacker can create new transfers billed to your account and list or delete your transfer records, which is why revocation matters and is immediate. What the attacker cannot do with the API key alone is decrypt any file, because the decryption keys travel only in link fragments and are not stored server-side or derivable from the API key. (If a full standard link was also sitting on that box, in shell history or an agent transcript, that link is readable, per the leak caveat below. That is a link leak, not an API-key leak.)
What YAFL does not protect against
This is the section other products leave out. Read it before you trust the tool with anything sensitive.
- The link leaking. For a standard link, the link is the key. If a full link ends up in shell history, a browser history, an agent transcript, a screenshot, a chat log, or a paste into the wrong window, whoever sees it can download and decrypt the file until it expires. A password-protected link additionally requires the password, so a leaked link alone is not enough there. Mitigations exist (short expiry, one-time links, the password layer, all below) but none of them undo a leaked standard link.
- A compromised endpoint. Encryption and decryption happen in plaintext on the two machines at the ends of the transfer. If either machine is compromised, the file is readable there. YAFL protects the file in transit and at rest on the server; it cannot protect a machine that is already owned.
- Traffic and timing metadata. The server, and the infrastructure in front of it, necessarily see request metadata: your account email, the IP a request came from, the size of a transfer, and when it happened. The contents and the filename are encrypted; the fact and shape of a transfer are not.
- A malicious or subpoenaed server operator. This is the point of client-side encryption: even the operator sees only ciphertext and the metadata above. But a compromised server could serve altered web UI code to a browser in a future session. If your threat model includes the operator, use the CLI or MCP client (where the crypto is the installed, inspectable package) rather than the web UI, and pin what you install.
- Cryptographic review at scale. No third-party audit has been done. The design uses standard primitives (AES-256-GCM, PBKDF2-SHA256) with no custom cipher or invented protocol, and it is published here and in the source so that it can be checked, but that is a claim you should verify rather than take on trust.
Expiry, precisely
Two independent layers, and the distinction matters.
- Access cutoff (authoritative). Every attempt to resolve a transfer checks its expiry first. Once the 24-hour window has passed, the server returns
410and refuses to issue a download URL at all, before any storage is touched. This app-level check, run on every resolve, is what makes "the link expires in 24 hours" true for a user. - Storage cleanup (backstop, best effort). Separately, the object storage bucket has a lifecycle rule that deletes the ciphertext objects. This is a cost-and-liability backstop, and object-lifecycle deletion is best-effort timing, not second-precise. It is not the thing that enforces the 24-hour promise; the access cutoff above is.
One-time links add a third condition: the first successful download burns the transfer, and any later attempt is refused.
An optional password layer (v2 links) derives an additional key from a password using PBKDF2-SHA256 at 600,000 iterations over a fresh 256-bit salt that is itself carried in the fragment, so the password is a required second factor on top of the link, not the sole source of strength. The server never sees the password.
Headless authentication
An agent on a box with no browser should not need you to paste a long-lived secret into a dotfile. YAFL uses the standard device-authorization flow (RFC 8628): the agent prints a short code and a URL, you approve it from your phone, and the agent receives its own named, individually revocable API key. The long device code is stored server-side only as a SHA-256 hash (the plaintext is never persisted), the short user code is compared with a constant-time comparison (timingSafeEqual), and the grant is single-use.
Abuse controls
Bot protection (Cloudflare Turnstile) guards the account and magic-link surfaces, and the sensitive endpoints are rate limited (for example, magic-link requests and transfer resolution). These protect the service; they are not part of the file confidentiality guarantee, and are listed here only so the picture is complete.
Audit status, and how to check the claims
YAFL is a solo project and has not had a third-party cryptographic audit. Everything above is verifiable rather than asserted: the encryption module is a small, standalone package, the primitives are named exactly, and there is no custom cipher or invented protocol to reason about. If you find a hole in any of this, that is the most useful thing you can send, and it is welcome. Report security issues to the contact on the site rather than in a public thread, and expect an honest answer, including "you're right, here is what we are changing."
Install: npx -y @yafldev/mcp with
YAFL_API_KEY set, or the one-liner
curl -fsSL https://yafl.dev/install.sh | sh.