Principles of EmoSend

8 min read Original article ↗

EmoSend

I built EmoSend because my kid wanted a way to move photos from his iPad to a Windows PC.

It had to work in a browser, without an account, and without first uploading the files somewhere.

Those constraints led to three basic principles.

1. It should be usable by anyone

Pairing with emojis

The default pairing method is a sequence of eight emojis. One device shows the sequence. On the other, you tap the same emojis in the same order.

The sending screen showing an eight emoji code, a QR code and the files being sent
The sending device. The eight emojis are the code. The QR carries the same code, plus the key that seals the setup, for a device with a camera.
The receiving screen with three of eight emojis tapped
The receiving device, three emojis in. Tapping the eighth starts the transfer - there is no button to press.

I initially considered using emoji but Apple, Microsoft and Google emojis do not look the same.

EmoSend therefore uses its own set of images. The sequence looks the same on both devices regardless of fonts, operating system or language settings.

  • catcat
  • dogdog
  • frogfrog
  • penguinpenguin
  • fishfish
  • butterflybutterfly
  • appleapple
  • bananabanana
  • pizzapizza
  • cakecake
  • starstar
  • moonmoon
  • treetree
  • rocketrocket
  • carcar
  • heartheart
  • boatboat
  • keykey
  • giftgift
  • bookbook
The twenty emojis.

QR code and link

The same pairing information can also be carried in a QR code or a link.

Scanning the QR code or opening the link avoids entering the emoji sequence manually.

The QR code and the copyable pairing link beneath it
The same code as a QR and as a link.

No app or account is required.

2. The server should not be able to receive the files

The files are transferred over a WebRTC data connection between the two browsers.

The application server is unable to accept file data.

Signaling passes through the cloud server - file data goes directly between browsers
Signaling goes through the server. File data has no line that touches it.

What the server does

The server is used for signaling: enough information for the two browsers to establish a WebRTC connection.

It accepts small JSON messages. HTTP request bodies are limited to 4 KiB. Binary WebSocket messages are rejected, and the service has no object storage for transferred files.

The actual file data is sent over the peer connection.

Setup exchanges small messages through the server.
Setup and transfer are separate. The server takes part in the first and not the second.

On the same network

If both devices are on a normal local network, WebRTC will usually find a local route between them.

In that case a transfer between an iPad and a PC on the same Wi-Fi stays on that network.

iPad to Wi-Fi router to Windows PC, with the server outside the file path
On one Wi-Fi the file goes iPad, router, PC, and never leaves the local network.

Across different networks

If the devices are on different networks, WebRTC uses STUN to discover possible public routes.

With ordinary NAT configurations, it can often establish a direct connection between the browsers over the internet.

The file is still transferred over the encrypted WebRTC connection, not through the EmoSend server.

A phone on cellular reaching a PC behind a home router, directly
Across networks STUN finds a route. The file still goes browser to browser.

When a direct connection cannot be established

Some networks make direct peer-to-peer connections impossible.

Common examples are guest Wi-Fi with client isolation, symmetric or carrier-grade NAT, and networks that block UDP.

If the direct attempt has not succeeded after a few seconds, EmoSend offers a TURN relay while it keeps trying.

The screen offering an encrypted relay after a direct connection could not be made
What the offer looks like once the direct attempt has given up. Nothing is relayed unless this is accepted on both devices.

The relay is only used after both sides agree to it. It forwards the encrypted WebRTC traffic but does not have the keys needed to decrypt the transferred file. That rests on the setup messages arriving unchanged, which the section below is about.

Browser A to TURN relay to Browser B, carrying encrypted WebRTC
The relay forwards ciphertext between the two browsers. It has no key.

A less obvious failure

Firefox on Linux can advertise its local address using mDNS. If the system has no working mDNS responder, Chrome on another machine may be unable to resolve that address.

This can make a local connection fail even when both computers are on the same network.

EmoSend detects this case and suggests a workaround.

The failure screen naming the local lookup problem, with connection details expanded
The real message, with the details panel open. Everything in it is read from the candidates the two browsers exchanged.

Checking the selected path

Once WebRTC connects, both browsers inspect the selected ICE candidate pair.

If the selected route is a relay and the users did not both consent to using one, the transfer is stopped before file data is sent.

The intended behavior: use a direct connection when one can be established, and use a relay only when both sides have explicitly allowed it.

