Loop/Architecture/Context
The rooms (conversations) in Loop are planned to include additional room context information, such as a URL that will be the topic of the conversation, a thumbnail of that URL, and a long-form description of the conversation purpose. Note that this is list is an initial proposal; it is probable that the context information associated with a room will evolve over time. The initial version of this feature is tracked in bug 1115340. The UX mock-up may be helpful in visualizing how this information is to be presented.
Note: Subsequent versions will require live updates of context information during a conversation. This design does not include that functionality.
Contents
Information Privacy
The context information is user-supplied, so we need to carefully consider privacy handling. The design below serves this purpose by storing the information on the Loop server in an encrypted form. Each room will have its own symmetric key, which will be available to the room owner and anyone to whom they provide the room URL. These room keys will never be available to any server.
This section explains the means by which this achieved at the level of information flow. Details are provided in the sections below. See also bug 1132293, which describes the API being implemented by the FxA client to facilitate the behavior described below.
When a Loop client logs into the FxA server, it will obtain a user-specific symmetric key, kB, and an OAuth application identifier. The key kB is wrapped with the user's account login password. Upon obtaining the key, the FxA client unwraps kB for use as a master user key. This key is then used to derive a loop-specific key, kBr. The Loop client then derives a kRWrapper key via HKDF by mixing in the usage identifier "metadata."
When a client creates a new room, it first generates a new symmetric room key, kR. The room context information is serialized as a JSON object, and encrypted using kR. The client uses the KRWrapper key wrap this key via AES-GCM. The encrypted context information and the wrapped room key wrap(kR) are sent to the Loop server as part of the room creation request; these are stored as part of the room's information.
Upon receipt of the resulting room URL from the server, the client appends the room metadata key kR to the URL as a URL fragment before storing it locally.
Because all desktop clients can constitute their kB upon logging in to FxA and their purpose-specific keys from it, any room context retrieved from the Loop server can be decoded by unwrapping the kR received as part of the room information and using it to decrypt the context. For resilience purposes (e.g., in the case of a lost password), the various kR values are also stored locally by the client.
Standalone clients receive the room metadata key kR in the fragment portion of the URL that was provided to them as the means of joining the room. Upon retrieving the encrypted room context information from the server, they use this kR to decode the room context information and display it to the user.
Further information on derivation of kB can be found at Identity/AttachedServices/KeyServerProtocol. The proposal for creating application specific keys (kBr) was originally made on the fxa-dev mailing list, and this proposal re-uses the terminology outlined in that message.
Note: The preceding design is predicated on implementing the FxA key API in time for the initial feature landing. This ability is being tracked on the FxA github repo. If this cannot be done in time, we will need to store room-keys client-side, without any mechanism to share them among a user's clients. This is sub-optimal, as it makes it impossible to create a room on one client and view or manipulate information on another.
Algorithm Details
To allow for evolving the underlying crypto algorithm, any encrypted context will be paired with an explicit indication of the algorithm in use. For the moment, we define only one algorithm, "AES-GCM". Key length is not explicitly included in the algorithm name, and is instead implied by the length of the accompanying key. For our initial implementation, we will be generating 128-bit keys; however, code should be forwards-compatible with longer key lengths.
For AES-GCM, the "context.value" field is formatted as follows:
Base64(IV || ciphertext || tag)
Where IV is 12 bytes in length, and tag is 16 bytes (128 bits) in length.
Encryption consists of selecting a random 12-byte IV value. This IV, the plaintext JSON representation of the room context fields, and kR are used as input to the AES-GCM encryption algorithm, which is configured to generate a 128-bit validation tag. The IV is then concatenated with the ciphertext and the validation tag. The resulting bytestring is Base64 encoded, and included as the "context.value" field in the appropriate Loop Server API call, alongside the wrapped room key wrap(kR) and the algorithm name ("AES-GCM").
Decryption consists of Base64 decoding the "context" field, splitting off the first 12 bytes for use as an IV, and splitting off the final 16 bytes as the validation tag. These are then used as input to the AES-GCM decryption algorithm (along with kR), the output of which is a JSON object containing fields that correspond to the various room context information fields.
The "key" field is encrypted and decrypted using the kRWrapper key, via AES-GCM.
Local Key Storage and Recovery
One of the consequences of using kB to encrypt room keys is that we lose the ability to decrypt wrap(kR) if the user has to reset his FxA password (note: this is only true for password resets, such as in the case of a forgotten password -- it does not apply to password changes, where the user has the old password and is gracefully changing it to a new one).
As a mitigation against this loss, clients will cache room keys locally. This applies to both keys created by the client as well as keys learned from the Loop server. This allows users to retrieve encrypted context information as long at they haven't both forgotten their password and lost their Firefox profile. To keep the keys on the Loop server valid in the face of password resets, clients will validate the value of wrapped room keys whenever they receive room information. In the case that decrypting their locally-stored copy of kR with kRWrapper yields a different result than the value stored in the Loop server, the client will update the room information with a corrected wrappedKey value.
Loop Server API Changes
From a Loop Server API perspective, this change is nothing more than mechanically replacing "roomName" with "context", which is a itself a structure containing three fields: "value", "alg", and "wrappedKey". The values of these fields are opaque to the server, and are simply stored as part of the room's information, and returned to clients when the room information is fetched.
Note that there will be a transition period during which rooms that were created prior to the introduction of encrypted context will contain a "roomName" field instead of "context". The server will need to deal with this transition gracefully. The server may assume, but is not required to enforce, the constraint that "roomName" will never appear in a room alongside "context".
For the sake of efficiency, the Loop server can also safely assume that the "value" and "wrappedKey" fields are Base64 encoded. This allows, for example, decoding them and storing them as more compact binary fields. The resulting binary data will have very low entropy, so any attempts to compress the information will likely be futile.
As a final note about context information: the current UX calls for this context information to include small image thumbnails as part of the context. This means that context data for a single room will likely be on the order of 20 to 30kB in size, on average.
POST /rooms
This generates a new room and returns the associated URL that can be used to join the room.
POST /rooms HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate Authorization: <stripped> Content-Type: application/json; charset=utf-8 Host: localhost:5000 { "context": { "value": "PWjHj89HBS-...ICUX3Iqd9ZsfDNLoUeAb5KGJgEtDy-7ag52rYY5mGgP2GQ==", "alg": "AES-GCM", "wrappedKey": "KLPCJEy8vewUeHFFLtvMNA" }, "expiresIn": 5, "roomOwner": "Alexis", "maxSize": 2 }
- context.value - The room context information, encrypted and Base64 encoded.
- context.alg - The encryption algorithm used to encrypt the context information.
- context.wrappedKey - The room key (kR), wrapped by the user's application-specific key kRWrapper.
- expiresIn - The number of hours for which the room will exist.
- roomOwner - The user-friendly display name indicating the name of the room's owner.
- maxSize - The maximum number of users allowed in the room at one time.
NOTE: the roomName parameter no longer appears in the message; it has been moved into the "context" portion of the message.
HTTP/1.1 200 OK Connection: keep-alive Content-Type: application/json; charset=utf-8 Date: Wed, 16 Jul 2014 13:09:40 GMT Server-Authorization: <stripped> { "roomToken": "_nxD4V4FflQ", "roomUrl": "http://localhost:3000/rooms/_nxD4V4FflQ", "expiresAt": 1405534180 }
- roomToken - The token used to identify this room.
- roomUrl - A URL that can be given to other users to allow them to join the room. Note that the user must append the room key kR as a url fragment before using this URL.
- expiresAt - The date after which the room will no longer be valid (in seconds since the Unix epoch).
Server implementation note: The Loop server should contact the TokBox servers and retrieve a sessionId for the room at room creation time. This sessionId should be stored persistently with rest of the information associated with the room.
PATCH /rooms/{token}
The "PATCH /rooms/{token}" endpoint is used to update information about an existing room.
PATCH /rooms/_nxD4V4FflQ HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate Authorization: <stripped> Content-Type: application/json; charset=utf-8 Host: localhost:5000 { "context": { "value": "PWjHj89HBS-...ICUX3Iqd9ZsfDNLoUeAb5KGJgEtDy-7ag52rYY5mGgP2GQ==", "alg": "AES-GCM", "wrappedKey": "KLPCJEy8vewUeHFFLtvMNA" }, "expiresIn": 24 }
The parameters for this endpoint are the same as for "POST /rooms". Any omitted parameters are not updated.
HTTP/1.1 200 OK Connection: keep-alive Content-Type: application/json; charset=utf-8 Date: Wed, 16 Jul 2014 13:09:40 GMT Server-Authorization: <stripped> { "expiresAt": 1405534180 }
- expiresAt - The date after which the room will no longer be valid (in seconds since the Unix epoch).
GET /rooms/{token}
This endpoint is used to retrieve information about a single room, including a list of room participants.
Because providing this information to users who are not in the room would be surprising for those in the room, we only allow the room owner and users in the room to access this method. Room owners are authenticated via their HAWK credentials, while room participants are authenticated via the sessionToken bearer token provided to them when they joined the room.
GET /rooms/3jKS_Els9IU HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Authorization: <elided> Host: localhost:5000
- For a desktop client user, the "Authorization" header field is populated with the HAWK token (using a scheme of "Hawk"), just like it is for other requests.
- For the standalone client, the "Authorization" header field is encoded using Basic authentication. The user ID portion is the sessionToken provided to the user when they joined the room, and the password is blank.
HTTP/1.1 200 OK Connection: keep-alive Content-Length: 30 Content-Type: application/json; charset=utf-8 Date: Wed, 16 Jul 2014 13:23:04 GMT ETag: W/"1e-2896316483" Timestamp: 1405516984 { "roomToken": "3jKS_Els9IU", "context": { "value": "PWjHj89HBS-...ICUX3Iqd9ZsfDNLoUeAb5KGJgEtDy-7ag52rYY5mGgP2GQ==", "alg": "AES-GCM", "wrappedKey": "KLPCJEy8vewUeHFFLtvMNA" }, "roomUrl": "http://localhost:3000/rooms/3jKS_Els9IU", "roomOwner": "Alexis", "maxSize": 2, "clientMaxSize": 2, "creationTime": 1405517546, "ctime": 1405517824, "expiresAt": 1405534180, "participants": [ { "displayName": "Alexis", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" }, { "displayName": "Adam", "roomConnectionId": "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" } ] }
- roomToken - The token that uniquely identifies this room
- context - Has the same meaning as in POST /rooms
- roomUrl - A URL that can be given to other users to allow them to join the room.
- roomOwner - The user-friendly display name indicating the name of the room's owner.
- maxSize - The maximum number of users allowed in the room at one time (as configured by the room owner).
- clientMaxSize - The current maximum number of users allowed in the room, as constrained by the clients currently participating in the session. If no client has a supported size smaller than "maxSize", then this will be equal to "maxSize". Under no circumstances can "clientMaxSize" be larger than "maxSize".
- creationTime - The time (in seconds since the Unix epoch) at which the room was created.
- expiresAt - The time (in seconds since the Unix epoch) at which the room goes away.
- participants - An array containing a list of the current room participants. Each participant is formatted with the same fields as described in #User Identification in a Room.
- ctime - Similar in spirit to the Unix filesystem "ctime" (change time) attribute. The time, in seconds since the Unix epoch, that any of the following happened to the room:
- The room was created
- The owner modified its attributes with "PATCH /rooms/{token}"
- A user joined the room
- A user left the room
GET /rooms
List all rooms associated with account
GET /rooms?version=<version> HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Host: localhost:5000
- version - Optional; if present, indicates the version received from the simple push server; the server will limit results to those rooms that have changed on or after this time.
HTTP/1.1 200 OK Connection: keep-alive Content-Length: 30 Content-Type: application/json; charset=utf-8 Date: Wed, 16 Jul 2014 13:23:04 GMT ETag: W/"1e-2896316483" Timestamp: 1405516984 [ { "roomToken": "_nxD4V4FflQ", "context": { "value": "PWjHj89HBS-...ICUX3Iqd9ZsfDNLoUeAb5KGJgEtDy-7ag52rYY5mGgP2GQ==", "alg": "AES-GCM", "wrappedKey": "KLPCJEy8vewUeHFFLtvMNA" }, "roomUrl": "http://localhost:3000/rooms/_nxD4V4FflQ", "roomOwner": "Alexis", "maxSize": 2, "creationTime": 1405517546, "ctime": 1405517546, "expiresAt": 1405534180, "participants": [] }, { "roomToken": "QzBbvGmIZWU", "context": { "value": "7s-v8S37f_wpltgN9oA...wdNnoLZGxNkPJp72B8_rG-JKLi1Gd0Ar0La_2jIxbfbV5ztNXVTVjf==", "alg": "AES-GCM", "wrappedKey": "vB--kjDi837BSwbZpQzkHg" }, "roomUrl": "http://localhost:3000/rooms/QzBbvGmIZWU", "roomOwner": "Alexis", "maxSize": 2, "creationTime": 1405517546, "ctime": 1405517546, "expiresAt": 1405534180, "participants": [] }, { "roomToken": "3jKS_Els9IU", "context": { "value": "nF6F9fp4kg7...ps4zAiBh6Bat9qMfdKSlLhHCG-9BYorTos4p4doT3So3kXQDeLHp==", "alg": "AES-GCM", "wrappedKey": "cKnAvMBgA75QhANh04sT3g" }, "roomUrl": "http://localhost:3000/rooms/3jKS_Els9IU", "roomOwner": "Alexis", "maxSize": 2, "clientMaxSize": 2, "creationTime": 1405517546, "ctime": 1405517818, "expiresAt": 1405534180, "participants": [ { "displayName": "Alexis", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" }, { "displayName": "Adam", "roomConnectionId": "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" } ] }, { "roomToken": "z9Hg4Bk19_Z", "deleted": true } ]
This body is an array of rooms, each of which is formatted as described in #GET_.2Frooms.2F.7Btoken.7D.
Additionally, if a "?version=<version>" parameter is included, then the list will include the rooms that have been deleted since that version. These entries will simply include the roomToken, plus a field "deleted" with a value of "true". None of the other fields need to be included for deleted rooms.
Format of context.value
The unencrypted format stored in context.value is a JSON object. The intention is that the fields will grow over time, so implementations should take care not to remove unknown fields when they modify context information. For future expandability, care should be taken to ensure that elements that might conceivably appear more than once are stored as arrays (even if the current design only calls for one instance). As with unknown fields, implementations that expect only one element in an array should preserve the other values.
For the fields currently defined in the initial version of room context, the structure will look like this:
{ "roomName": "Birthday gift discussion", "urls": [ { "location": "https://www.amazon.com/gp/registry/registry.html", "description": "Amazon.com - Your Lists", "thumbnail": "data: url goes here; implementation should handle other URL schemes gracefully" } ], "description": "Let's get together to talk about what we're going to get the twins for their birthday this year" }