I saw Firefox Send pop up on HackerNews the other day and thought it was pretty interesting. I uploaded and downloaded a few small image files and then had the bright idea that it'd be cool to use Python to upload/download files from Send. This led me down a bit of a rabbit hole, similar to the way the author of this article got sucked into the Starbucks API.

If you'd like to follow along locally, the referenced image, frame00.png, is from Google's Clojure-Turtle repository.

The first things I looked at after uploading a file were the request headers that Chrome sent, the type of request being made (PUT or POST), and what the upload endpoint was.

The endpoint makes sense: /upload.

Looking at the headers, most of what you'd expect to see is present(user-agent, etc.), but one header stood out:

X-File-Metadata: {"aad":"ca2ddab30e7363330e4f29a00c344496ee607308f4eb3bbfb651e79b054370df","id":"29f4742b6b05d74fdcb0d698","filename":"frame00.png"}

My initial assumption, which turned out to be true, was that this is a required header. If X-File-Metadata isn't present or is mal-formed, the server will return a 404 or a 400 response code.

My nex step was to figure out what the aad and id values in the Metadata header were and how to generate them myself.

The aad value is 64 characters long, similar to a SHA256 hash:

>>>len('ca2ddab30e7363330e4f29a00c344496ee607308f4eb3bbfb651e79b054370df')
64

shasum proved that the value in aad was the SHA256 hash:

$ shasum -a 256 frame00.png | awk '{print $1}'
ca2ddab30e7363330e4f29a00c344496ee607308f4eb3bbfb651e79b054370df

Tracking down where id came from was a bit of a challenge. Initially, I thought this would've been a server-provided value, but as it turns out, that's not the case. I ended up looking through the source code for a while trying to figure it out. I'm not overly familiar with JavaScript, so it took me longer than it probably should have. I ended up finding these two lines in frontend/src/fileSender.js:

iv = window.crypto.getRandomValues(new Uint8Array(12));
const fileId = arrayToHex(this.iv);

I fired up the JS console in Chrome and ran the getRandomValues function above and saw this:

iv = window.crypto.getRandomValues(new Uint8Array(12));
Uint8Array(12) [248, 95, 181, 133, 118, 31, 119, 10, 236, 11, 14, 161]

A random array of numbers? Ok. Cool. We can do that in Python like this (it's not quite the same thing as an int8Array, but it's close enough and gets the job done):

random.sample(range(1, 254), 12)

I found the arrayToHex function in frontend/src/utils.js and pasted it into the console too. Passing the array from above into that function returned this:

arrayToHex(iv)
"89f26692a397ba1d3fbd16cb"

That looks like an ID to me. All we had to do know was repeat this process in Python. Easy:

>>> random_list = random.sample(range(1, 254), 12)
>>> file_id = binascii.hexlify(str(random_list))[:24]
>>> file_id
'5b36362c203233332c203233'
>>> # This also generates a 24 character string and might be more like what Send is actually doing:
>>> binascii.hexlify(np.array(random_list, dtype=np.uint8).tostring())
>>> '4afe5b9b993b4b22dec9cca9'

The last element of the X-File-Metadata header is just the filename. Pretty self explanatory.

After you send a successful POST to the /upload endpoint, you're given a short JSON response:

{"url":"https://send.firefox.com/download/2288aeef57/","delete":"666f4c2bee7e6fbd86b0","id":"2288aeef57"}

Most of the keys in the response are self explanatory. If you visit url in a browser, you'll be presented with the same download page you'd see if you uploaded a file through the browser.

If you click the download button in the browser, you'll see a request made to the /assets endpoint that ends with the id from the upload response: https://send.firefox.com/assets/download/{{ id }}. Entering that URL in a browser or something like wget or curl -O will download the file that was previously uploaded.

Here's the functions I used to upload files to Send:

and here's the function I used to test downloads:

I've found one 'gotcha' in this experiment. If a file is uploaded with the Python function above, and you try to download the file in a browser from the url in the upload response, you get an error, but you're still able to download the file using the /assets endpoint. If a file is uploaded through the browser, a fragment identifier is appended to the end of the /download endpoint, e.g https://send.firefox.com/download/2288aeef57/#vsJrzSnYu2ug7JkDZvREUw. This is the decryption key, and I haven't quite figured out how that gets generated, but plan to keep digging through the source until I do. It's also entirely possible that the file isn't getting encrypted on upload (The upload script definitely isn't doing any encryption), but I'm not 100% sure and don't know of a way to verify that. Assuming I figure it out, I'll update this post.

Update: Using the functions above uploads files un-encrypted, which is why trying to download them in the browser fails. The browser expects a decryption key. I was able to locate where Send creates the key and performs the encryption:

I'm still working on performing the same 2 actions in Python. Any input would be appreciated.