Identity/AttachedServices/KeyServerProtocol

< Identity‎ | AttachedServices
Revision as of 23:53, 8 August 2013 by Warner (talk | contribs) (→‎SRP Notes: reminder: A and B must not be zero)

PiCL Key Server / IdP Protocol

NOTE: This specification is slowly converging on stability (31-Jul-2013). Several pieces are not yet complete. If you write any code based on this design, keep a close eye on this page and/or contact me (warner) on the #picl IRC channel to learn about changes. Eventually this will be nailed down and should serve as a stable spec for the PICL keyserver/IdP protocol.

The server is being developed in https://github.com/mozilla/picl-idp . This repo currently includes a demonstration client (node.js CLI).

Note that all messages are delivered over an HTTPS connection. The client browser may also implement cert-pinning to improve on the certificate validation process. The protections described below are in addition to those provided by TLS.

Remaining TODO items:

  • decide on client-side key-stretching parameters: http://keywrapping.appspot.com can help
  • finalize SRP questions (definition of M1, generation of a/b)
  • finalize proof-of-work/DoS-prevention details
  • decide how to rate-limit account-creation calls
  • confirm this is actually implementable inside Firefox (especially w.r.t. NSS and Android/Java crypto)

Creating The Account

The first act performed by a user is to create the account. They enter email+password into their browser, which then does the following steps:

  • decide upon stretching parameters (perhaps consulting the keyserver for recommendations, but imposing a minimum strength requirement). For now we used a fixed {firstPBKDF:20000, scrypt: {N:65536, r:8, p:1}, secondPBKDF:20000}.
  • randomly choose a 32-byte mainSalt (this should be unique, but is not secret)
  • choose the SRP group parameters (fixed: use the 2048-bit group described below)
  • perform key-stretching (as described below), derive stretchedPW and srpPW
  • randomly choose a 32-byte srpSalt (unique, but not secret)
  • create srpVerifier from srpPW and srpSalt (as described below)
  • deliver (email, stretchParams, mainSalt, srpParams, srpSalt) to the keyserver's "POST /account/create" API


To limit abuse, the createAccount() should also require a fresh "createToken". This should be created by some other API, outside the scope of this document, that perhaps requires a CAPTCHA or something. createAccount() might also require a proof-of-work token, as described below.

The server, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit (32-byte) strings. It stores these, along with all the remaining values, indexed by email, in the account table where they can be retrieved by getToken later.

Email Verification

To prevent fixation attacks, we require new accounts to verify their configured recovery email address before letting them learn the generated keys or obtain a signed certificate. Nevertheless, we wish clients to forget the user's password while they wait for email verification to complete. To achieve this, clients can obtain a sessionToken before verification, but most APIs that require it will raise errors until verification is finished.

The server will send email with a URL that contains a long random "verification code" in the "fragment" hash. This URL points to a static page with some javascript that submits the code to the "POST /account/recovery_methods/verify_code" API. The URL can be clicked by any browser (it is not bound to anything), and when the API is hit, the account is marked as verified.

After the client submits /account/create, it performs the "/session/auth" login sequence below to obtain a sessionToken. It then polls the "GET /account/recovery_methods" (which requires a sessionToken but not account verification) until the user clicks the email link and the API reports verification is complete. Then the client uses "GET /account/keys" and "POST /certificate/sign", described below, to obtain kA, kB, and a signed certificate to talk to the storage server.

Login: Obtaining the authToken

To connect a browser to an existing account, we use the following login protocol to transform an email+password pair into a single-use authToken. We will use this in the next section to create a session, from which we can obtain encryption keys and signed certificates.

This protocol starts by using key-stretching to transform the email+password into a "stretchedPW", then feeds this into an SRP protocol to get the authToken.

 

This authToken can be used (once) to do one of the following:

  • /session/create: obtain a sessionToken (and keyFetchToken), which enables storage server access
  • /password/change: obtain an accountResetToken, to safely reset the account password
  • /account/delete: to delete the entire account

The protocol is designed to enable parallelism between key-stretching and the initial network messages, to reduce the time it takes to connect a browser to the account. In total, the browser requires five messages in four roundtrips (1: /auth/start, 2: /auth/finish, 3: /session/create, 4: /account/keys and /certificate/sign in parallel) before it is ready to talk to the storage server.

/auth/start

As soon as the user finishes typing in the email address, the client should send it in the "/auth/start" message to the keyserver. The response will include a set of parameters that are needed for key-stretching (described below), and the common parameters used by both sides of the SRP protocol to follow. These are simply looked up in a database entry for the client, along with an account-id. It must also include an allocated loginSRPToken that is used to associate this request with the subsequent /auth/finish request. Finally, the response also includes the server's contribution to the SRP protocol ("srpB"), which is calculated on the server based upon a random value that it remembers in the session.

To mitigate DoS abuse, /auth/start may also require a proof-of-work string, described below.

Client-Side Key Stretching

"Key Stretching" is the practice of running a password through a computationally-expensive one-way function before using it for encryption or authentication. The goal is to make brute-force dictionary attacks more expensive, by raising the cost of testing each guess.

To protect the user's class-B data against compromises of our keyserver, we perform this key stretching on the client, rather than on the server. We use the memory-hard "scrypt" function (pronounced "ess-crypt") for this purpose, as motivated by the attacker-cost studies in Identity/CryptoIdeas/01-PBKDF-scrypt. Slower (low-RAM) devices can still participate by outsourcing the scrypt step to an "scrypt helper". To provide at least minimal protection against a compromise of this helper, we sandwich the scrypt step between two PBKDF2 steps. The complete protocol looks like this:

 

Our initial parameter choices for this stretching are to use 20000 iterations of PBKDF2 on each side (40k in all), and to run scrypt with N/r/p set to 64k/8/1. This requires roughly 60MB of RAM for about 1.3 seconds on a 1.8GHz Intel Atom linux box (it will run faster, but use the same memory, on more modern CPUs). We are still studying performance on various platforms to decide what parameters to use in V1: lowering them speeds up the login process, but reduces security (by reducing the cost of an dictionary attack).

Since the stretching is expected to take a second or two, the client can optimistically start this process (using default parameters) before receiving the auth/start response, and then check that it used the right parameters afterwards (repeating the operation if not). (We'll want to build the stretching function with periodic checkpoints so that we don't have to lose all progress if the parameters turn out to be wrong). The "mainSalt" is added *after* the stretching, to enable this parallelism (at a tiny cost in security).

After "stretchedPW" is derived, a second HKDF call is used to derive "srpPW" and "unwrapBKey" which will be used later.

 

SRP Protocol Details

