Services/Sync/KeyRecovery: Difference between revisions

From MozillaWiki
< Services‎ | Sync
Jump to navigation Jump to search
Line 151: Line 151:


E.g. take a hash of the user's password as the "user auth key" and upload that along with the recovery data.  When going to retrieve the data, the client generates its own auth token and signs it with the previously-uploaded key.
E.g. take a hash of the user's password as the "user auth key" and upload that along with the recovery data.  When going to retrieve the data, the client generates its own auth token and signs it with the previously-uploaded key.
(This is quite similar to https://bugzilla.mozilla.org/show_bug.cgi?id=638905 except the "proof of identity" is derived from the password, not the sync key)


The advantage is that this simplifies the workflow for the client, and removes one component which could provide an additional attack surface.
The advantage is that this simplifies the workflow for the client, and removes one component which could provide an additional attack surface.

Revision as of 23:38, 22 September 2011

Draft-template-image.png THIS PAGE IS A WORKING DRAFT Pencil-emoji U270F-gray.png
The page may be difficult to navigate, and some information on its subject might be incomplete and/or evolving rapidly.
If you have any questions or ideas, please add them as a new topic on the discussion page.

Goal

To securely allow a user to recover their sync key, using only the username and password for their Mozilla Services account.

Overview

Currently the sync key is never stored on Mozilla servers in any form; it only exists locally on each device connected to the sync account, plus in any backups explicitly made by users. This provides some additional security for their sync data above that provided by their account password.

However, if the user accidentally deletes or loses their sync key, they permanently lose access to the sync data stored on Mozilla servers. Currently their only option is to discard all sync data and start again with a new key.

If the user *opts in* to the key recovery service then their sync key will be encrypted and stored on Mozilla servers, where it can be recovered using their account username and password. They may also use this service to obtain the sync key when setting up a new device, rather than using the current J-PAKE scheme to transfer it from an existing device.

The client will use the user's account username and password to encrypt the sync key prior to transmission to the service. Barring our deliberate snooping or cracking of the user's password, this means that the sync key cannot be read by Mozilla.

If the user forgets or resets their password then the stored sync key will be unreadable and must be re-stored from a connected device. This is a feature - even if an attacker compromises their email and resets their password to gain control of their account, the attacker will not gain access their existing sync data.

Since this scheme reduces the security of all the user's sync data to the security of their account password, it will be a completely opt-in service and will be disabled by default.

The encrypted sync key represents a particularly high-value target for an attacker, because:

  • it potentially allows access to *all* of the user's sync data, and
  • it will be encrypted using a relatively low-entropy key (the user's account password)

We therefore entrust its storage to a separate service from the main sync-storage service, so that it can be run from a high-security server.

Details

Naming

In initial discussions we've been calling this a "key escrow service", but to me (rfkelly) that conjures up too many big-brother clipper-chip-style associations. Since the idea is that Mozilla won't be able to obtain your sync key even if you enable this service, I think "key recovery service" has more accurate connotations. Thoughts?

Server API

Since the server component is intended to run from a high-security restricted-access environment, it should be as simple and light-weight as possible. Hence, we provide the smallest and simplest API the could possibly work: you get a single blob of plain text data, keyed by your username, limited to 1 KB.

   GET https://recov-server/username
   => 404 Not Found
   PUT https://recov-server/username
   Content-Length: 11
   hello world
   => 201 No Content
   GET https://recov-server/username
   => 200 OK
      Content-Length: 11
      Content-Type: text/plain
      hello world
   PUT https://recov-server/username
   Content-Length: 2048
   <mwuahahaha I store my warez on you>
   => 413 Request Entity Too Large
   DELETE https://recov-server/username
   => 201 No Content
   GET https://recov-server/username
   => 404 Not Found

It's tempting to expand this API into something more generic, e.g. to provide multiple different recovery keys for each username. But the less this service has to do, the less chance there is for something to go wrong.

If we can successfully bootstrap from the user's password into a strong crypto key, then anything else they might need to keep safe can be stored in standard sync storage with strong encryption.


Sync Key Encryption

Before uploading to the service, the client encrypts the sync key using its existing standard encryption routines. The encryption key is derived from the username and password using PBKDF2. The details that follow are just to explain the process - in the client code this should be a thin layer on top of existing methods such as Utils.deriveKeyFromPassphrase and CryptoWrapper.encrypt.

To encrypt the sync key for storage in the recovery service, the client uses PBKDF2 to derive an appropriate encryption key from the user's account username and password:

   salt = get_random_bytes(16)
   enc_key = PBKDF2(username + password, salt, 4096, 32)

This is then used to encrypt the sync key via AES-256, with a random IV and HMAC-SHA256:

   IV = get_random_bytes(32)
   ciphertext = AES-256-ENCRYPT(enc_key, IV, sync_key)
   hmac = HMAC-SHA256(enc_key, ciphertext)

The information necessary to decrypt the sync key is serialized into a JSON structure, which is sent to the key recovery service for storage:

   recov = { 
     //  Parameters for key derivation, as used by deriveKeyFromPassphrase
     "salt":  "b64-encoded salt",
    
     //  Encrypted payload, same format as CryptoWrapper WBO output
     "ciphertext": "b64-encoded ciphertext",
     "IV": "b64-encoded IV",
     "hmac": "hex-encoded hmac",
   }
   HTTP.PUT(recovery_url, JSON.stringify(recov))

To recover the sync key, the client retrieves the above JSON from the recovery service and does:

   recov = JSON.parse(HTTP.GET(recovery_url))
   enc_key = PBKDF2(username + password, recov["salt"], 4096, 32)
   if HMAC-SHA256(enc_key, recov["ciphertext"]) != recov["hmac"]:
       ABORT!
   sync_key = AES-256-DECRYPT(enc_key, recov["IV"], recov["ciphertext"])


Open questions:

  • is there something better than PBKDF2 for this purpose?
  • should be include the number of iterations in the stored PBKDF2 parameters? Someday we might want to increase it.
  • should we mix the HMAC_INPUT string into the PBKDF2 inputs?

Authentication

Anyone who can access the stored recovery data for a user can run a dictionary or brute-force attack against their password. So, we should only allow retrieval of the recovery data when authenticated as the user.

However, since the server component is intended to run from a high-security restricted-access environment, it should be as simple and light-weight as possible. It will therefore offload responsibility for authentication to a separate service so that it doesn't have to handle passwords.

To access the recovery service, the user must provide an "auth token" to prove their credentials. I see two possibilities: server-generated tokens and user-generated tokens.

Server-Generated Tokens

The recovery service shares a private key with a separate "authentication service" which generates signed auth tokens for the user. The user provides their account credentials to the authentication services and obtains a signed auth token in return:

   GET https://auth-server/token
   Authorization:  Basic XXXYYYZZZ
   => 200 OK
      <username>:<timestamp>:<hmac>

(Obviously it would be better to use Digest-Auth or SRP or something that doesn't reveal the password to the server; this is just an example.)

Here the token contains a username to identify the user, a timestamp to allow tokens to be expired, and a HMAC signature using the shared private key.

The user then presents this token to the recovery service and can retrieve, update or delete the stored recovery data:

   GET https://recov-server/username
   => 401 Unauthorized
   GET https://recov-server/username
   X-Auth-Token:  <username>:<timestamp>:<hmac>
   => 200 OK
      Content-Type: text/plain
      { ...recovery data here... }

The recovery service checks the provided auth token to make sure the signature is good, and that it "new enough" according to its own local time. It can thus authenticate users quickly and simply without needing to consult any external services during the request.

Open Questions:

  • is there a standard for signed auth tokens like this?

User-Generated Tokens

Instead of relying on another server and a single shared signing key, each user could upload their own individual signing key when they store the recovery data.

E.g. take a hash of the user's password as the "user auth key" and upload that along with the recovery data. When going to retrieve the data, the client generates its own auth token and signs it with the previously-uploaded key.

(This is quite similar to https://bugzilla.mozilla.org/show_bug.cgi?id=638905 except the "proof of identity" is derived from the password, not the sync key)

The advantage is that this simplifies the workflow for the client, and removes one component which could provide an additional attack surface.

The disadvantage is that it can't be used to authenticate the initial upload of the recovery data, so there's a bootstrapping problem.

Wider Implications

Since this service effectively reduces the security of the user's sync data to the security of their account password, we need to consider the wider implications for password management across all Services products.

Account management and authorization in Services currently uses HTTP-Basic-Auth, and hence transmits the password to Mozilla in the clear. Thus, users of the recovery service are trivially vulnerable to us snooping on them, or to anyone who manages to compromise any of our servers. That's bad.

Ideally, we would move to a system that can provide authentication without the server learning the user's password. HTTP-Digest-Auth at a minimum. Something like the Secure Remote Password Protocol would be even better, but there's no current standard for integrating this into the HTTP-Auth workflow.

In any case, since such a move will have to happen across the whole services infrastructure to be worthwhile, it's largely orthogonal to the development of the key recovery service itself.

Keeping Things in Sync

The client will need to have some protocol for updating the stored recovery information when the user changes their password, or for detecting when the stored info is stale due to a password reset. Will this fit into the existing "uh-oh your password seems to have changed" workflow on the client?

We could *try* to manage some of that automatically on the server but that sounds like trouble. Perhaps we need the ability for the account-management service to forcibly delete stored data when it knows a user's password ha been changed.