The direct route compared with the relay route
The two routes. Same encryption either way - only the path differs.

Encrypting the setup

The offer and answer pass through the server. They contain the certificate fingerprints the two browsers check each other against. When using the emoji sequence, a malicious server could swap those and trick both devices into talking to it instead of each other. The workaround would require 30 taps instead of 8.

For increased security the QR code or link can be used. It carries a random key in its fragment. The server never sees that key, and the setup messages are encrypted with it.

The sender is told which case it is by the server. A lie there hands the server the sender's messages in the clear.

The files never go through the server, in any of these cases.

The connection details on the last screen, opened, saying the files went straight between the devices and the setup messages were encrypted
The last screen reports which of the two happened.

3. Pairing should be difficult to guess

A pairing room exists for five minutes.

The server keeps only a small amount of state for it: creation time, expiry time, two random 128-bit tokens, and a flag for whether the receiver arrived holding a link key.

If a relay is agreed, it also records that each side consented. That record is what makes "both must agree" something the server enforces. The room's expiry is extended to cover the transfer it enabled. Expired rooms are deleted.

A timeline from room created to expiry at five minutes, with a branch for the relay
Five minutes for the pairing itself, extended only if both sides agreed to a relay.

The emoji sequence

The visible pairing code consists of eight different emojis chosen from a set of twenty.

  • 2cat
  • dog
  • frog
  • 6penguin
  • fish
  • butterfly
  • 7apple
  • banana
  • 8pizza
  • cake
  • 3star
  • 1moon
  • tree
  • rocket
  • car
  • 5heart
  • boat
  • 4key
  • gift
  • book
Eight of the twenty, in order. A real code from a real pairing: moon, cat, star, key, heart, penguin, apple, pizza.

That gives:

20P8 = 5,079,110,400 possible sequences

or a little over 32 bits.

Twenty choices for the first emoji falling to thirteen for the eighth
Each emoji used removes one from the pool, which is where the number comes from.

Join attempts are also rate-limited to ten per minute per IP address.

Why twenty emojis

Removing repeated symbols from a code makes it easier to enter, but it also reduces the number of possible codes.

With sixteen emojis and eight positions, allowing repetition gives:

16⁸ = 4,294,967,296

possible sequences.

If every emoji must be different, the number becomes:

16P8 = 518,918,400

which is about 28.9 bits.

So removing duplicates reduces the search space by roughly a factor of eight. Using twenty emojis brings it back above 32 bits while keeping the no-duplicates property.

Allowing repeats gives four billion sequences. Forbidding them gives five hundred million
Forbidding repeats costs about a factor of eight. Twenty emojis buy it back.

QR codes and share links

The QR code and share link contain the pairing sequence and the key that seals the setup. Both sit in the URL fragment, the part after #.

For example:

https://emosend.com/#b4ig715j-L9J2_qQbwOLa_Nc9Hqryg

URL fragments are handled by the browser and are not included in the HTTP request sent to the server.
Browsers never send the part after #, so the sequence and the key both stay out of URLs and request logs. The server knows the sequence anyway, since it generated it. It never learns the key, and it never sees the files.

It expires with the room.

What else the page loads

Two things reach a third party. Never the files or the pairing code.

The page loads Cloudflare Web Analytics. It counts page views without cookies and without identifying visitors. The server also increments a few aggregate counters - rooms created, whether a join succeeded, whether a relay was used or declined. They tell me whether pairing works for people. Those counters only count things - no IP address, pairing sequence, token or filename is written.

You will see the beacon in DevTools. The counters are written inside the server and make no request the browser can show.

EmoSend itself runs on Cloudflare. The server is a Worker, the STUN server is Cloudflare's, and a relay, when both sides agree to one, is Cloudflare's too. The relay forwards traffic it has no key for.

Verifying it

Most of this can be checked from the browser.

Open DevTools, start a transfer, and look at the Network and WebRTC information.

You should see a small signaling WebSocket connection, the analytics beacon described above, and a WebRTC peer connection carrying the transfer. There should not be an HTTP request containing the file itself.

The largest HTTP request was 454 bytes while 8.25 MB of files moved over WebRTC
Measured during one real transfer of three files. The largest HTTP request carried a pairing code, not a photo.

That is the design - make the common case simple for my kid, and keep the file transfer and pairing easy to inspect.

The receiving device listing three files ready to save
The photos on the other device, ready to save.