The PiCL client uses the SRP protocol (http://srp.stanford.edu/) to prove that it knows the (stretched) account password without revealing the actual password (or information enabling a brute-force attack) to the server or any eavesdroppers.

SRP is somewhat underspecified. We use SRP-6a, with SHA256 as the hash, and the 2048-bit modulus defined in RFC 5053 Appendix A. We consistently zero-pad all string values to 256 bytes (2048 bits), and use H(A+B+S) as the key-confirmation message "M1". These details, plus the SRP design papers and RFCs 2945 and 5054, should be enough to build a compatible implementation. The diagrams below and the test vectors at the end of this page can be used to verify compatibility.

The server should use Jed's SRP module from https://github.com/jedp/node-srp . The client might use SJCL (http://crypto.stanford.edu/sjcl/) or native code (NSS).

The basic idea is that we're using the main-KDF output "srpPW" as a password for the SRP calculation. We use the email address for "identity", and a server-provided string for "salt". (We could safely leave them blank, since equivalent values are already folded into the password-stretching process, but it's less confusing to follow the SRP spec and fill them in with something sensible).

Note that SRP-6a uses a "k" value which basically encodes the group being used ("N" and "g"). Since all PICL accounts use the same 2048-bit group, they will all use the same "k" value (not to be confused with the per-session shared-secret "K" key that emerges from the protocol). This group's "k" integer is (as a base-10 number): 2590038599070950300691544216303772122846747035652616593381637186118123578112

SRP Verifier Calculation

When the client first creates the account, it must combine the account email address, the stretched password (srpPW), and a randomly-generated srpSalt, to compute the srpVerifier. The server will use this verifier later, to check whether or not the client really knows the password.

If the server is compromised and an attacker learns the srpVerifier for a given account, they cannot use this to directly log in (the verifier is not "password-equivalent"), but it does allow them to perform an offline brute-force attack against the user's password. In this respect, it is similar to a traditional hashed password. We make these attacks somewhat more expensive by performing the client-side stretching described above, instead of using the raw user password in the SRP calculation.

 

Test vectors are provided at the end of this page. The protocol requires that several values ("a", "b", "srpSalt") are chosen randomly, but for illustrative purposes, the test vectors use carefully-crafted non-random values. For the user's sake, please ensure implementations use proper random values instead of simply copying the test vectors.

The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.

SRP Server-side Sign-In Flow

When the user connects a new device to their account, they use the /auth/start API to start the SRP protocol. This sends the account email address to the server. The server looks up the stored srpVerifier for this account, creates a random 'b' integer, performs some math to compute the "B" number, then converts B into a string known as "srpB". "srpB" is returned to the client, along with srpSalt and the key-stretching parameters. "b" and "srpB" are retained for the subsequent /auth/finish call. Note that it is critical that the "b" integer remain secret on the server. The server also allocates a random loginSRPToken to connect the two calls.

 

Later, /auth/start will be called with the client's srpA string and its M1 key-confirmation string. srpA is combined with srpB to calculate the "u" integer. srpA is also turned into an integer and used to compute the shared-secret "S" integer. S is then used to compute the shared key "srpK", which is the output of the SRP process.

SRP Client Calculation

While the client is waiting for the response to /auth/start, it begins its key-stretching calculations. Everything else must wait until the response to /auth/start arrives, which includes the key-stretching parameters (which are retroactively confirmed), srpSalt, and the server's generated srpB value.

 

