Skip to content
This repository has been archived by the owner on May 11, 2022. It is now read-only.

Scramble Protocol

DC edited this page Sep 22, 2013 · 11 revisions

This describes the protocol and how it ties in to the Scramble REST API.

Terms

The "client" and "server" refer to Scramble.io code, written in Javascript and Go, respectively. The "user" is the person using the client.

The client runs in a browser, and is assumed to be trustworthy. (This assumption is reasonable if the user is running the browser extension. It becomes weaker if the user is running the web app. See the overview for a description of the threat model.)

The server is not assumed to be trustworthy. Connections to the server may be spied on, even though they are always over HTTPS. Any information the server stores may be spied on as well. The server may even be commandeered by the adversary. In that case, the server becomes the adversary. Denial of service is possible, but loss of private data should not be possible.

Protocol

We'll create an account for user 'A'. We'll log in as user A, and receive an email from user B. Finally, we'll send an email to user B.

Creating an account

The client generates an PGP key pair using OpenPGP.js, which uses the Javascript secure random number API. We now have PubKey(A) and PrivKey(A) for user A.

We could use any public key algorithm here. Currently, we're using 2048-bit RSA.

The user enters a token ("ironman") and passphrase ("correct horse battery staple").

The client computes a 160-bit hash, which we'll use for authentication. Note that the "||" notation means concatenating two strings.

PassHash  = scrypt(Passphrase, "1" || Token)

We're using scrypt(passphrase, salt, k) to produce 128-bit hashes with the following parameters:

N=16384   // 2^14 iterations, recommended range 2^14 to 2^20
r=8       // recommended value for memory difficulty
p=1       // recommended value for parallelization (CPU difficulty)
dkLen=128 // outputs 128 bits

For information about scrypt and the author's own recommended parameters, see his documentation.

The next step is to derive a symmetric encryption key from the passphrase. This will be used to encrypt user A's private data, including PrivKey(A), for storage on the untrusted server.

We could use any symmetric encryption algorithm here. Currently, we're using AES-128, which requires 128-bit keys.

K = scrypt(Passphrase, "2" || Token)

This must be unrelated to the authentication hash, hence the different salt.
The server will see the token and PassHash, but must never know K.

Now, the client tells the server to create an account.

POST /users/

Client to server

key value
token Token
passphrase hash PassHash
public key PubKey(A)
encrypted private key AES128K(PrivKey(A))

If the token is already taken, then the account creation request will fail, and the user will try again until they find one that's not taken.

The server has now set up a mail box for user A. The corresponding email address is a hash of PubKey(A). We use the same format as Onion URLs: the first 80 bits of SHA1(PubKey(A)), encoded in Base32 (RFC4648). For example,

vqxtivp5tq643a26@scramble.io

We'll refer to the first part (eg vqxtivp5tq643a26) as ShortHash(PubKey(A)).

Logging in

To log in, the user enters a token and passphrase.

The client stores both for the current browser session only. Specifically, Token and PassHash are session cookies. The symmetric key K is stored in HTML5 sessionStorage—it is never sent to the server, and never stored in localStorage or cookies.

The server is stateless, and does not track sessions. There are no session tokens. Instead, the client sends Token and PassHash with every request.

Loading your inbox

The first communication between client and server after the user enters their credential follows:

GET /box

Client to server

key value
token Token
passphrase hash PassHash
box "inbox"

The server returns a list of email headers. Each one consists of metadata and an encrypted subject.

Server to client

key value
message id 160-bit UUID
time Unix time, no time zone
from email address
to,cc,bcc one or more email addresses
encrypted subject {SignedSubject}PK(A)

Loading sent mail, archived mail, and so on works the same way, but with different values for "box" in the request.

Loading an email

The inbox request only gives metadata and encrypted subject lines for each message.

GET /email/{message id}

Client to server

key value
token Token
password hash PassHash
message id 160-bit UUID, hex encoded

Server to client

key value
encrypted body OpenPGP ASCII armored block, contains {SignedBody}PK(A)

Sending an encrypted email

The user composes an email.

The first step to sending the email is looking up the public keys of the recipients. Here, we'll assume the email has one recipient, address B. If there are multiple recipients, or a CC or BCC, the steps are simply repeated: we have to look up the public key of each recipient, and we encrypt and send our message separately for each recipient.

Remember that address B takes the form NameB@HostB, where

NameB = ShortHash(PubKey(B))

So we look up B's public key. No authentication required.

POST /publickeys

Client to server

key value
addresses B

If there were multiple recipients, the key for every recipient would be fetched in one request.

Server to client

key value
public key B PubKey(B)

The client verifies that the server provided the correct public key. It computes ShortHash of the response, which should match NameB.

Next, the client encrypts and signs both the message and the subject. This follows the OpenPGP standard. The message is signed, symmetrically encrypted, and the symmetric encryption key is RSA-encrypted for each recipient. The result is one OpenPGP ASCII-armored block for the subject and another (potentially much longer) one for the body.

