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:
- Security & Privacy Considerations (opens in a new tab)
- Threat Library (opens in a new tab)
- Specification (opens in a new tab)
- Error Handling (opens in a new tab)
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
documentId
: the documentId created by the documentChain - https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L125 (opens in a new tab)signatureKeyPair
: the client's device key pairs https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/page/Page.tsx#L126 (opens in a new tab)
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.
Then the full KeyDerivationTrace for the Snapshot is constructed including the snapshotKey entry.
subkeyId = generateSubkeyId();
snapshotKey = kdf(subkeyId, "snapshot", parentFolderKey);
With it a new Snapshot is created.
The document title key is derived from the Snapshot key and encrypted. (explained in more detail further down on this Page)
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.
Snapshot Key Trace Structure
Document Snaphot in a root folder example
{
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
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).
Then the actual keys are derived for the KeyDerivationTrace and the last entry's key is used as the parent folder key.
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.
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
);
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.
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];
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.
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);
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)