If you haven’t heard, Firefox Send is a service that solves the problem of sending large attachments without going through email. It does this in a privacy-preserving manner by encrypting the file in your browser first, before upload.
The concept is simple:
- An encryption key is generated in your browser
- Your file is encrypted with that key before being uploaded to the server.
- The download URL is returned by the server,
but will only work after the browser appends the secret key to the URL fragment.
Note that URL fragments are never sent to the server. They are often used for page anchors, and sometimes to keep track of local state in SPA.
This has been made possible through the use of Web Crypto API exposed via JavaScript.
Technical Details
The code that powers Firefox Send is actually open source, so you can run your own server, or read the code to figure out exactly how it works. The encryption details are documented in docs/encryption.md.
A master key is first generated and from it, a few keys are derived using HKDF SHA-256. The derived key length depends on its purpose, so for AES-128 encryption, the key will be 128-bit. Oddly though, the Subtle Crypto API returns a a 512-bit key for HMAC SHA-256, which had me stumped for a while. I wrote some code that you can try out online.
Because HKDF is based on a hash algorithm, derived keys are inherently not reversible to obtain the master key from which they were derived (unless the algorithm itself is somehow broken).
3 keys are derived from the master key:
- Data Encryption key. Used to encrypt the actual file contents.
- Authentication key. Given to the service and used to authenticate future downloaders.
- Metadata key. Used to encrypt the upload manifest (filename and size information) for display.
To download a file from the server, you need to authenticate using a challenge-response protocol. This uses the authentication key that was derived.
Due to its use of different keys, this scheme enables a weird use case where the file can be retrieved by an oblivious third party, who can download the file on your behalf, but yet not be able to decrypt its contents.
Authentication is a means to prove that you have access to the file. A new challenge is generated upon each successful authentication. You have to authenticate when you are fetching the file metadata or its contents. This presents a chance for race conditions, because you first need to retrieve the challenge from the server before doing an operation. If multiple users simultaneously attempt a download, the challenge would have changed between requests and additional round trips are required. Interestingly, this mechanism might deter people from (ab)using the service as a file share because that was not its intended purpose.
Note that the metadata key is used purely to encrypt the upload metadata for presentation in the UI. This includes information like the file name, size and its MIME type. The metadata is not necessary for decryption at all, and can be totally skipped when downloading.
File Encryption
Files are encrypted using 128-bit AES-GCM. GCM is an authenticated mode, which offers better integrity guarantees than just CBC.
As this service (or “experiment”, as Mozilla calls it) is in its infancy stages, the protocol may see changes. This impacts third party clients (more on that in the following section), which need to keep up with the changes in order to function.
Note that because both the server and client-side code is written in Javascript and the client runs in a web browser, they can evolve in lock-step more easily than conventional client-server applications. The client-side code is almost always up-to-date, unlike regular programs installed on the user’s computers.
In the previous version, vanilla AES-GCM encryption was used, together with a MIME multipart upload by the client. An easy-to-read Python implementation for this protocol can be found here.
In this newer version (mid 2019), it was changed to Encrypted Content-Encoding or ECE (which also uses AES-GCM internally) and WebSockets for uploading each chunk. ECE was designed to adapt the block-based cipher for use in a stream-oriented protocol like HTTP. Special considerations for AES-GCM, like not reusing the nonce, has already been spelled out in the RFC.
Encrypted Content-Encoding
ECE also uses HKDF with SHA-256 to derive the actual content encryption key (CEK), and for generating the nonce for each block. Because of this, Firefox Send doesn’t need to derive a separate data encryption key from the master key, but rather uses it directly as the input-keying material for the ECE protocol.
ECE has a record header (shown below), followed by each encrypted chunk. The chunk sizes are defined by a record size field in the ECE header. GCM adds overhead for each chunk, due to the authentication tag.
+-----------+--------+-----------+---------------+ | salt (16) | rs (4) | idlen (1) | keyid (idlen) | +-----------+--------+-----------+---------------+
During file upload over a WebSocket connection, the download settings are first transmitted, together with the encrypted file metadata. The ECE record header is sent next, followed by the encrypted chunks of the file. Because the server does not have the encryption key, the file blocks are opaque to it — there is no verification or checking to determine if they were received correctly. Decryption and checking will be done when the file is retrieved by the client.
Clients
Note that relying on JavaScript for client-side encryption might be risky. For one, the service operator or a privileged third party may deploy malicious JavaScript code that transmits the secret key. Because it already has the file, it can now decrypt the file’s contents. The malicious code could also just upload the file without encryption. Users would be oblivious to these malicious changes in JavaScript code, unless they audited the code every time they visited the site.
To counter that, you can use alternative non-web-based clients to upload and download files. That way, you can be sure the files are really encrypted before uploading it to the server, provided you audited the code once before.
Apart from existing third-party clients, I also wrote another one in Go, unimaginatively called ffsendr. This particular implementation offers some control over the encryption and decryption processes.
This was not meant to be a fully-featured client, but rather one that is small, hackable and easy to audit. This is why, for instance, the client does not download and decrypt the metadata when downloading files. Therefore, downloaded files can only be named as [fileId].bin
instead of cat-pic.jpg
. The metadata is, however, uploaded to the server for interoperability, so that someone can always use the share URL to download the file.
If you are looking for a more capable client, see the later section below for a list.
Walkthrough
To upload a file, use ffsendr upload
like so:
ffsendr upload cat-pic.jpg
You can optionally specify the -key
flag if you want control over the master key used to encrypt the file. Since the service is open source, you can also run your own instance either locally or “in the cloud”, and you can specify the -serviceUrl
flag to use a third-party server.
You can download the files by hand, using curl
and some code to answer the authentication challenge. The challenge is contained in the WWW-Authenticate
header of the download URL. Sign the challenge using the derived authentication key to download the file with curl
:
curl -H "Authorization: send-v1 xxx" -vv \
https://send.firefox.com/api/download/aabbccdd/
Note that the download is using an API endpoint, not the original share URL.
You can achieve the same thing with the Go client. Simply leave out the secret key URL fragment, like so:
ffsendr -authkey xxx download https://send.firefox.com/download/aabbccdd/
But notice that in lieu of the secret key, you will need to specify the authentication key using -authkey
.
Due to the lack of the encryption key, the file has to be manually decrypted after download:
ffsendr -key <key> decrypt aabbccdd.bin
Feel free to download the Go client, try it out, hack it up or whatever.
Alternative Clients
There have been a few clients already written for Firefox Send. However, I know only of one that has been updated to work with the new ECE protocol.
- https://github.com/nneonneo/ffsend
- https://github.com/ehuggett/send-cli
- https://github.com/timvisee/ffsend
The last one is written in Rust, and I would say it’s the most feature-complete, but yet quite complex that I spent some time poring through the codebase just to realize the protocol is implemented in a separate library.
Conclusion
I have to say that I’m not an expert on cryptography. That being said, here are my thoughts on the service.
After reviewing the encryption procedure, I feel pretty confident to use this service for sending files, provided you use a third-party binary client.
Also, unlike other file sharing services (or file syncing actually), I don’t have to sign up for an account to use the service. You can transfer up to 1GB this way, good for small video clips, or a KNOPPIX CD ISO.
Unless there’s a vulnerability in SHA-256 or AES-GCM, I don’t think the uploaded contents will ever be exposed. However, if you are still worried, another round of encryption can be applied by hand before the file is uploaded.