Technical Documentation

Please note that this documentation is a work-in-progress and will be improved over the coming weeks.

Desired Behaviour

End-to-end encryption of sensitive information

This includes the content and title of notes, the contact names and the device names.

Rely on state-of-the-art encryption

The end-to-end encryption must using the Olm & Megolm cryptographic ratchets and follow the documentation's best practices e.g. periodically start new sessions to establish Backward secrecy.

Devices and contacts must be verified

This means currently data is only encrypted for a user's devices or contacts which have been verified by the user. This might change with the introduction of cross-signing for contacts of contacts or the some form of organisation concept.

Entities

User

A user has one Ed25519 keypair. The private key must be storred in a secure store on the device (e.g Keychain Services or Android keystore system). In addition a user has a user ID which is generated by the server.

Possible Improvements

In the future there ideally would be multiple Ed25519 keypairs per user. This way one keypair could invalid e.g. when a user looses a device and wants to invalidate one keypair, but not the others. The downside of this is that it increases complexity for contact and device verification.

Device

A user can have several devices. Each device has

  • one Ed25519 fingerprint key pair and
  • one Curve25519 identity key pair

The private keys of both must never leave the device and must be stored in a secure store (e.g Keychain Services or Android keystore system).

In addition each device creates a number of Curve25519 one-time key pairs. These are used to establish Olm session. To avoid running out of possible sessions also a Curve25519 fallback key pair must be available. For the fallback key also a signature must exist signed with the device's Ed25519 keypair. All one-time keys as well as the fallback key must have a corresponding signature signed by the device. Whenever a one-time key or fallback key is claimed to initiate an Olm session it must be verified that the expected device signed the key.

Since a devices current state consists of the two private keys and all it's one-time and fallback keys, the serialized state can get larger than the maximum value to be stored in a secure store (e.g Keychain Services or Android keystore system). Therefor from version 1.8.3 on the device is encrypted with AES-256 in CBC mode and stored in an insecure storage. The generated encryption key and IV are stored in the secure store which makes storing the device secure again.

Further every device must have a signingKey signed by the user Ed25519 keypair to allow verification that this device belongs to this user. This key is a stringified JSON object with the following structure:

{
  version: 1,
  message: `${public_Ed25519_user_signing_key} ${public_Ed25519_device_signing_key} ${device_id_key}`,
  signature: `${Ed25519_user_signature_of_the_message}`,
}

The public_Ed25519_user_signing_key is included to later support multiple user signing keys per user.

Contact

A contact is another user that has verified through the contact adding/verification flow. In this process each user creates a stringified JSON object with the following structure:

{
  version: 1,
  message: `${public_Ed25519_user_signing_key} ${public_Ed25519_user_signing_key_of_the_other_person}`,
  signature: `${Ed25519_user_signature_of_the_message}`,
}

It includes a message which contains the public Ed25519 keys of both users. This message must be signed with the users Ed25519 keypair. With this signature all your devices can verify that this is the Ed25519 keypair of the other user.

Since this the same Ed25519 keypair used to sign devices everyone also can verify that a certain device belongs to this user.

In order to encrypt data for another contact that data is encrypted for each device. In order to do so all devices of all contacts must be fetched from the server and verified. This is describe in detail later.

The public_Ed25519_user_signing_key is included to later support multiple user signing keys per user.

Note

Creating a Note

When a note is created all devices of a user and one one-time key per devices are fetched from the server. Each device is verified using the user Ed25519 keypair and the ones which aren't valid are filtered out.

A new Megolm group session is initialized and the content of the note is encrypted with it. For each verified device a new Olm session is initialized and a message created containing the keys to decrypt the Megolm session. For each Olm session a one-time key per device must be claimed from the server.

The encrypted note as well as all the Olm messages (GroupSessionMessage) are sent to the server via the GraphQL mutation createRepository. The server responds with an ID and an ID per GroupSessionMessage. On the client the following data is persistet:

  • id (server)
  • groupSession
  • groupSessionMessageIds
  • creation time of the group session

From the App version 1.7.0 on the editor schema version and a signature of the schema version is attached to the note. This way clients can check if their editor is up to date to receive the changes.

Updating a Note

Before updating a note all devices of the current user as well as all devices of the participating contacts are fetched from the server and verified. Non-verified devices are filtered out.

Now either two things can happen:

  • the existing Megolm session is used to encrypt a new message containing the encrypted note
  • a new Megolm session is created

A new Megolm session is created in case the last note update from the current device is older than 24 hours or the GroupSessionMessages sent to the server don't match to the devices. This could be the case if the a device was added or removed. The 24 hour limit is in place to compensate for the lack of backward secrecy in Megolm as described here.

