-
Notifications
You must be signed in to change notification settings - Fork 32
Scramble Protocol
This describes the protocol and how it ties in to the Scramble REST API.
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.
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
.
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,
We'll refer to the first part (eg vqxtivp5tq643a26
) as ShortHash(PubKey(A))
.
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.
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.
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) |
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.
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.
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 :)
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.
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.)
They can check this themselves, by trying to create an account with that token.
The server never stores plaintext messages, so we can't tell them.
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.
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.