Identity/AttachedServices/KeyServerProtocol

< Identity‎ | AttachedServices

Contents

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, srpVerifier) to the keyserver's "POST /account/create" API

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.

IdP Auth Protocol

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:

Stretching KDF

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.

masterKey KDF

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 5054 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.

client-side SRP Verifier 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.

server-side SRP

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.

client-side SRP

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.

Decrypting the authToken

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.

After Login: Using the authToken

After the authToken is acquired, the client can create a session and fetch the encryption keys. The high-level flow looks like this:

Using the authToken

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.

Decrypting the sessionToken and keyFetchToken

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.

keyFetchToken: server encrypts keys

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.

keyFetchToken: client decrypts keys

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).

unwrapping 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


Using the sessionToken, signing certificates

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.

Server encrypts passwordChange response

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


Client encrypts resetAccount request

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.

Deleting the Account

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 (revision aa441c6). The diagrams above 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

authtoken

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID (authToken): 9a39818e3bbe6132 38c9d7ff013a1841 1ed2c66c3565c3c4 de03feefecb7d212

reqHMACkey: 4a17cbdd54ee17db 426fcd7baddff587 231d7eadb408c091 ce19ca915b715985

requestKey: 9d93978e662bfc6e 8cc203fa4628ef5a 7bf1ddfd7ee54e97 ec5c033257b4fca9

/session

requestKey: 9d93978e662bfc6e 8cc203fa4628ef5a 7bf1ddfd7ee54e97 ec5c033257b4fca9

respHMACkey: cd3f50403d060b21 76d32f71ca105bd8 7c9b6c4e10e3ebf9 3f5077bec2db24fa

respXORkey: 8422c53143dea9c6 044afbe95228f291 74996b830b1794a3 eff132da53174d43 92eeb87ccf8ad7a8 0c432894e066de6b 0ff70658dfbf2f07 b9c7704045edcd54

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

sessionToken: a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

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

ciphertext: 04a347b2c75b2f41 8cc37162dea57c1e e408f9109f820234 7768a841cf8ad3dc 324f1adf6b2f710f a4ea823f4ccb70c4 bf46b4eb6b0a99b0 017ecafbf95073eb

MAC: 7973ddbb184b601a c4df09704028ebfc 754dd50e7d8eebfa 52ce3fd868c69852

response: 04a347b2c75b2f41 8cc37162dea57c1e e408f9109f820234 7768a841cf8ad3dc 324f1adf6b2f710f a4ea823f4ccb70c4 bf46b4eb6b0a99b0 017ecafbf95073eb 7973ddbb184b601a c4df09704028ebfc 754dd50e7d8eebfa 52ce3fd868c69852

/account/keys

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

tokenID (keyFetchToken): 3d0a7c02a15a62a2 882f76e39b6494b5 00c022a8816e0486 25a495718998ba60

reqHMACkey: 87b8937f61d38d0e 29cd2d5600b3f4da 0aa48ac41de36a0e fe84bb4a9872ceb7

keyRequestKey: 14f338a9e8c6324d 9e102d4e6ee83b20 9796d5c74bb734a4 10e729e014a4a546

respHMACkey: f824d2953aab9faf 51a1cb65ba9e7f9e 5bf91c8d8fd1ac1c 8c2d31853a8a1210

respXORkey: ce7d7aa77859b235 9932970bbe2101f2 e80d01faf9191bd5 ee52181d2f0b7809 8281ba8cff392543 3a89f7c3095e0c89 900a469d60790c83 3281c4df1a11c763

kA: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

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

ciphertext: ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636 c2c0f8cfbb7c6304 72c0bd88451342c6 c05b14ce342c5ad4 6ad89e84464c993c

MAC: 3927d30230157d08 17a077eef4b20d97 6f7a97363faf3f06 4c003ada7d01aa70

response: ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636 c2c0f8cfbb7c6304 72c0bd88451342c6 c05b14ce342c5ad4 6ad89e84464c993c 3927d30230157d08 17a077eef4b20d97 6f7a97363faf3f06 4c003ada7d01aa70

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 (sessionToken): c0a29dcf46174973 da1378696e4c82ae 10f723cf4f4d9f75 e39f4ae3851595ab

reqHMACkey: 9d8f22998ee7f579 8b887042466b72d5 3e56ab0c094388bf 65831f702d2febc0

/password/change

requestKey: 9d93978e662bfc6e 8cc203fa4628ef5a 7bf1ddfd7ee54e97 ec5c033257b4fca9

respHMACkey: 60d01e3d1da53b10 93124a30c26889d7 b2e067e7a09fde14 6f935e3c653614f9

respXORkey: 3de5bd5e80faf84a dfca5396148123ef 8184cd4bc10a7c8a db1688495affee67 e07f80d914c5105f c86d6af24c4be1b1 ef6c9c661422ac43 181b3d29624a0cc2

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

accountResetToken: c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

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

