Cryptography
Folder (Encryption & Integrity)

Folder Encryption

Folders are encrypted with a folder key derived from a workspace key. Only the folder name is encrypted. All sub-folders and files are encrypted with their own keys derived from the folder key.

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)
  • canonicalize: predictable canonicalization of JSON as defined by RFC8785 https://www.npmjs.com/package/canonicalize (opens in a new tab)

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)

Creating a folder

At first only the key derivation trace is created. The key derivation trace is used to derive the folder key and all sub-folder keys. While each folder could be directly derived from a workspaceKey this design was chosen to later on support sharing entire folders. Once a folderKey is known all sub-folders and documents inside can be decrypted knowing the subkeyIds used to derive the sub-keys.

This means only root folders are directly derived from the workspace key. All sub-folders are derived from their parent folder key.

Building a key derivation trace is done here:

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

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

Deriving a folder key

subkeyId = generateSubkeyId();
folderKey = kdf(subkeyId, "folder__", parentKey); // parentKey is the workspaceKey for root folders or the parent folder key for sub-folders

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

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

Folder Key Trace Structure

Root folder example

{
  workspaceKeyId: "5Q5_3zwQ9ZOkykoLvVNHtmz48_4Fxfvq";
  trace: [{
    entryId: "EWJjmGc7ErIDLQfaFBzqJJDXqLxutq6J";
    subkeyId: 42;
    parentId: null;
    context: "folder__";
  }];
}

Subfolder:

{
  workspaceKeyId: "5Q5_3zwQ9ZOkykoLvVNHtmz48_4Fxfvq";
  trace: [{
    entryId: "EWJjmGc7ErIDLQfaFBzqJJDXqLxutq6J";
    subkeyId: 42;
    parentId: null;
    context: "folder__";
  },
  {
    entryId: "lY5fksq266Y9lDPxsnpHxX6toMpQ5pM7";
    subkeyId: 856;
    parentId: "EWJjmGc7ErIDLQfaFBzqJJDXqLxutq6J";
    context: "folder__";
  }];
}

Encrypting the folder name

subkeyId = generateSubkeyId();
folderKey = kdf(subkeyId, "folder__", parentKey); // parentKey is the workspaceKey for root folders or the parent folder key for sub-folders
nonce = generateNonce(); 
additionalAuthenticatedData = canonicalize(folderId, workspaceId, keyDerivationTrace);
ciphertext = encryptAead(folderName, additionalAuthenticatedData, nonce, folderKey);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebar/Sidebar.tsx#L98-L105 (opens in a new tab) (creating a folder) https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/createWorkspaceForm/CreateWorkspaceForm.tsx#L116-L123 (opens in a new tab) (create workspace with a folder and a document) https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarFolder/SidebarFolder.tsx#L483-L490 (opens in a new tab) (update folder name) https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/sidebarFolder/SidebarFolder.tsx#L236-L243 (opens in a new tab) (create sub-folder)

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

Decrypting a folder name

folderKey = kdf(subkeyId, "folder__", parentKey); // parentKey is the workspaceKey for root folders or the parent folder key for
additionalAuthenticatedData = canonicalize(folderId, workspaceId, keyDerivationTrace);
folderName = decryptAead(ciphertext, additionalAuthenticatedData, nonce, folderKey);

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

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

Deleting a folder

Currently only a post request to the server. As described further down in the future we plan to add a hash tree structure to make sure a client can validate that the server has provided all sub-folders and documents. Then on delete a client would also need to create and send along a new version of the hash tree structure.

Additional Authenticated Data

The AAD when encrypting/decrypting a folder name includes the workspaceId, folderId and keyDerivationTrace to bind the result to a specific workspace and folder. This should avoid that an attacker or the server send a folder in place of another one.

Future improvements

In the future we plan to add a hash tree structure (similar to a merkel tree) to make sure a client can validate that the server has provided all sub-folders and documents. This is not implemented yet. This also can be useful for a client to verify if their folder structure is still up to date by only querying for the root hash.

Note: Probably makes sense to includes the document chain hash in the createDocumentHash