Cryptography
Document

Document Encryption

Document Content Encryption

Document content encryption is using secsync (opens in a new tab). It was developed in parallel to Serenity to provide a secure and scalable document encryption solution. For Serenity currently the Secscyn implementation is inlined https://github.com/serenity-kit/Serenity/tree/main/packages/secsync (opens in a new tab) in the Serenity codebase as it helps for easier debugging and development. The code bases are kept in sync. In the future it will use the published package.

More information about the encryption can be found in the documentation:

Cryptographic Dependencies and actual implementation

  • generateSubkeyId: sodium.randombytes_buf(16)
  • kdf: sodium.crypto_kdf_hkdf_sha256_expand(sodium.crypto_kdf_hkdf_sha256_extract(key, subkeyId), context, crypto_aead_xchacha20poly1305_ietf_KEYBYTES)
  • generateNonce: sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES)
  • encryptAead: sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(message, additionalData, nonce, key)
  • decryptAead: sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, additionalData, nonce, key)
  • generateBoxNonce: sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES)
  • asymmetric_encrypt: sodium.crypto_box_easy(message, nonce, recipientPublicKey, senderPrivateKey)
  • asymmetric_decrypt: sodium.crypto_box_open_easy(ciphertext, nonce, senderPublicKey, recipientPrivateKey)

Note: For encryptAead every message is prefixed with a block of four 0-bytes to ensure commitment. More info here (opens in a new tab)

Usage in Serenity

The Page component integrates with secsync here: https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L123-L376 (opens in a new tab)

There are two main aspects on how Secsync is used in Serenity:

  • The publicData (AAD) of each Snapshot contains the full KeyDerivationTrace for the Snapshot key. This allows anyone with access to the referenced workspaceKey to decrypt the Snapshot key and therefor decrypt the document.
  • Also the documentTitle ciphertext, nonce and KeyDerivationTrace are sent along with ever Snapshot. This allows anyone with access to the referenced Snapshot key to decrypt the documentTitle. We considered making the title ciphertext and nonce part of the Snapshot AAD, but then a client would need to do a new Snapshot when updating the title. It was a trade-off between cryptographically binding them together vs flexibility to update the title separately.

Important params

Creating a new Document incl. a Snapshot

When creating a document from the sidebar first the parentFolder's key is derived. Ever document has a parentFolder as Documents can't exist without one.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarFolder/SidebarFolder.tsx#L271-L291 (opens in a new tab)

Then the full KeyDerivationTrace for the Snapshot is constructed including the snapshotKey entry.

subkeyId = generateSubkeyId();
snapshotKey = kdf(subkeyId, "snapshot", parentFolderKey);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarFolder/SidebarFolder.tsx#L292-L317 (opens in a new tab)

With it a new Snapshot is created.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarFolder/SidebarFolder.tsx#L329-L352 (opens in a new tab)

The document title key is derived from the Snapshot key and encrypted. (explained in more detail further down on this Page)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarFolder/SidebarFolder.tsx#L353-L362 (opens in a new tab)

All the data is sent to the backend and the document is created in the database.

Introduction Document on Workspace Creation

Note: When creating a new workspace it is created with one folder and an introduction document prefilled with content.

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

Snapshot Key Trace Structure

Document Snaphot in a root folder example

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

{
  workspaceKeyId: "5Q5_3zwQ9ZOkykoLvVNHtmz48_4Fxfvq";
  trace: [{
    entryId: "EWJjmGc7ErIDLQfaFBzqJJDXqLxutq6J";
    subkeyId: 42;
    parentId: null;
    context: "folder__";
  },
  {
    entryId: "48KAsddOZ2vhn8usF7ZLfpt_V2ohc7bT";
    subkeyId: 4323;
    parentId: "EWJjmGc7ErIDLQfaFBzqJJDXqLxutq6J";
    context: "snapshot";
  }];
}

Creating a new Snapshot for an existing Document

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L157-L256 (opens in a new tab)

What's known to the user is the parentFolderId and first the parent folder's key has to be derived. This is done by fetching active workspaceKeyId and the folder trace (without keys).

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/createNewSnapshotKey/createNewSnapshotKey.ts#L23-L34 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/folder/createFolderKeyDerivationTrace.ts#L12-L38 (opens in a new tab)

Then the actual keys are derived for the KeyDerivationTrace and the last entry's key is used as the parent folder key.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/createNewSnapshotKey/createNewSnapshotKey.ts#L35-L48 (opens in a new tab)

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

With this key a new snapshot key is derived and pushed to the end of the new KeyDerivationTrace including the new snapshot key. The trace as well as the new snapshot key are provided to secsync to encrypt the new snapshot content.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L165-L169 (opens in a new tab)

In addition to make sure the content can be decrypted by a document share link a new encrypted box is created for each document share link and sent along with the new snapshot.

// for each active document share link of the document (determined by the document chain)
nonce = generateBoxNonce();
snapshotKeyEncryptionContext = 1;
snapshotKeyEncryptionVersion = 0;
boxCiphertext = asymmetric_encrypt(
  concat(
    snapshotKeyEncryptionContext,
    snapshotKeyEncryptionVersion,
    documentId,
    snapshotId,
    snapshotKey
  ),
  nonce,
  shareLinkDevicePublicKey,
  senderDeviceEncryptionPrivateKey
);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L211-L228 (opens in a new tab)

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

Derivation of a Snapshot Key to decrypt a Snapshot

In the callback getSnapshotKey the Snapshot key is derived from the workspaceKey and the KeyDerivationTrace stored in the publicData (AAD) of the Snapshot.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L301-L305 (opens in a new tab)

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

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

Decrypting a Snapshot key with a document share link

content = asymmetric_decrypt(
  boxCiphertext,
  nonce,
  senderDeviceEncryptionPublicKey,
  shareLinkDevicePrivateKey
);
assert(content[0] === 1); // check for snapshot key encryption
assert(content[1] === 0); // check for snapshot key ecryption version
assert(content[2...26] === documentId); // check for document id
assert(content[27...51] === snapshotId); // check for snapshot id
snapshotKey = content[52...131];

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sharePage/SharePage.tsx#L116-L132 (opens in a new tab)

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

Validating collaborators

In the callback isValidClient each snapshot, update or ephemeral message is verified if the author is a member of the workspace.

Based on the WorkspaceMemberDevicesProof it can be determined if the device was an active member of the workspace at the time of the snapshot, update or ephemeral message.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L351-L370 (opens in a new tab)

Document Title

The document title shows up in the sidebar and to efficiently list the documents in a folder the document title can be decrypted independently from the document content. That said the document title key always references the latest snapshot key. This whenever a new snapshot key is created a new document title key is created as well and the document title re-encrypted.

Encrypting a new Document Title

subkeyId = generateSubkeyId();
documentTitleKey = kdf(subkeyId, "doctitle", snapshotKey);
nonce = generateNonce();
ciphertext = encryptAead(documentTitle, {}, nonce, documentTitleKey);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L200-L209 (opens in a new tab)

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

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

Decrypting the Document Title

The document title shows up in the sidebar and to efficiently list the documents in a folder the document title can be decrypted independently from the document content.

documentTitleKey = kdf(subkeyId, "doctitle", snapshotKey);
documentTitle = decryptAead(ciphertext, {}, nonce, documentTitleKey);

In the sidebar

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarPage/SidebarPage.tsx#L67-L71 (opens in a new tab) https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/documentStore.ts#L163-L174 (opens in a new tab)

In the document before re-encrypting the current one based on a new snapshot

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L190-L195 (opens in a new tab)

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

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

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