POST /email/{message id}

Client to server

key value
token Token
passphrase hash Passhash
message id 160-bit UUID, made with Javascript SRNG, hex encoded
to,cc,bcc one or more email addresses
encrypted subject OpenPGP-encrypted ASCII armored block
encrypted body OpenPGP-encrypted ASCII armored block

Since the message id is long and comes from a secure RNG, there should never be accidental collisions. If someone edits the client and tries to send new message with the same message id as a previous one, the new one is simply ignored.

Federation

You might notice that the client always talks to its own server to look up public keys.

What happens when NameA@HostA wants to send an email to NameB@HostB, where both are Scramble addresses, but HostA does not have the public key for NameB? In this case, HostA forwards the request to

https://HostB/publickeys

It forwards the response back to the client. Both hosts are untrusted.

This lets anyone with a webserver run their own Scramble server. Just clone the Github repo, follow the Quick Start, and you can have your own {ShortHash}@mydomain.com address.

Authentication

The authentication is rudimentary. Sending TokenHash and PassHash with every request is similar to HTTP Basic authentication.

The cookies are created with secure=true, so will only ever be transmitted over HTTPS.

Note that we don't need authentication at all for message secrecy. The mechanism is just to avoid leaking info to fellow users, and to prevent an adversary from eg. downloading all the encrypted private keys, and attempting to crack the passphrases used to encrypt them. The authentication provides no additional protection against government adversaries, and other groups with the means to obtain the server database.

We could just allow any client to download any inbox—all messages are encrypted with the recipient's public key. That gives any user the same amount of information a central adversary would have. Unfortunately, that does reveal whether a particular address received a message or not, and other high-level metadata. For a central adversary, this seems unavoidable. The recourse is to keep the addresses anonymous. For ordinary adversaries, we can avoid leaking any info at all except the fact that a given token is taken, via authentication.

To clarify: even if you can steal someone's TokenHash and PassHash, you still cannot:

  • Read their email. You can download it, but you don't have the private key to decrypt it.
  • Read their inbox (subject lines and metadata) or address book.
  • Send encrypted email as them. You don't have the private key to sign it, so the recipient will reject it.

You can send unencrypted email impersonating them, but you can already do that just with sendmail :)

Key storage

The way users are stored on the server mirrors the protocol for creating accounts, described above.

token hash              SHA1(token)
passphrase hash         SHA1("1" || passphrase)
public key              PubKey(A)
public key hash         ShortHash(PubKey(A))
encrypted private key   AES128<sub>K</sub>(PrivKey(A))

There's an extra column, storing the hash of the user's public key, which corresponds to their email address. The table is indexed to allow fast lookup by token hash or public key hash.

Scenarios

Someone demands the user name of all users

We can't tell them. We only know the SHA1 hash of each token. (They can demand the list and try to crack the hashes. We warn users to pick tokens that don't identify them.)

Someone demands to know whether a particular user name exists

They can check this themselves, by trying to create an account with that token.

Someone demands the inbox for a particular address

The server never stores plaintext messages, so we can't tell them.

Someone wiretaps connections to the server, and connections to other SMTP servers

This would mean the adversary has either done an SSL MITM attack, or compelled us to allow a secret wiretap.

The server transmits plaintext messages only from and to non-Scramble addresses. Communications between Scramble addresses would remain safe.

The server never sees plaintext address books. Users who keep their Scramble address anonymous would not be revealed even with a full wiretap, even if they are listed in their contacts' address books by their real name.

Someone takes control of the server

This allows them to serve malicious Javascript.

This won't affect users who install the Chrome extension, once that's available. To them, the server is fully untrusted, and this scenario is the same as the wiretap scenario. The adversary can deny service, to everyone or to particular addresses, but they can't read encrypted mail or address books.

An adversary who controls the server could serve malicious Javascript to all webmail users, or specific ones by IP.

Serving malicious Javascript to all users should be caught very quickly. In the future, there might be a small program for volunteers all over the world to run, which would download the resources (HTML, JS, etc) at random intervals to check that they haven't been tampered with. When we get there, modifications to the resources or even their HTTP cache settings should be caught quickly.

As a result, an adversary that controls the server probably won't want to serve malicious Javascript to all users.

Serving malicious code to a single address (not by IP) should fortunately not be possible.

When a user visits https://scramble.io, the site loads all resources immediately (index.html, app.js, and style.css). Then, it runs as a single-page web application. All further communication is via XHRs according to the protocol described above. By the time the server knows that a particular user is logging in, that browser session is already treating the server as untrusted.

Even if the user refreshes the page, the cache settings on resources mean that the browser won't request them again.

This means that a user logging in over Tor—or, say, from a public library computer—should be safe, even if they're using web mail and an adversary controls the server.