Once the client knows srpSalt, it computes the same "x" integer as it did in the middle of the srpVerifier calculation. It also converts srpB into an integer named "B". Then it creates a random "a" integer, uses it to compute the string "srpA", then combines srpA with the server's srpB to compute the "u" integer. It then combines the static "k", the password-derived "x", the combined "u", and the server's "B", together with some magic math, to derive the "S" integer. If everything went well, the client will compute the same "S" value as the server did. If not (the password was wrong, or the client is talking to a fake server that doesn't really know srpVerifier), then the two "S" values will not match.

(Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.)

To safely tell if the "S" values match, both client and server combine srpA, srpB, and their (independently) generated "S" strings to form a string named "M1". The client sends M1 (along with srpA) in the /session/auth/finish message. The server compares the client's copy of M1 against its own. If they match, the client knew the password and the server can safely respond with the encrypted account data. If they do not match, the client (or a man-in-the-middle attacker) did not know the password, and the client should increment a counter that can trigger defenses against online guessing attacks. The server must then return an error to the client, and not use or reveal srpK (or the correct M1) in any way.

Both client and server also hash "S" into "srpK". This is the shared session key, from which specific message encryption and MAC keys are derived (as described below).

SRP Notes

The SRP "g" (generator) and "N" (prime modulus) should use the 2048-bit value from RFC 5054 Appendix A. Clients should not accept arbitrary g/N values (to protect against small primes, non-primes, and non-generators). In the future we might allow alternate parameter sets, in which case the server's first response should indicate which parameter set to use.

There are several places in SRP where integers (usually in the range 1..N-1) are converted into bytestrings, either for transmission over a wire, or to be passed into a hash function. The SRP spec is somewhat ambiguous about padding here: if the integer happens to be less than 2^2040, the simplest toString() approach will yield a 255 byte string, not a 256 byte string. PiCL consistently uses padding, so compatible implementations must prepend one or more NUL bytes to these short strings before transmission or hashing. The test vectors below were brute-forced to ensure that "srpVerifier", "srpA", "srpB", and "S" all wind up with leading zeros, to exercise the padding code in compatible implementations. If you are having problems getting your code to match these results, add some assertions to test that the stringified integers being put into hashes are exactly 256 bytes long.

The client does its entire SRP calculation in a single step, after receiving the server's "B" value. It creates its "A" value, computes the shared secret S, and the proof-of-knowledge M1. It sends both "A" and "M1" in the same message (/auth/finish).

The server receives "A" in /auth/finish, computes the shared secret "S", computes M1, checks that the client's M1 is correct, then derives the shared session key K. It then allocates a token (of the requested type) and encrypts keyFetchToken+sessionToken as described below, returning the encrypted/MACed bundle in the response to /auth/finish.

On the server, it is critical to reject an "A" value that is 0, or some other multiple of N. If the server does not check this, anybody can trivially sign in to any account without knowing the password. Likewise, it is critical for the client to reject a "B" value where B%N==0. If the client does not check this, the server (or an attacker pretending to be the server) will get a value that can be used in an offline brute-force search for the user's password.

Outstanding crypto questions:

  • How exactly should the "a" and "b" integers be generated? The issue is of how much bias SRP can tolerate. Ideally these integers are uniformly distributed from 1 to N-1 (inclusive). The only way to obtain a purely uniform distribution from a source of random bytes is try-try-again: pick a (integral-number-of-bytes) number, compare it to the desired range, try again if it falls outside the range. If you get unlucky, this can take a lot of guesses, depending upon how close the range is to a power of two. ECDSA implementations tend to pick a number twice as long as the modulus and then modulo it down (rand(2^4096) % N), which yields a tiny fraction of a bit of bias. The cheaper approach is to do the same with a number of equal length (rand(2^2048) % N), which imposes more bias. Some SRP implementations appear to be satisfied by rand(2^256). We need more review here.
  • The original SRP papers defined M1=H(A+B+S) as we use here, but other implementation (in particular RFC2945) uses a curious construct that includes the username, the salt, and an odd XOR combination of N and g. We need to decide what to use for M1.

/auth/finish

The client-side SRP calculation results in two values that are sent to the server in the /auth/finish message: "srpA" and "srpM1". "A" is the client's contribution to the SRP protocol. "M1" is an output of this protocol, and proves (to the server) that this client knew the right password.

The server feeds "A" into its own SRP calculation and derives (hopefully) the same "S" value as the client did. It can then compute its own copy of M1 and see if it matches. If not, the client (or a man-in-the-middle) did not get the right password, and the server will return an error and increment it's "somebody is trying to guess passwords" counter (which will be used to trigger defenses against online guessing attacks). If it does match, then both sides can derive the same "K" session key.

The server then allocates a single-use 32-byte random token named "authToken". It encrypts the token with the session key, and returns a success message with the encrypted bundle.

All tokens have an associated tokenID, described below. The server needs to maintain a table that maps the tokenID to the token itself, so it can derive other values from the token later. The tokens are also associated with a specific account, so later API requests do not specify an email address or account ID.

Decrypting the /auth/finish Response

The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.

 

The respXORkey is used to encrypt the authToken string, by simply XORing the two. This ciphertext is then protected by a MAC, using HMAC-SHA256, keyed by respHMACkey. The MAC is appended to the ciphertext, and the whole bundle is returned to the client.

The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then returns the authToken value.

Creating a Session

For login, the single-use authToken is spent on a call to /session/create . This allocates two new (random 32-byte) tokens: a long-lived "sessionToken", and a single-use "keyFetchToken". The /session/create call returns an encrypted bundle containing the two tokens.

 

For calls which accept an authToken, the client uses authToken to derive three values:

  • tokenID
  • reqHMACkey
  • requestKey


The client uses tokenID and reqHMACkey for a HAWK (https://github.com/hueniverse/hawk/) request to the "POST /session/create" API, using tokenID as "credentials.id" and reqHMACkey as "credentials.key". The server uses tokenID to look up the corresponding token, then derives reqHMACkey to validate the request.

Each authToken-using call then derives additional API-specific values from requestKey. /session/create uses two derived values:

  • respHMACkey
  • respXORkey


When the server receives a valid /session/create request, it allocates sessionToken and keyFetchToken, concatenates them, encrypts the pair by XORing it with the derived respXORkey, and attaches a MAC generated with respHMACkey. The encrypted MACed bundle is returned to the client.

The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then splits it into the separate keyFetchToken and sessionToken values.

Each authToken is single-use: once a successful request has been made with it, the authToken and its corresponding ID is removed from the server's memory, and subsequent attempts to use it will return a "no such token" error. The token is consumed even if the request fails (e.g. the MAC did not match).

The authToken can be used (once) by multiple APIs. The server only needs to maintain one table mapping tokenID to authToken, because all these APIs share the same method of deriving a tokenID and reqHMACkey from the authToken.

When a HAWK request appears, it should look up the tokenID in this table, retrieve the authToken, recompute reqHMACkey and requestKey, validate the request, delete the table entry, then hand the HTTP request and requestKey to the specific API handler. That handler should derive the API-specific values (respHMACkey, respXORkey, etc) to process the request or construct the response.

The server can support multiple sessions per account (typically one per client device, plus perhaps others for account-management portals). There can also be multiple outstanding keyFetchTokens. The sessionToken lasts forever (until revoked by a password change or explicit revocation command), and can be used an unlimited number of times. The keyFetchToken expires after 60 seconds, and is single-use.

Obtaining keys kA and kB

Clients which have exchanged an authToken for either a sessionToken or an accountResetToken will also receive a keyFetchToken. This single-use token allows the client to retrieve kA and wrap(kB), which enables the client to encrypt and decrypt browser data (bookmarks, open-tabs, etc) correctly. As above, the keyFetchToken is used to derive tokenID, reqHMACkey, respHMACkey, and respXORkey, which are used in a HAWK request to the "GET /account/keys" API.

The server pulls kA and wrap(kB) from the account table, concatenates them, encrypts the pair by XORing it with the derived respXORkey, and attaches a MAC generated with respHMACkey.

 

The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then splits it into the separate kA and wrap(kB) values.

 

Finally, the server-provided wrap(kB) value is simply XORed with the password-derived unwrapBKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).

 

"kA" and "kB" enable the browser to encrypt/decrypt synchronized data records. They will be used to derive separate encryption and HMAC keys for each data collection (bookmarks, form-fill data, saved-password, open-tabs, etc). This will allow the user to share some data, but not everything, with a third party. The client may intentionally forget kA and kB (only retaining the derived keys) to reduce the power available to someone who steals their device.

Note that /account/keys will not succeed until the account's email address has been verified. Also note that each keyFetchToken is single-use and short-lived. The token is consumed even if the request fails (e.g. the MAC does not match).

Signing Certificates

The sessionToken is used to derive two values:

  • tokenID
  • request HMAC key


 

The requestHMACkey is used in a HAWK request to provide integrity over many APIs, including /certificate/sign. requestHMACkey is used as credentials.key, while tokenID is used as credentials.id . HAWK includes the URL and the HTTP method ("POST") in the HMAC-protected data, and will optionally include the HTTP request body (payload) if requested.

For /certificate/sign, it is critical to enable payload verification by setting options.payload=true (on both client and server). Otherwise a man-in-the-middle could submit their own public key, get it signed, and then delete the user's data on the storage servers.

The following keyserver APIs require a HAWK-protected request that uses the sessionToken. In addition, some require that the account be in the "verified" state:

  • GET /account/devices
  • POST /session/destroy
  • GET /recovery_email/status
  • POST /recovery_email/resend_code
  • POST /certificate/sign (requires "verified" account)

Resetting The Account

The account may be reset in two circumstances: when the user changes their password, and when the user forgets their password. In both cases, the client first obtains an "accountResetToken". This token is then used to change the SRP Verifier and either reset or replace the wrap(kB) value.

Changing the Password

When the user wishes to change their password (i.e. they still know the old password), they first use the /auth/start+/auth/finish SRP protocol safely obtain an "authToken". They they use the "/password/change/start" API to exchange the authToken for an accountResetToken and a keyFetchToken.

 

The accountResetToken will be used below to set the new password safely. The keyFetchToken should be used first, to obtain kB, so the subsequent account reset can replace wrap(kB) with a new value. This allows the password-changing client to retain their class-B data.

Requiring an authToken proves that the user has provided the correct account password recently. When the account is reset, all active sessions and tokens will be cancelled (disconnecting all devices from the account). The client should immediately establish a new session as described above.

This API is only used when the user knows their old password: if they have forgotten the password, use the "/password/forgot" APIs below.

Handling a Forgotten Password

When the user has forgotten their password, they can use one of their "recovery methods" to obtain an accountResetToken. For now, this means we send a random code to the email address associated with their account. The user must copy this code from the email into their client, whereupon the client will get an accountResetToken that can be delivered to the API below.

Note that, since the forgotten-password client never learns kB, any class-B data will be lost. This is necessary to protect class-B data from attackers who can read the users's email but do not know the account password (including those who compromise the IdP and the keyserver itself). When using /account/reset below, they will set wrap(kB) to a string of all zeros, which means the server should generate a new random wrap(kB) (just as it does during account creation).

The /password/forgot/send_code API is used to ask the server to send a recovery code. This takes a recovery method, which for now is just an email address. This API is unauthenticated (after all, the user who has forgotten their password knows nothing but their email address). The server marks the corresponding account as "pending recovery", allocates a random forgotPasswordToken for the account, creates a recovery code, and sends the code (with instructions) via email. The API returns forgotPasswordToken to the client.

The user must copy the recovery code into the same browser where they started the process. The client then submits the code to /password/forgot/verify_code along with the forgotPasswordToken they received. If they match, the server allocates a accountResetToken and returns it to the client. If they do not match, the server increments a counter (which is used to decide if an online guessing attack is happening).

forgotPasswordToken can be used three times before it is exhausted. If the user guesses incorrectly this often, the client must call send_code again to get a new token and code. Each account has at most one token+code active at a time.

The recovery code is initially a random 8-digit decimal number. If an attacker tries to sign in as someone else, hits the "forgot my password" button, then submits a guess to /password/forgot/verify_code, they will have a 1-in-100-million chance of success. If the server detects too many wrong guesses, it should increase the length of new codes. Another defensive technique is to require that users click an email link before being given the code: the server is told when the link is clicked, so the code will not be enabled until the email has been read. It remains to be seen whether this will be sufficient.

The exact thresholds are TBD, but a nominal goal is to keep the chances of any attack succeeding to below 1-in-a-million per year. To achieve this, we can tolerate 100 verify_code failures in a single year before we must increase the length of the code.

Using accountResetToken

An accountResetToken, obtained through either of the methods above, is used to invoke the /account/reset API. If the request is accepted, the server replaces the account password with a new one, updates the wrap(kB) value, cancels all active sessions and tokens (disconnecting all devices from the account), and sends a "your password has been changed" email to the user.

If the client knew the old password, it can supply a new wrap(kB) value that will yield the same kB key as before, so no data will be lost. If the client did *not* know the old password, then it will supply a wrap(kB) of all-zeros, which tells the server to generate a new random wrap(kB) (just like during account creation), which means the class-B data will be lost.

The client puts their new password through the same stretching procedure as described in the new-account section above, resulting in a new srpVerifier and unwrapBKey. If they knew the old kB, they XOR it with the new unwrapBKey to obtain a new wrap(kB).

/account/reset needs request confidentiality, since the arguments include the newly wrapped kB value and the new SRP verifier, both of which enable a brute-force attack against the password. HAWK provides request integrity. The response is a single "ok" or "fail", conveyed by the HTTP headers, so we do not require response confidentiality, and can live without response integrity.

So the single-use resetToken is used to derive three values:

  • tokenID
  • request HMAC key
  • request XOR key


 

The request data will contain wrap(kB) and the new SRP verifier, concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, both pieces are fixed-length. We generate enough reqXORkey bytes to cover both values.

The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).

