Client side encryption of event data
This document outlines the procedures and technologies used for encrypting event data in a way that it can only be decrypted by users and operators satisfying the established restrictions.
It also deals with the measures that need to be taken in order to store identifiers and keys so that they cannot be accessed by third parties running scripts in the context of the host site.
Stages in the user journey
Considering that a user visits a site that is served on the host foo.xyz which is run by an operator who is using offen to collect usage data by embedding the script, the following happens:
On first visit of a site that is using offen ever, no tokens and keys needed for identification and encryption are present yet, so they need to be created using the following mechanisms:
- The script injects an
iframeelement called vault, which is then loading a document from the domain vault.offen.dev.
- Once the vault is loaded, it will generate a random UserSecret which is supposed to be used as a key for symmetrically encrypting event data later on. This UserSecret is saved in the browser's
LocalStoragein the context of vault.offen.dev. The user secret is unique per operator, meaning that on visit of site using a different AccountID, a new UserSecret will be created.
- The vault will download the operator's PublicKey from the hypothetical domain exchange.offen.dev, which is part of an asymmetric key pair that was created when the operator created their account with offen.
- The PublicKey is now used to encrypt the UserSecret and generates the EncryptedUserSecret.
- The EncryptedUserSecret is now being sent to exchange.offen.dev.
- This request will trigger the server-side creation of a unique UserID which will be used to identify the user and grant access to the associated data.
- The EncryptedUserSecret will be saved alongside the UserID in the operator's account data. In order to make the UserID opaque to other accounts it is hashed before saving, salted using the AccountID.
- The response to sending the EncryptedUserSecret will now contain the UserID in an HttpOnly cookie in the context of *.offen.dev
- The vault is now ready to receive events. Events that have been sent before the above exchange has finished will have been queued and are sent now.
After this procedure has finished, the UserSecret and the UserID are available for scripts that are running in the context of vault.offen.dev, meaning that it can now encrypt data in a way that it can be decrypted by both the user (owning the UserSecret) as well as the operator (owning the EncryptedUserSecret which can by decrypted using the operator's PrivateKey).
Scripts running in the context of the host - both first-party and third-party - have no way of accessing the UserSecret or the UserID as they are bound to either *.offen.dev (Cookie) or vault.offen.dev (LocalStorage).
For a returning visitor, the key and secret exchange can be skipped as it is already saved on the server.
- The script injects an
iframeelement containing the vault which is then loading a document from the hypothetical domain vault.offen.dev.
- The vault successfully checks for the presence of a UserSecret for the currently used AccountID and skips the exchange procedure.
First visit of a different site
For a user that has already visited a site that uses offen, but not this one, the following will happen:
- On a visit of the host site a new UserSecret would be generated in the vault as none matching the current AccountID could be found.
- When exchanging the EncryptedUserSecret, the already present UserID sent in the request's cookies will be used in its hashed form for saving the secret with the account record.
Once a relevant event payload is being generated, it needs to be securely transferred to the server. The following steps will be applied:
- The script running in the host uses the
postMessageAPI to send the event payload to the embedded
iframecontaining the vault.
- The vault uses the UserSecret to symmetrically encrypt the event payload and send it to the hypothetical domain events.offen.dev
- The server running on events.offen.dev saves the encrypted event alongside the operator's AccountID and the hashed UserID which is sent in the request's Cookies. Hashing the UserID with a salt that is unique to the operator ensures the identifiers cannot be connected across accounts.
As neither the UserSecret nor the operator's PrivateKey are known to the server, there is no way of decrypting the data from knowing the contents of the database.
User deletes cookies and/or LocalStorage
At any time, a user might delete the data stored in cookies as well as
The following would happen in case only
LocalStorage is wiped:
- On a visit of the host site a new UserSecret would be generated.
- The key and secret exchange would be run again, sending the new EncryptedUserSecret.
- Seeing that it receives an EncryptedUserSecret alongside a UserID value in the request's cookies, the exchange server needs to trigger the following in order not to invalidate existing data:
- All events and the EncryptedUserSecret that are currently associated with the hashed version of the UserID need to be migrated to a new random identifier that keeps the data available, but detaches it from the identifier in use.
- The new EncryptedUserSecret is saved to replace the previously used value assocaiated with the hashed version of the UserID
The following would happen in case only cookies are wiped:
- On a visit of the host site an event encrypted using the previously generated UserSecret would be sent to events.offen.dev.
- Seeing that there is no UserID is present in the request's cookies, the server rejects the payload with status
- On receiving the
401response, the vault needs to wipe all stored UserSecret values and re-initiates the exchange procedure and re-schedules the newly encrypted event payload to be sent again once the exchange has finished.
In case a user deletes both cookies and
LocalStorage they will restart the user journey from the beginning with no additional logic needed to be performed.
All three deletion scenarios would have the following consequences:
- The user loses access to all data that is tied to their previous identifier, meaning it can also never be deleted on their behalf again. The operator will still be able to analyze data tied to the previous identifier, but has no way of connecting the old identifier with the new one.
- Data tied to the newly issued UserID behaves just like before.
- To the operator the user now appears as a new entity.
All traffic is expected to use the HTTPS protocol. Downgrade attacks will be prevented implementing HSTS.
The vault is used to isolate the UserSecret and handle all outbound requests. This keeps the UserSecret protected from third party script access as it is subject to the Same Origin Policy.
When run in the context of the analytics application itself it needs to be able to respond to requests for decrypting events that have been provided by the server in their encrypted form. Communication between the embedding analytics application and the vault uses the
postMessage API and ensures this mechanism cannot be hijacked by third parties by sending a
targetOrigin value with each
message that restricts receivers of the messages to documents served by the offen.dev domain.
As the UserSecret needs to be exported, using
IndexedDB (which could save a raw
CryptoKey object) instead of
LocalStorage is not an option that brings improvements.
Secure and HttpOnly cookies
The UserID is sensitive information in the context of offen, yet needs to be shared when sending event data. It is therefore needed to use
HttpOnly cookies bound to the *.offen.dev domain so that it is never accessible to any script or any server that is not serving from offen.dev.