Cryptography
Workspace Key (Encryption)

Workspace Key

Each workspace has one currently active workspace key and can have multiple previous workspace keys.

The Workspace info (currently only the workspace name) is directly encrypted with a workspace key.

Keys (except document attachment keys) to encrypt workspace data are derived from the currently active workspace key using key derivation. These are keys to encrypt:

  • folder names
  • document names
  • document content & cursor information (Secsync snapshots, updates and ephemeral messages)
  • document comments

Goals of this Design

  • Prevent removed members from decrypting data that was encrypted after they left the workspace
  • Reduce the number of necessary encryption boxes. Currently one encryption box is used for each user per workspaceKey and all the other keys (except document attachments) can be derived.
  • Avoid re-encrypting all data when a member is removed from a workspace. It's the servers responsibility to also block removed members from accessing the workspace, but this can't be guaranteed.

Cryptographic Dependencies and actual implementation

  • generate_id: sodium.to_base64(sodium.randombytes_buf(24))
  • keygen: sodium.crypto_kdf_keygen()
  • noncegen: sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
  • asymmetric_encrypt: sodium.crypto_box_easy(message, nonce, recipientPublicKey, senderPrivateKey)
  • asymmetric_decrypt: sodium.crypto_box_open_easy(ciphertext, nonce, senderPublicKey, recipientPrivateKey)
  • encryptAead: sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(message, additionalData, nonce, key)
  • decryptAead: sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, additionalData, nonce, key)

Initially generating a workspace key and the corresponding encryption boxes

When a workspace is created, a new workspace key is generated and encrypted for each device of each member of the workspace.

workspaceKeyEncryptionContext = 0;
workspaceKeyEncryptionVersion = 0;
workspaceKeyId = generate_id();
workspaceKey = keygen();
// for each active device of each user (determined by the workspace chain)
nonce = noncegen();
boxCiphertext = asymmetric_encrypt(
  contact(
    workspaceKeyEncryptionContext,
    workspaceKeyEncryptionVersion,
    workspaceId,
    workspaceKeyId,
    workspaceKey
  ),
  nonce,
  receiverDeviceEncryptionPublicKey,
  senderDeviceEncryptionPrivateKey
);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/device/createWorkspaceKeyBoxesForDevices.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/encryptWorkspaceKeyForDevice/encryptWorkspaceKeyForDevice.ts (opens in a new tab)

The workspace key is stored in the backend and set as the active workspace key.

Decrypting a workspace key

When a user logs in, all workspace keys are retrieved and decrypted for the active device.

content = asymmetric_decrypt(
  boxCiphertext,
  nonce,
  senderDeviceEncryptionPublicKey,
  receiverDeviceEncryptionPrivateKey
);
assert(content[0] === 0); // check for workspace key encryption
assert(content[1] === 0); // check for workspace key ecryption version
assert(content[2...26] === workspaceId); // check for workspace id
assert(content[27...51] === workspaceKeyId); // check for workspace key id
workspaceKey = content[52...131];

Workspace keys are fetched on demand e.g. in the Sidebar:

and then calls

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/retrieveWorkspaceKey.ts (opens in a new tab) or https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/retrieveCurrentWorkspaceKey.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/decryptWorkspaceKey/decryptWorkspaceKey.ts (opens in a new tab)

Workspace keys are fetched on demand e.g. in the Sidebar:

Adding a new device to the workspace and creating the corresponding encryption box on a login

When a new device is added during login, the mainDevice is available. All boxes for all workspaceKeys are retrieved and decrypted. Then a new box is created for each workspaceKey and encrypted for the new device.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/login/LoginForm.tsx#L95-L97 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/attachDeviceToWorkspaces.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/device/createNewWorkspaceKeyBoxesForActiveDevice.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/encryptWorkspaceKeyForDevice/encryptWorkspaceKeyForDevice.ts (opens in a new tab)

Adding a new user to the workspace and creating the corresponding encryption boxes

When a new user is added to the workspace, each workspace key is encrypted for each device of the new user.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/authorizeMembersIfNecessary.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/encryptWorkspaceKeyForDevice/encryptWorkspaceKeyForDevice.ts (opens in a new tab)

Rotation a workspace key when removing a member

When a member is removed from a workspace, a new workspace key is generated and encrypted for each device of each member of the workspace.

This action can only be done by an admin of the workspace (as defined in the workpace-chain state).

https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/workspaceSettingsMembersScreen/WorkspaceSettingsMembersScreen.tsx#L171-L175 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/rotateWorkspaceKey.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/encryptWorkspaceKeyForDevice/encryptWorkspaceKeyForDevice.ts (opens in a new tab)

Rotating a workspace key when removing a device

When a device is removed by a user from their device list in the UI, a new workspace key is generated and encrypted for each device of each member of the workspace. This is done for every workspace of the user.

NOTE: This interaction is problematic since any user can rotate a workspace key (currently also with the role VIEWER or COMMENTER). The benefit is that everyone can force any new encryption to be done with a new workspace key. The downside is that this can be used to create an error state for specific users or even specific devices of a user by creating broken encrypted workspace keys boxes.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/accountDevicesSettingsScreen/AccountDevicesSettingsScreen.tsx#L115-L128 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/rotateWorkspaceKey.ts (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/encryptWorkspaceKeyForDevice/encryptWorkspaceKeyForDevice.ts (opens in a new tab)

Future implementation

Before shipping Serenity to customers the following architecture will be implemented:

In the user interface the user has two options how to remove a device:

  • remove device
  • report device as compromised

When the user removes a device the workspace continues to work.

When the user reports a device as compromised the workspace is locked down and no further reads or writes can be done until a workspace admin comes online and a workspace key roation is invoked.

Encrypting workspace info (name)

nonce = generateNonce();
ciphertext = encryptAead(workspaceInfo, {}, nonce, workspaceKey);

During workspace creation: https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/createWorkspaceForm/CreateWorkspaceForm.tsx#L204-L207 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/encryptWorkspaceInfo/encryptWorkspaceInfo.ts (opens in a new tab)

or also in the settings: https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/workspaceSettingsGeneralScreen/WorkspaceSettingsGeneralScreen.tsx#L92-L95 (opens in a new tab)

Decrypting workspace info (name)

workspaceInfo = decryptAead(ciphertext, "", nonce, key);

In the settings: https://github.com/serenity-kit/Serenity/blob/main/apps/app/machines/workspaceSettingsLoadWorkspaceMachine.ts#L172-L190 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/decryptWorkspaceInfo/decryptWorkspaceInfo.ts (opens in a new tab)

Or when loading the workspace info from the API to list them in the AccountMenu https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/workspaceStore.ts#L111-L184 (opens in a new tab)

Open Questions

  • Must the workspaceKey and workspaceKeyId cryptographically e.g. signing bound to the workspace and the workspace chain with a reference to the event hash? If not could someone replace a workspace key and abuse that to decrypt data with an attack? The important part is probably to verify that the author of a workspace key box is verified by referencing the workspace chain.