The client submits other values in the same request:

  • stretchParams
  • mainKDFSalt
  • srpSalt


These values do not require confidentiality, so are not included in the encrypted bundle. They are still protected by the HAWK integrity check. Note that the server should assert that both salts are different than the previously stored values.

After using /account/reset, clients should immediately perform the login protocol from above. If the old password was forgotten, this is necessary to fetch kA. In either case, a new sessionToken is required, since old sessions and tokens are revoked by /account/reset. Clients should retain the new srpPassword value during this process to avoid needing to run the lengthy key-stretching routine a second time.

Deleting The Account

When the user wishes to completely delete their account, the browser needs to perform two actions:

  • contact the storage servers and delete all records and collections
  • contact the keyserver and delete the account information

The user should be prompted for their password as confirmation (i.e. a browser in the normal attached-and-synchronizing state should not be able to erase the account information: it must acquire a new authToken first).

The device then obtains an authToken as described above, then spends it on a HAWK-protected request to the /account/destroy endpoint. This request contains no body and returns only a success code.

 

Crypto Notes

Strong entropy is needed in the following places:

  • (client) creation of private "a" value inside SRP
  • (server) initial creation of kA and wrap(kB)
  • (server) creation of private "B" value inside SRP
  • (server) creation of signToken and resetToken


On the server, code should get entropy from /dev/urandom via a function that uses it, like "crypto.randomBytes()" in node.js or "os.urandom()" in python. On the client, code should combine local entropy with some fetched from the keyserver via getEntropy(), to guard against failures in the local entropy pool. Something like HKDF(IKM=localEntropy+remoteEntropy, salt="", info=KW("mergeEntropy")).

An HKDF-based stream cipher is used to protect the response for getToken2(), and the request for resetAccount(). HKDF is used to create a number of random bytes equal to the length of the message, then these are XORed with the plaintext to produce the ciphertext. An HMAC is then computed from the ciphertext, to protect the integrity of the message.

