diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 4bd9c3b9..6df12bf4 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -330,7 +330,15 @@ class DocumentSerializer(ListDocumentSerializer): "attachments" field for access control. """ content = self.validated_data.get("content", "") - extracted_attachments = set(utils.extract_attachments(content)) + + # Encrypted content cannot be parsed as a Yjs update + # TODO: for now skip attachment extraction for encrypted documents but we should have them + is_encrypted = self.validated_data.get( + "is_encrypted", self.instance and self.instance.is_encrypted + ) + extracted_attachments = ( + set() if is_encrypted else set(utils.extract_attachments(content)) + ) existing_attachments = ( set(self.instance.attachments or []) if self.instance else set() @@ -418,15 +426,15 @@ class DocumentAccessSerializer(serializers.ModelSerializer): def get_fields(self): """Dynamically control field availability and requirements based on document encryption status.""" fields = super().get_fields() - + # Get the document from context (if available) document = None if "view" in self.context and hasattr(self.context["view"], "document"): document = self.context["view"].document - + # Get the encrypted_document_symmetric_key_for_user field key_field = fields.get("encrypted_document_symmetric_key_for_user") - + if key_field: # If document is encrypted, make the field required if document and getattr(document, "is_encrypted", False): @@ -435,7 +443,7 @@ class DocumentAccessSerializer(serializers.ModelSerializer): # If document is not encrypted, remove the field entirely elif document and not getattr(document, "is_encrypted", False): fields.pop("encrypted_document_symmetric_key_for_user", None) - + return fields def get_abilities(self, instance) -> dict: diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptedWebsocket.ts b/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptedWebsocket.ts index 0363a96f..d4329669 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptedWebsocket.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptedWebsocket.ts @@ -8,6 +8,7 @@ import { export class EncryptedWebSocket extends WebSocket { protected readonly encryptionKey!: CryptoKey; protected readonly decryptionKey!: CryptoKey; + protected readonly onSystemMessage?: (message: string) => void; constructor(address: string | URL, protocols?: string | string[]) { super(address, protocols); @@ -24,6 +25,15 @@ export class EncryptedWebSocket extends WebSocket { // eslint-disable-next-line @typescript-eslint/no-explicit-any const messageEvent = event as any; + // some messages are here to help adjusting the interface or even reloading it + // in case it there is an ongoing decryption, or symmetric key rotation... + // those messages must be parsable so they are not encrypted + if (typeof messageEvent.data === 'string') { + this.onSystemMessage?.(messageEvent.data as string); + + return; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (!(messageEvent.data instanceof ArrayBuffer)) { throw new Error( @@ -127,9 +137,11 @@ export class EncryptedWebSocket extends WebSocket { export function createAdaptedEncryptedWebsocketClass(options: { encryptionKey: CryptoKey; decryptionKey: CryptoKey; + onSystemMessage?: (message: string) => void; }) { return class extends EncryptedWebSocket { protected readonly encryptionKey = options.encryptionKey; protected readonly decryptionKey = options.decryptionKey; + protected readonly onSystemMessage = options.onSystemMessage; }; } diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx index 4f5b09a7..241a249f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx @@ -9,7 +9,7 @@ import { isFirefox } from '@/utils/userAgent'; import { toBase64 } from '../utils'; -const SAVE_INTERVAL = 100 * 60000; +const SAVE_INTERVAL = 60000; export const useSaveDoc = ( docId: string, diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx index f24d382a..9b77988b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx @@ -56,6 +56,11 @@ export const useProviderStore = create((set, get) => ({ const AdaptedEncryptedWebSocket = createAdaptedEncryptedWebsocketClass({ encryptionKey: encryptionSymmetricKey, decryptionKey: encryptionSymmetricKey, + onSystemMessage: (message) => { + if (message === 'system:authenticated') { + set({ isReady: true, isConnected: true }); + } + }, }); provider = new RelayProvider(wsUrl, storeId, doc, { diff --git a/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts b/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts index 4f7e6d9c..db60d61b 100644 --- a/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts +++ b/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts @@ -84,6 +84,9 @@ export const collaborationWSHandler = async ( // Since for "end-to-end encryption" the server cannot maintains its own state for the document // we use a different strategy with a relay server if (document.is_encrypted) { + // mimick the Hocuspocus protocol to properly hide the frontend loader + ws.send('system:authenticated'); + await handleRelayServerConnection(ws, roomId); } else { hocuspocusServer.hocuspocus.handleConnection(ws, req, {