From the App version 1.7.0 on the editor schema version and a signature of the schema version is attached to the note. This way clients can check if their editor is up to date to receive the changes.

Possible Improvement

Currently the server determines if there is a GroupSessionMessages for every device. This should be done by the client.

Adding a contact to a note

When one user adds a contact (another user) to a note all the contact's devices are retrieved from the server. The devices are verified and the once that don't pass are filtered out. A one-time key per verified device is claimed and a new Olm session with a GroupSessionMessage per device created. It contains the all the information and keys to decrypt the existing Megolm session.

In addition the addCollaboratorToRepositories mutation has to be invoked with the repositoryId and contactId. The server will keep the list of contacts the note is shared with.

Possible Improvement

The client should sign that this contact can be part of a note. A user can't share a note with anyone that's not a verified contact, but the decision of who a collaborator is should be the responsibility of the client. This would require a system that also handles removal or users.

Removing a contact from a Note

The removeCollaboratorFromRepository mutation has to be invoked with the repositoryId and collaboratorId. The server will remove the user from the list of contacts the note is shared with.

Private info

Every user has one "private info" data structure to make sure sensible information can be exchanged between the user's devices without the server knowing about the content.

The following data is stored in there:

  • devices (public device Curve25519 key, public device Ed25519 key, device name, app name)
  • contacts (server user ID, name, public user Ed25519 key)
  • contact invitations (server contact invitation ID, user ID, public user Ed25519 key, client secret, server secret)

The private info is encrypted as a Megolm session. For each verified device of user a new Olm session is initialized and a message created containing the keys to decrypt the Megolm session. In contrast to notes every update must be a new Megolm session.

Contact invitation

