Introduction
Definitions
User: Is an entity representing a person who can use the system.
Client: Is an instance of the software that is used by a user to interact with the system.
Server: Is a centralized service that is responsible for storing and exchanging data between client.
Security Model
The main objective of the whole project is to create a system where the server can never access the content or name of a folder or document. The assumes that the code the client is executing is not compromised.
The server has a lot of meta-data. The aim is to minimize the amount of meta-data in the future, but ideally keep all the verifications in place. We hope zero-knowledge proofs will help us to achieve this, but this is unexplored territory for our team.
Clients must trust other verified clients with the encryption. A malicious client can leak data or in worst case create malicious data that could prevent other clients from decrypting the content.
What can't be prevented is that the server will hide data from a user. A server could even fork a workspace, but once that happens the participansts of each for can't work together anymore since the chain verifications would dedect the fork.
Client Responsibilities
- Authenticate changes to chains & encryptions before presenting data to the user
- Inform the user on malicious activity and set the respective document or workspace to read-only
Server Responsibilities
- Register new users
- Authenticate users
- Authorization when accessing data
- Authorization and validation before writing it to the database
- This excludes validation of encrypted data since the server should never be able to decrypt data belonging to a user.
- Validation means
- Verifying signatures
- Ensuring that all signature chains (user, workspace and document) are in the correct order and no forks are created.
Signature chains
A lot of the security of the system is based on signature chains. The idea is that critical changes e.g. adding or removing a member from a workspace is recorded as an transaction, then hashed and signed.
Such an event is sent and stored on the server. The server is responsible to keep ensure a single chain is created and no forks are created.
The benefit of this design is that the hash can be referenced in other data structures or stored locally and be used to verify that the server is not hiding data or sending old data. While it's not possible to prevent forks by the server, it discourages due client side verification.
Signing convention
The content to get signed must always be a string.
The reason here is that signatures often include other data as well end it's easier to stay consistent and always use strings. In addition every string must be prefixed with the domain context to make sure signatures can't be misused in other places.
Signature Domain Context
- Workspace chain entry:
- Chain Event:
workspace_chain
- Invitation:
workspace_chain_invitation
- Accept Invitation:
workspace_chain_accept_invitation
- Chain Event:
- User chain:
- Chain Event:
user_chain
- User device signing_key proof
user_device_signing_key_proof
- User device encryption_public_key:
user_device_encryption_public_key
- Chain Event:
- Document chain:
- Chain Event:
document_chain
- Share document device encryptionPublicKey:
share_document_device_encryption_public_key
- Chain Event:
- SessionToken signature:
login_session_key
- Comment signature:
comment
- Comment reply signature:
comment_reply
- Workspace Member Devices Proof Signature:
workspace_member_devices_proof
Key Separation
Two symmetric enryption keys are encrypted using asymmetric encryption (box). Specifically:
- Workspace key
- SnapshotKey (only used for decrypting a snapshot when using a documents via a link)
In order to make sure these keys are not mistaken with each other they are prefixed with a context byte (0x00 for workspace and 0x01 for share document).
This byte is followed by another byte 0x00 indicating the version. This is to make sure that in the future the encryption can be changed and newer systems can still decrypt older data.
Depending on the key type more data is added:
- For the workspace key encryption the
workspaceId
andworkspaceKeyId
is included an checked upon decryption. - For the snapshot key encryption the
documentId
and thesnapshotId
is included an checked upon decryption.
Keys encryption convention
The keys to be encrypted must be a Uint8Array.
Example:
sodium.crypto_box_easy(workspaceKey, nonce, publicKey, privateKey);
In the case above the workspaceKey must be a Uint8Array
.
KDF Context
Each key that is derived using a KDF has a context set depending on the purpose of the key:
m_device
- mainDevice encryption key based on the exportKeywsinvite
- workspaceInvitation encryption keyfolder__
- folder encryption keysnapshot
- document snapshot encryption keydoctitle
- document title encryption keycomment_
- document comment encryption keysession_token
- session tokensession_datetime
- session datetime
Code separation
There is a clear separation between the server and client code to ensure a compromised server can't trick a client in breaking the security model.
- Server code is deployed on fly.io (API Service) https://serenityapi.app (opens in a new tab)
- Client code is a static build and deployed on Netlify https://www.serenityapp.page (opens in a new tab)
This is especially important for the Opaque protocol. When the API service is compromised, clients using high-entropy passwords remain secure, thanks to Argon2 running on the client side. However, if a weak password is employed and the API service is breached or the database is leaked, an attacker can relatively effortlessly target the export-key by executing the protocol using the correct server values for a specific registration record.
Miscellaneous
Why not one chain (basically signed event sourcing) used for a workspace incl. all members, devices, folders, documents?
The benefit is very clear. The server could not hide single documents from a user and the complexity would be drastically reduced.
This benefit though comes with a downside:
- It's not possible to drastically reduce the amount of entries to that need to be processed to show a signle document.
- Granular access would not be possible e.g. if a user only gets access to a single document though could not verify everything in the workspace chain without exposing it.
Possible Improvements
The whole design was made with the intention to offer a secure system based on established cryptography concepts. In addition the goal was to keep the complexity for the user as low as possible and offer a good user experience.
There are many possible improvements and we plan to incorporate them in the future. Suggestions are very welcome.