ciphertext: bd643fdd047f7ecd 5743d91d980cad60 11155fd8559fea1d 438f12d2c66270f8 20be421ad000d698 00a4a03980862f7e 3fbd4eb5c0f77a94 c0c2e7f2be97d21d

MAC: 804fc4bc30923cc0 d6c07ffea954848e 0076b94f7deee71f a34db5c106d91980

response: bd643fdd047f7ecd 5743d91d980cad60 11155fd8559fea1d 438f12d2c66270f8 20be421ad000d698 00a4a03980862f7e 3fbd4eb5c0f77a94 c0c2e7f2be97d21d 804fc4bc30923cc0 d6c07ffea954848e 0076b94f7deee71f a34db5c106d91980

/account/reset

accountResetToken: c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

tokenID (accountResetToken): 46ec557e56e531a0 58620e9344ca9c75 afac0d0bcbdd6f8c 3c2f36055d9540cf

reqHMACkey (for HAWK): 716ebc28f5122ef4 8670a48209190a16 05263c3188dfe452 56265929d1c45e48

requestKey: aa5906d2318c6e54 ecebfa52f10df4c0 36165c230cc78ee8 59f546c66ea3c126

reqHMACkey (for ciphertext): a0d894a6232f2e78 66a51dda3f84e01e ae5adb812564f391 6c0d3cb16bdb743c

reqXORkey: 9cbde8fc9df31455 837b881e6c0d7e3c ca13589bc868c527 95fc00e51f2048ab d56de37629cda0b0 3f580a9e6c433724 b5df12a735ccf2a1 e232d4f5fef84f86 a1b4fdc47f8d1f73 12a6a230a8742d5b c144ee9abce25b57 9670b81085064cfb dcab862d9d57abcc 2142dcdde6682281 d378c89b0dce06ae cd1c1ff68ad6db9a 9cab0b02e160805b 59bb8712c8233056 1b3ded75c430e23c 22338833b6f2ba39 f5015ca7a905d6ee 6ec5b1e3ae5204ba 6f3630ebf30ebbac 1f47329e8fe22770 2a3d61f593328dd4 f0a96b628aa8ffec 181e93d2af8d87ff 2d90d67caaf7f7c9 af024c93cfc79e94 67ba70b3076c20cc 141aa254ff159b25 3125a304441cecf3 4fc1845ce96ee598 21fde83cd24e3209 4d304477bfa2c8ed df236e512560694e

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: dcfcaabfd9b65212 cb32c25520403073 9a420ac89c3d9370 cda55abe437d16f4 c47cf26738dcb1a1 2e491b8f7d522635 a4ce03b624dde3b0 f323c5e4efe95e97 b0a5ecd56e9c0e62 03b7b321b9653c4a d055ff8badf34a46 8761a90194175dea cdba973c8c46badd 3053cdccf7793390 c269d98a1cdf17bf dc0d0ee79bc7ca8b 8dba1a13f071914a 48aa9603d9322147 0a2cfc64d521f32d 33229922a7e3ab28 e4104db6b814c7ff 7fd4a0f2bf4315ab 7e2721fae21faabd 0e56238f9ef33661 3b2c70e482239cc5 e1b87a739bb9eefd 090f82c3be9c96ee 3c81c76dbbe6e6d8 be135d82ded68f85 76ab61a2167d31dd 050bb345ee048a34 2034b215550dfde2 5ed0954df87ff489 30ecf92dc35f2318 5c215566aeb3d9fc ce327f403471785f

MAC: 1d3572fe0b4bdf66 f2b2657cb2ee56fc 80f7a82708cafd82 1952e1f01761cb29

response: dcfcaabfd9b65212 cb32c25520403073 9a420ac89c3d9370 cda55abe437d16f4 c47cf26738dcb1a1 2e491b8f7d522635 a4ce03b624dde3b0 f323c5e4efe95e97 b0a5ecd56e9c0e62 03b7b321b9653c4a d055ff8badf34a46 8761a90194175dea cdba973c8c46badd 3053cdccf7793390 c269d98a1cdf17bf dc0d0ee79bc7ca8b 8dba1a13f071914a 48aa9603d9322147 0a2cfc64d521f32d 33229922a7e3ab28 e4104db6b814c7ff 7fd4a0f2bf4315ab 7e2721fae21faabd 0e56238f9ef33661 3b2c70e482239cc5 e1b87a739bb9eefd 090f82c3be9c96ee 3c81c76dbbe6e6d8 be135d82ded68f85 76ab61a2167d31dd 050bb345ee048a34 2034b215550dfde2 5ed0954df87ff489 30ecf92dc35f2318 5c215566aeb3d9fc ce327f403471785f 1d3572fe0b4bdf66 f2b2657cb2ee56fc 80f7a82708cafd82 1952e1f01761cb29

/account/destroy

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID (authToken): 9a39818e3bbe6132 38c9d7ff013a1841 1ed2c66c3565c3c4 de03feefecb7d212

reqHMACkey: 4a17cbdd54ee17db 426fcd7baddff587 231d7eadb408c091 ce19ca915b715985

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"