HKDF, like any KDF, is defined to produce output that is indistinguishable from random data ("The HKDF Scheme", http://eprint.iacr.org/2010/264.pdf , by Hugo Krawczyk, section 3). XORing a plaintext with a random keystream to produce ciphertext is a simple and secure approach to data encryption, epitomized by AES-CTR or a stream cipher (http://cr.yp.to/snuffle/design.pdf). HKDF is not the fastest way to generate such a keystream, but it is safe, easy to specify, and easy to implement (just HMAC and XOR).

Each keystream must be unique. SRP is defined to produce a random session key for each run (as long as at least one of the sides provides a random ephemeral key). We define resetToken to be a single-use randomly-generated value. Hence our two HKDF-XOR keystreams will be unique.

A slightly more-traditional alternative would be to use AES-CTR (with the same HMAC-SHA256 used here), with a randomly-generated IV. This is equally secure, but requires implementors to obtain an AES library (with CTR mode, which does not seem to be universal). An even more traditional technique would be AES-CBC, which introduces the need for padding and a way to specify the length of the plaintext. The additional specification complexity, plus the library load, leads me to prefer HKDF+XOR.

kB is equal to the XOR of wrapKey (which is a deterministic function of the user's email address, password, mainSalt, and the stretching parameters) and the server's randomly-generated wrap(kB) value, making kB a random value too. Using XOR as a wrapping function allows us to avoid sending kB or wrap(kB) in the initial createAccount arguments, which are not protected by SRP, and thus would enable an eavesdropper to mount a dictionary attack on the password (using wrap(kB) as their oracle).

Likewise, allowing the server to generate kA avoids exposure during createAccount. The only point of vulnerability is a forgotten-password resetAccount() message, if an eavesdropper can learn the resetToken. In this case, the attacker might either use the resetToken themselves (then use signToken to learn kA and wrap(kB)), or passively decrypt the resetAccount() arguments to retrieve just wrap(kB). Given wrap(kB) and some encrypted browser data, the attacker can guess passwords (and derive kB, etc) until the data decrypts properly.

To make this technique safe, any time kB or the password is changed, the mainSalt should be changed too. Otherwise knowledge of both wrap(old-kB) and old-kB would reveal wrapKey, making it easy to deduce the new kB. Changing mainSalt causes wrapKey to change too, preventing this.

mainSalt is incorporated at the end of the stretching process to allow it to proceed in parallel with the getToken1() call that retrieves the salt. The inputs to the lengthy stretch come entirely from the user (email and password) or are optimistically (stretching parameters). This speedup seems more important than the minor security benefit of including the salt at the beginning of the stretch.

There is no MAC on wrap(kB). If the keyserver chooses to deliver a bogus wrap(kB) or kA, the client will discover the problem a moment later when it talks to a storage server and attempts to retrieve data from an unrecognized collection-ID (since we intend to derive collection-IDs from the key used to encrypt their data, which will be derived from kA or kB as appropriate). It might be useful to add a checksum to kA and wrap(kB) to detect accidental corruption (e.g. store and deliver kA+SHA256(kA)), but this doesn't protect against intentional changes. We omit this checksum for now, assuming that disks will be reliable enough to let us never experience such failures.

We use scrypt without a per-user salt. This is safe because the "password" input to scrypt is already diversified by the user's email address. The intention is to allow the scrypt-helper to run on anonymous data, so that an attacker who compromises this helper cannot easily learn the email addresses of the partially-stretched K1 values that it is receiving, confounding their attack.

HAWK provides one thing: integrity/authentication for the request contents (URL, method, and optionally the body). It does not provide confidentiality of the request, or integrity of the response, or confidentiality of the response. For /certificate/sign, we do not need request confidentiality or response confidentiality, since the client's pubkey and the resulting certificate will both be exposed over a similar SSL connection to the storage server later. And it is sufficient to rely on the response integrity provided by SSL, since the client can verify the returned certificate for itself. For the other keyserver APIs protected by HAWK, these properties are either unnecessary, or are provided by additional mechanisms.

Proof-Of-Work

To protect the server's session table memory and CPU usage for the initial SRP calculation, the server might require clients to perform busy-work before calling getToken1(). The server can control how much work is required.

The getToken1() call looks for a "X-PiCL-PoW:" HTTP header. Most of the time, clients don't supply this header. But if the server responds to the getToken1() call with an error that indicates PoW is required, clients must create a valid PoW string and include it as the value of an "X-PiCL-PoW:" header in their next call to getToken1().

The server's error message includes two parameters. The first is a "prefix string": the client's PoW string is required to begin with this prefix. The second is a "threshold hash". SHA256(PoWString) is required to be lexicographically earlier than the thresholdHashString (i.e. the numerical value of its hash must be closer to zero than the threshold). The client is expected to concatenate the prefix with a counter, then repeatedly increment the counter and hash the result until they meet the threshold, then re-submit their getToken1() request with the combined prefix+counter string in the header. If the client has spent more than e.g. 10 seconds doing this, the client should probably help the user cancel the operation and try again.

When a server is under a DoS attack (either via some manual configuration tool or sensed automatically), it should start requiring valid unique X-PiCL-PoW headers. The server should initially require very little work, by using a threshold hash with just a few leading zero bits. If this is insufficient to reduce the attack volume, the threshold should be lowered, requiring even more work (from both the attacker and legitimate clients).

The server should create a prefix string that contains a parseable timestamp and a random nonce (e.g. "%d-%d-" % (int(time.time()), b32encode(os.urandom(8)))). The server should also decide on a cutoff time (perhaps ten minutes ago). Each server must then maintain a table of "old PoW strings" to prevent replay attacks (these do not need to be shared among all servers: an in-RAM cache is fine).

When the server receives a proposed PoW string, it first splits off the leading timestamp, and if the timestamp is older than the cutoff time, it rejects the string (either by dropping the connection, or returning a new "PoW required" error if it's feeling nice). Then it hashes the whole string and compares it against the threshold, rejecting those which fail to meet the threshold. Finally, for strings that pass the hash threshold, it checks the "old strings" table, and rejects any that appear on that list.

If the PoW string makes it past all these checks, the server should add the string to the "old strings" table, then accept the request (i.e. compute an srpB value and add a session-id table entry for the request).

The old-strings table check should be optimized to reject present strings quickly (i.e. if we are under attack, we should expect to see lots of duplicates of the same string, and must minimize the work we do when this occurs).

The server can remove values from the old-strings table that have timestamps older than the cutoff time. The server can also discard values at other times (to avoid consuming too much memory), without losing anything but protection against resource consumption.

Other notes:

The server-side code for this can be deferred until we care to have a response to a DoS attack. However the client-side code for this must be present from day one, otherwise we won't be able to turn on the defense later without fear of disabling legitimate old clients.

The server should perform as little work as possible before rejecting a token. Every extra CPU cycle it spends in this path is increasing the DoS attack amplification factor.

The nonce in the prefix string exists to make sure that two successive clients get different prefixes, and thus do not come up with the same counter value (and inadvertently create identical strings, looking like a replay attack). If this proved annoying or expensive, we could instead obligate clients to produce their own nonce.

TBD: Is this worth it? Should the PoW string go into an HTTP header? (I want it to be cheap to extract, and not clutter logs). Should the error response be a distinctive HTTP error code so our monitoring tools can easily count them? We can also use this feature to slow down online guessing attacks (i.e. trigger it either when getToken1 is called too much or when getToken2 produces too many errors). Since getToken1() includes an email address, we could also requires PoWs for some addresses (e.g. those we know to be under attack) but not others.

Glossary

This defines some of the jargon we've developed for this protocol.

  • data classes: each type of browser data (bookmarks, passwords, history, etc) can be assigned, by the user, to either class-A or class-B
  • class-A: data assigned to this class can be recovered, even if the user forgets their password, by proving control over an email address and resetting the account. It can also be read by Mozilla (since it runs the keyserver and knows kA), or by the user's IdP (by resetting the account without the user's permission).
  • class-B: data in this class cannot be recovered if the password is forgotten. It cannot be read by the IdP. Mozilla (via the keyserver) cannot read this data, but can attempt a brute-force dictionary attack against the password.
  • kA: the master key for data stored as "class-A", a 32-byte binary string. Individual encryption keys for different datatypes are derived from kA.
  • kB: the master key for data stored as "class-B", a 32-byte binary string.
  • wrap(kB): an encrypted copy of kB. The keyserver stores wrap(kB) and never sees kB itself. The client (browser) uses a key derived from the user's password to decrypt wrap(kB), obtaining the real kB.
  • sessionToken: a long-lived per-device token which allows the device to obtained signed BrowserID certificates for the account's identity (GUID@picl-something.org). This token remains valid until the user revokes it (either by changing their password, or triggering some kind of "revoke a specific device" or "revoke all devices" function).

Test Vectors

The following example uses a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264).

These test vectors were produced by the python code in https://github.com/warner/picl-spec-crypto . The diagrams may lag behind the latest version of that code.

stretch-KDF

email: 616e6472c3a94065 78616d706c652e6f 7267

password: 70c3a4737377c3b6 7264

K1 (scrypt input): f84913e3d8e6d624 689d0a3e9678ac8d cc79d2c2f3d96414 88cd9d6ef6cd83dd

K2 (scrypt output): 5b82f146a6412692 3e4167a0350bb181 feba61f63cb17140 12b19cb0be0119c5

stretchedPW: c16d46c31bee242c b31f916e9e38d60b 76431d3f5304549c c75ae4bc20c7108c

main-KDF

mainSalt (normally random): 00f0000000000000 0000000000000000 0000000000000000 000000000000034d

srpPW: 00f9b71800ab5337 d51177d8fbc682a3 653fa6dae5b87628 eeec43a18af59a9d

unwrapBKey: 6ea660be9c89ec35 5397f89afb282ea0 bf21095760c8c500 9bbcc894155bbe2a

internal x (base 10): 8192518690918 99580124814080709381476194749939 03899664126296984459627523279550

internal x (hex): b5200337cc3f3f92 6cdddae0b2d31029 c069936a844aff58 779a545be89d0abe

v (verifier as number) (base 10): 114649 57230405843056840989945621595830 71784395917725741221739574165799 54316134303691657140298181419198 87853709633756255809680435884948 69849281177012209169281795507853 57610332070005048463659745521969 83218225819721112680718485091921 64608360806562626442477160609654 43167308814558974899899506977051 96721477608178869100211706638584 53875100985456239693728258285562 04889672594983678412848291529879 88548996842770025110751388952323 22170663943486107183421205517476 84831590615660554713667726412525 73641352721966728239512914666806 49625530438034148797508015907639 67594925530663571631035463732161 30193328802116982288883318596822

SRP Verifier

k (base 10): 259003859907 09503006915442163037721228467470 35652616593381637186118123578112

srpSalt (normally random): 00f1000000000000 0000000000000000 0000000000000000 0000000000000179

srpVerifier: 00173ffa0263e63c cfd6791b8ee2a40f 048ec94cd95aa8a3 125726f9805e0c82 83c658dc0b607fbb 25db68e68e93f265 8483049c68af7e82 14c49fde2712a775 b63e545160d64b00 189a86708c69657d a7a1678eda0cd79f 86b8560ebdb1ffc2 21db360eab901d64 3a75bf1205070a57 91230ae56466b8c3 c1eb656e19b794f1 ea0d2a077b3a7553 50208ea0118fec8c 4b2ec344a05c66ae 1449b32609ca7189 451c259d65bd15b3 4d8729afdb5faff8 af1f3437bbdc0c3d 0b069a8ab2a959c9 0c5a43d42082c774 90f3afcc10ef5648 625c0605cdaace6c 6fdc9e9a7e6635d6 19f50af773452247 0502cab26a52a198 f5b00a2798589165 07b0b4e9ef9524d6

SRP B

private b (normally random) (base 10): 1198277 66042000957856349411550091527197 08125226960544768993643095227800 29105361555030352774505625606029 71778328003253459733139844872578 33596964143417216389157582554932 02841499373672188315534280693274 23189198736863575142046053414928 39548731879043135718309539706489 29157321423483527294767988835942 53343430842313006332606344714480 99439808861069316482621424231409 08830704769167700098392968117727 43420990997238759832829219109897 32876428831985487823417312772399 92628295469389578458363237146486 38545526799188280210660508721582 00403102624831815596140094933216 29832845626116777080504444704039 04739431335617585333671378812943

private b (hex): 00f3000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 000000000000000f

transmitted srpB: 0022ce5a7b9d8127 7172caa20b0f1efb 4643b3becc535664 73959b07b790d3c3 f08650d5531c19ad 30ebb67bdb481d1d 9cf61bf272f84398 48fdda58a4e6abc5 abb2ac496da5098d 5cbf90e29b4b110e 4e2c033c70af7392 5fa37457ee13ea3e 8fde4ab516dff1c2 ae8e57a6b264fb9d b637eeeae9b5e43d faba9b329d3b8770 ce89888709e02627 0e474eef822436e6 397562f284778673 a1a7bc12b6883d1c 21fbc27ffb3dbeb8 5efda279a69a1941 4969113f10451603 065f0a0126666456 51dde44a52f4d8de 113e2131321df1bf 4369d2585364f9e5 36c39a4dce33221b e57d50ddccb4384e 3612bbfd03a268a3 6e4f7e01de651401 e108cc247db50392

SRP A

private a (normally random) (base 10): 1193346 47663227291363113405741243413916 43482736314616601220006703889414 28162541137108417166380088052095 43910927476491099816542561560345 50331133015255005622124012256352 06121987030570656676375703406470 63422988042473190059156975005813 46381864669664357382020200036915 26156674010218162984912976536206 14440782978764393137821956464627 16314542157937343986808167341567 89864323268060014089757606109012 50649711198896213496068605039486 22864591676298304745954690086093 75374681084741884719851454277570 80362211874088739962880012800917 05751238004976540634839106888223 63866455314898189520502368799907 19946264951520393624479315579863

private a (hex): 00f2000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 000000000000d3d7

transmitted srpA: 007da76cb7e77af5 ab61f334dbd5a958 513afcdf0f47ab99 271fc5f7860fe213 2e5802ca79d2e5c0 64bb80a38ee08771 c98a937696698d87 8d78571568c98a1c 40cc6e7cb101988a 2f9ba3d65679027d 4d9068cb8aad6ebf f0101bab6d52b5fd fa81d2ed48bba119 d4ecdb7f3f478bd2 36d5749f2275e948 4f2d0a9259d05e49 d78a23dd26c60bfb a04fd346e5146469 a8c3f010a627be81 c58ded1caaef2363 635a45f97ca0d895 cc92ace1d09a99d6 beb6b0dc0829535c 857a419e834db128 64cd6ee8a843563b 0240520ff0195735 cd9d316842d5d3f8 ef7209a0bb4b54ad 7374d73e79be2c39 75632de562c59647 0bb27bad79c3e2fc ddf194e1666cb9fc

SRP key-agreement

u: b284aa1064e87751 50da6b5e2147b47c a7df505bed94a6f4 bb2ad873332ad732

S: 0092aaf0f527906a a5e8601f5d707907 a03137e1b601e04b 5a1deb02a981f4be 037b39829a27dba5 0f1b27545ff2e287 29c2b79dcbdd32c9 d6b20d340affab91 a626a8075806c26f e39df91d0ad979f9 b2ee8aad1bc783e7 097407b63bfe58d9 118b9b0b2a7c5c4c debaf8e9a460f4bf 6247b0da34b760a5 9fac891757ddedca f08eed823b090586 c63009b2d740cc9f 5397be89a2c32cdc fe6d6251ce11e44e 6ecbdd9b6d93f30e 90896d2527564c7e b9ff70aa91acc0ba c1740a11cd184ffb 989554ab58117c21 96b353d70c356160 100ef5f4c28d19f6 e59ea2508e8e8aac 6001497c27f362ed bafb25e0f045bfdf 9fb02db9c908f103 40a639fe84c31b27

M1: 27949ec1e0f16256 33436865edb037e2 3eb6bf5cb91873f2 a2729373c2039008

srpK: e68fd0112bfa31dc ffc8e9c96a1cbadb 4c3145978ff35c73 e5bf8d30bbc7499a

/auth

srpK: e68fd0112bfa31dc ffc8e9c96a1cbadb 4c3145978ff35c73 e5bf8d30bbc7499a

respHMACkey: 6584613597ef012f f1752b7869f01d03 c72547a7b7199681 531d9df1991edf23

respXORkey: 455835926ae37a1b 627bd16affbeeab6 27ecc737121826ca 4a2bac2c100bf417

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

plaintext: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

ciphertext: 253957f10e861c7c 0a12bb0193d384d9 579db544666d50bd 3252d6576c768a68

MAC: a98c87f5769ab4cc ca3df863faeb217e b16ddc29d712b301 12b446324ee806d6

response: 253957f10e861c7c 0a12bb0193d384d9 579db544666d50bd 3252d6576c768a68 a98c87f5769ab4cc ca3df863faeb217e b16ddc29d712b301 12b446324ee806d6

/session

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID: 6dcae8ff8f55a793 a0fa1ed31115451b 4df233b3a0641cc6 18ecadfd1fe4a691

reqHMACkey: 1640a4e6bc8c8e54 858be9960a8b0740 fa06effdf169246f 52012ae868fc6c48

respHMACkey: 7f3e075e74523ced fa817c2fa4ae97e1 e51da38d7a992b66 8a35c86af946b155

respXORkey: 02977a9167830705 74b610cc25320262 175b45fbd7b26438 f9e200abc029f14e f38399314b172f1e e928fcdcd194ab19 92433cab0e94569d bf623b46dd9fbf55

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

sessionToken: a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

plaintext: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

ciphertext: 8216f812e3068182 fc3f9a47a9bf8ced 87cad7684327f2af 617b9a305cb46fd1 53223b92efb289b9 418156777d3905b6 22f28e18ba21e02a 07db81fd612201ea

MAC: 639fd132f637abd3 ecd2482ccf11ed76 8cfd6979e1954046 1e8ef5204e66c542

response: 8216f812e3068182 fc3f9a47a9bf8ced 87cad7684327f2af 617b9a305cb46fd1 53223b92efb289b9 418156777d3905b6 22f28e18ba21e02a 07db81fd612201ea 639fd132f637abd3 ecd2482ccf11ed76 8cfd6979e1954046 1e8ef5204e66c542

/account/keys

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

tokenID: d010c94c753c012c d6801e8beb1aa6cc 3da9ea3de3de1dee 32785dbd99a579e8

reqHMACkey: 1707b05908acc4dc cda5b8304d9500d0 8c53e00c31672a53 490dfb5ef2934060

respHMACkey: 31d0c12186b76897 c3351878a65097cf d595da4ce48e69a2 485ff1a77c71b0d0

respXORkey: eed35591e1f1c43b 7cd604e371b9cfb7 a980c9a36fa737c6 a48c5d60a89fc291 4ec1a2150a0777b7 9a1e8499058cd17a ebc1441db8b3bf18 2cd0aefa92482692

kA: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

plaintext: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

ciphertext: cef277b2c5d4e21c 54ff2ec85d94e198 99b1fb905b9201f1 9cb5675b94a2fcae 0e80e0564e4231f0 d257ced249c19f35 bb90164eece6e94f 7489f4a1ce1578cd

MAC: 86f1c57d2e7f6c97 8181684e189b710f dd26a3f34e3aaed8 64be9577ae81a256

response: cef277b2c5d4e21c 54ff2ec85d94e198 99b1fb905b9201f1 9cb5675b94a2fcae 0e80e0564e4231f0 d257ced249c19f35 bb90164eece6e94f 7489f4a1ce1578cd 86f1c57d2e7f6c97 8181684e189b710f dd26a3f34e3aaed8 64be9577ae81a256

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

unwrapBKey: 6ea660be9c89ec35 5397f89afb282ea0 bf21095760c8c500 9bbcc894155bbe2a

kB: 2ee722fdd8ccaa72 1bdeb2d1b76560ef ef705b04349d9357 c3e592cf4906e075

use session (certificate/sign, etc)

sessionToken: a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

tokenID: 639503a218ffbb62 983e9628be5cd64a 0438d0ae81b2b9da deb900a83470bc6b

reqHMACkey: 3a0188943837ab22 8fe74e759566d0e4 837cbcc7494157aa c4da82025b2811b2

/password/change

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID: cafc36360afd92de 5ca21800022a9af1 3a5766b91bd82fd4 0eaa5b6e01489796

reqHMACkey: b07c0cf4553e44ff fe991caa2546b50d 895fb9ac8f8746d2 d29119d9616de193

respHMACkey: d2ddfefd1913fa34 48e18abda9b54c92 43fd51bf14dc9091 2179269c0e958a04

respXORkey: dcc5425e13b876ea f1d3aa95a4735622 46994088d86adb5a 526d9f1f5d170254 456dd26dcc54483e f489d55097b69028 8826f0cf1985a6ad e3e83461517c8d49

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

accountResetToken: c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

plaintext: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

ciphertext: 5c44c0dd973df06d 795a201e28fed8ad d608d21b4cff4dcd caf40584c18a9ccb 85ac10ae08918ef9 3c401f9b5b7b5ee7 58f7221ccd50707a 3b31eeba8da15396

MAC: cc3053fe922268d7 9c0dd6eb74bd40f5 07ae2d587483b864 8ef771b699dd39d9

response: 5c44c0dd973df06d 795a201e28fed8ad d608d21b4cff4dcd caf40584c18a9ccb 85ac10ae08918ef9 3c401f9b5b7b5ee7 58f7221ccd50707a 3b31eeba8da15396 cc3053fe922268d7 9c0dd6eb74bd40f5 07ae2d587483b864 8ef771b699dd39d9

/account/reset

accountResetToken: c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

tokenID: a6857e5d53d35073 d50ef2ce2c4dd747 32bb2eae1af5bf79 618ed945e1310792

reqHMACkey: 47fab27352ee6b48 33938d76519bbdb8 ac7293f8b5e74335 6fdd1d5edf39f52d

reqXORkey: 82ed612313a11673 95108d7d379b2029 7a539ce9d3861e95 1bf5a9b9cdbfb332 bd6aba056ce0c568 2c5a93963446b1b4 7397c8c24f3a1d67 2a0ddc856474f5b1 33ab884ce33335c1 5578a1a7302933cb 458fbee0a5e52414 c914beb97568a30c 28364dc8fb03ae7c 76a2f324a9a1cee6 71b74aa8906d0e03 39fb52a1bf2b1ef5 ab5d883295db62af 20701cb3af42a09e c76cda585ab5644b 7250ef7b780537e5 b3e784d37a118bd6 57a0fe29ec6e5cd3 325be8e1d8a3dd71 b360ea266757e463 ada6b0a7a85a8ac0 eed618d9f6ee91ab 1d2f714f224d67db 46843c4e3339de15 efe0297a45f9fe0d 6d768b5c589a290f 11f03237192cc0a3 a02645a810d83bb1 84d582bfb15d2393 3fa4805374da62c6 a2c887b157285c6a 79b47156c9abe02e

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

newSRPv: 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111

plaintext: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111

ciphertext: c2ac236057e45034 dd59c7367bd66e66 2a02ceba87d348c2 43acf3e291e2ed6d ac7bab147df1d479 3d4b82872557a0a5 6286d9d35e2b0c76 3b1ccd947565e4a0 22ba995df22224d0 4469b0b6213822da 549eaff1b4f43505 d805afa86479b21d 39275cd9ea12bf6d 67b3e235b8b0dff7 60a65bb9817c1f12 28ea43b0ae3a0fe4 ba4c992384ca73be 31610da2be53b18f d67dcb494ba4755a 6341fe6a691426f4 a2f695c26b009ac7 46b1ef38fd7f4dc2 234af9f0c9b2cc60 a271fb377646f572 bcb7a1b6b94b9bd1 ffc709c8e7ff80ba 0c3e605e335c76ca 57952d5f2228cf04 fef1386b54e8ef1c 7c679a4d498b381e 00e12326083dd1b2 b13754b901c92aa0 95c493aea04c3282 2eb5914265cb73d7 b3d996a046394d7b 68a56047d8baf13f

/account/destroy

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID: b2512ff41c4e6d8a beb3bda37e326f51 cf4efdbf90e50e77 029be2563884b9fe

reqHMACkey: 75cfa782c19e41f9 c7e125f3dc4c3bf1 0a77c93a9999e06f b2646b3038e4ea44

Keyserver Protocol Summary

  • POST /account/create (email,srpV,srpSalt) -> ok (server sends verification email)
    • creates a user account
  • GET /account/devices [sessionToken] () -> list of devices
  • GET /account/keys [keyFetchToken,needs-verf] () -> kA/wrap(kB)
    • single-use, only if email is verified, encrypted results
  • POST /account/reset [authed+encrypted by accountResetToken] (wrap(kB),srpV,srpSalt) -> ok
    • single-use, does not require email to be verified, revoke all tokens for account, send notification email to user
  • POST /account/delete [authToken] () -> ok, account deleted
  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /session/create [authToken] () -> keyFetchToken, sessionToken
  • POST /session/destroy [sessionToken] () -> ok
    • for detaching a device, destroy all tokens
  • POST /recovery_email/status [sessionToken] () -> "verified" status of email
    • use "Accept: text/event-stream" header for server-sent-events; server will send "update" event with the new content of the resource any time it changes.
  • POST /recovery_email/resend_code [sessionToken] () -> re-send verification email
  • POST /recovery_email/verify_code (code) -> set "verified" flag
    • this code will come from a clickable link and is an unauthenticated endpoint
    • this could maybe take the recovery method if that would be helpful
    • sets verified flag on recovery method
  • POST /certificate/sign [sessionToken,needs-verf] (pubkey) -> cert
    • only if recovery email is verified
  • POST /password/change/start [authToken,needs-verf] () -> accountResetToken, keyFetchToken
  • POST /password/forgot/send_code () -> forgotPasswordToken
    • sends code to recovery method (email for now, maybe SMS later)
    • this is a short code, not a clickable link
  • POST /password/forgot/resend_code (forgotPasswordToken) -> re-sends code
  • POST /password/forgot/verify_code (forgotPasswordToken, code) -> accountResetToken
    • sets verified flag on recovery method
  • POST /get_random_bytes


Typical Client Flows

Create account

  • POST /account/create (email,srpV,srpSalt) -> ok (server sends verification email)
  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /session/create [authed with authToken]() -> keyFetchToken, sessionToken
  • GET /recovery_email/status [sessionToken] () -> "verified" status
    • (optional, only if user requests resend) POST /recovery_email/resend_code [sessionToken]() -> ok
    • POST /recovery_email/verify_code (code) -> ok
  • GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
  • POST /certificate/sign [sessionToken] (pubkey) -> cert


Attach to new device

  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /session/create [authToken] () -> keyFetchToken, sessionToken
  • GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
    • (if unverified-error, do waitUntilEmailVerified, then try again)
  • POST /certificate/sign [sessionToken] (pubkey) -> cert


Forgot password

  • POST /password/forgot/send_code (email) -> forgotPasswordToken
  • POST /password/forgot/verify_code (forgotPasswordToken, code) -> accountResetToken
  • POST /account/reset [authed+encrypted by accountResetToken] (0000,srpV,srpSalt) -> ok
  • GOTO "Attach to new device"


Change Password

  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /password/change/start [authToken] () -> accountResetToken, keyFetchToken
  • GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
  • POST /account/reset [authed+encrypted by accountResetToken] (wrap(kB),srpV,srpSalt) -> ok
  • GOTO "Attach to new device"