Services/Sync/KeyRecovery
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?
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. Details follow.
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 HMAC256:
IV = get_random_bytes(32) ciphertext = AES-256-ENCRYPT(enc_key, IV, sync_key) hmac = HMAC256(enc_key, ciphertext)
The details necessary to decrypt the sync key are 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", }
To recover the sync key, the client retrieves the above JSON from the recovery service and does:
enc_key = PBKDF2(username + password, recov["salt"], 4096, 32) if HMAC256(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://server/path/authentication/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://server/path/recovery/username => 401 Unauthorized
GET https://server/path/recovery/username X-Auth-Token: <username>:<timestamp>:<hmac> => 200 OK Content-Type: text/json { ...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.
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.
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, such a move is largely orthogonal to the development of the key recovery service itself.