In order to verify a new contact first a new contact invitation has to be created. It contains the following properties

  • name (for the contact)
  • clientSecret (for verification on the other client)
  • serverSecret (for verification by the server)
  • userId (current user's server ID)
  • userSigningKey (current user's public user Ed25519 key)
  • contact invitation ID (provided by the server)

Flows

Server Authentication

In order to authorize with the server a client must send an authorization header. This header must must start with a prefix signed-utc-msg followed by the device's Ed25519 public key. Next up there must be an ISO 8601 datetime string and then a signature of the ISO 8601 datetime string.

Structure:

signed-utc-msg ${device_ed25519_public_key} ${ISO_8601_datetime_string} ${signature_of_ISO_8601_datetime_string}

The server validates the authenticity of header by verifying the combination of public key, message and signature. Since the API is secured using HTTPS the authorization header should never be exposed. The server will only accept requests that are within a 10 minute timeframe. The reason for this is to make sure that in case a CA gets compromised an old authorization header can't be used to authorize against the server.

Possible Improvements

This approach requires that the clock of the client is roughly in synchronization with the server. In order to avoid that the authorization process could change that every client does a pre-flight request to retrieve a session ID including an expiration datetime. The client would then sign the session ID and the server could verify the authenticity. When the session is soon to expire the client will request a new session.

Signup Process

  1. A new device (Ed25519 fingerprint key pair and one Curve25519 identity key pair) are created. In addition one fallback key and several one-time key pairs are created.

  2. A new user Ed25519 fingerprint key pair is created.

  3. A createUser mutation is invoked with

    • public user Ed25519 key
    • device:
      • public Curve25519 identity key
      • public Ed25519 fingerprint key
      • public one-time keys
      • signature (device keys signed with the user's Ed25519 key)
      • fallbackKey
      • fallbackKey signature
  4. Once the mutation succeeds the private info store is setup and filled with the information of the current device. The private info is encrypted and updated on the server.

Possible Improvement

Combine both mutations into one to avoid ending up in a broken state in case the second fails.

Device Linking

This describes the process to add a new device to an existing account.

On the new device a "device verification" string is constructed. It is a stringified JSON object with the following structure:

{
  version: "1",
  idKey,
  signingKey,
  oneTimeKey,
  secret,
  fallbackKey,
  fallbackKeySignature,
}

It contains the public Curve25519 identity key and the public Ed25519 signing key from the device. In addition a one-time key, the fallback key, a signature of the fallback key and a randomly generated secret.

This "device verification" must be entered to an existing device. Since there is no sensible information in the "device verification" it doesn't matter if the channel is later compromised.

On the existing device a new Olm session with one message is created for the new device. This message contains:

`${secret} ${verification_code} ${private_user_Ed25519_signing_key} ${server_user_id} ${new_device_fallbackKey}`

The verification_code is a randomly generated 6 digit string.

The encrypted message is sent to the server using the addDevice mutation with the following data:

  • device:
    • public Curve25519 identity key
    • public Ed25519 fingerprint key
    • public one-time keys
    • signature (device keys signed with the user's Ed25519 key)
    • fallbackKey
    • fallbackKey signature
  • verificationMessage (encrypted)
  • serverSecret (randomly generated 6 digits)

On the existing device the user gets presented with a 12 digit string. It consists of the the serverSecret and verification_code.

`${server_secret}${verification_code}`

Next up on the new device the user must enter the 12 digit code and once submitted the client fetches the encrypted verificationMessage from the server using the fetchAddDeviceVerification GraphQL query. The query requires the device's public Curve25519 identity key and the serverSecret. If both don't match up the server won't return the verification message. The purpose of this is to avoid man-in-the-middle attacks where an attacker would tamper with the device identification to get access to the private user Ed25519 signing key. That said if the same channel is used to exchange the device identification and the 12 digit verification a man-in-the-middle attack would still be possible.

Once the new device received the encrypted verificationMessage it can be decrypted. The private user Ed25519 signing key can be stored in a secure storage and the new device should add itself to the linked device list in the private info store.

Possible Improvement

The device is already added on the server once the existing device invokes the addDevice GraphQL mutation. This should only happen once the user verifies the full 12 digit verfication code to in order further mitigate man-in-the-middle attacks.

Removing a device

The deleteDevice mutation will remove the device from the server. Once that's done device must also be removed from private info and published to the server.

Adding and verifying Contacts

In order to verify another user as a contact a user Alice first must create a "contact invitation". In order to create one Alice first must provide a name she wants to use for Bob in their contact list. Once submitted a query string is generated containing the following information:

  • version: "1",
  • userId
  • public Ed25519 user signing key
  • serverSecret (randomly generated secret)
  • clientSecret (randomly generated secret)

Next Alice's device invokes the createContactInvitation GraphQL invitation with only the serverSecret and in return receives a id for the contact invitation. The contact invitation with the following information is stored in the private info store:

  • name
  • clientSecret
  • serverSecret
  • userId
  • userSigningKey (public Ed25519 user signing key)
  • contactInvitationId

The invitation code Alice created now should be send to another user Bob. Bob must enter the invitation code and provide a name she wants to use for Alice in their contact list. Once submitted the invitation code will be parsed an the GraphQL query devicesForContactInvitation invoked with

  • userId (of the inviter Alice)
  • userSigningKey (public Ed25519 user signing key of the inviter Alice)
  • serverSecret

If all of them match up the server will return a list of devices. These must be verfied and then an Olm session initialized with one message per device. The message includes the

  • clientSecret
  • userId (Bob)
  • userSigningKey (public Ed25519 user signing key of Bob)

Bob must invoke the GraphQL mutation acceptContactInvitation with the following parameters:

  • userId (of the inviter Alice)
  • userSigningKey (public Ed25519 user signing key of the inviter Alice)
  • serverSecret
  • signature (the signed userSigningKey)
  • contactInfoMessage (a list of all contact info messages)

Once that succeded from Bob's perspective the contact has been established and he must add Alice as contact in the private info store.

Alice periodically must query contactInvitations to look for accepted ones and once there is one it must be completed. This can be done by decrypting the contactInfoMessage from Bob, verifying the clientSecret and invoking the completeContactInvitation GraphQL mutation to finish the process from Alice's perspective. After the mutation was successful the private info store must be updated by removing the contact invitation and adding the new contact.

The purpose of the clientSecret validation is to avoid that the server can't do a man-in-the-middle attack and send devices and user keys of a different user.

Removing a contact

The deleteContact mutation will remove the contact from the server. This opperation though is only one-sided. Once the server deletion was successful the contact entry must also be removed from private info and update published to the server.

Removing a contact invitation

The deleteContactInvitation mutation will remove the contact invitation from the server. Once the server deletion was successful the contact invitation entry must also be removed from private info and the update published to the server.

Possible Improvement

Remove the contact on both sides and create a contact tombstone for the other user.

Updating one-time keys

Ever device sends it's one-time keys to the server. These one-time keys can be claimed by other devices to initiate Olm sessions.

Once an inbound Olm session is initiated the one-time key used to set it up is invalidated.

Devices must perodically if new one-time keys can created. New one-time keys must published to the server.

Known meta data to the server

The server's main purpose is to transfer the encrypted data between devices. Nevertheless a lot of meta data can be extracted:

  • amount of users
  • amount of notes
  • amount of deleted notes
  • amount of updates per note and when they occured
  • contact connections per user
  • amount of devices per user

In addition once a billing account is setup and connected to the account all the billing information can be connected to the user.

Possible Improvement

In the future only the last or last couple note updates will be stored and older once are removed.