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:
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
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)
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);
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