diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py
index 0b5b7608..6f5a0565 100644
--- a/src/backend/core/api/viewsets.py
+++ b/src/backend/core/api/viewsets.py
@@ -1385,6 +1385,14 @@ class DocumentViewSet(
# Check permissions first
document = self.get_object()
+ if document.is_encrypted:
+ raise drf.exceptions.ValidationError(
+ {
+ "detail": "Visibility cannot be changed for encrypted documents. "
+ "Encrypted documents must remain restricted.",
+ }
+ )
+
# Deserialize and validate the data
serializer = serializers.LinkDocumentSerializer(
document, data=request.data, partial=True
diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx
index 79059491..23f9c72a 100644
--- a/src/frontend/apps/impress/src/core/AppProvider.tsx
+++ b/src/frontend/apps/impress/src/core/AppProvider.tsx
@@ -9,6 +9,7 @@ import { useEffect } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
+import { UserEncryptionProvider } from '@/features/docs/doc-collaboration';
import { useResponsiveStore } from '@/stores/';
import { ConfigProvider } from './config/';
@@ -74,7 +75,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
- {children}
+
+ {children}
+
diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/UserEncryptionProvider.tsx b/src/frontend/apps/impress/src/features/docs/doc-collaboration/UserEncryptionProvider.tsx
new file mode 100644
index 00000000..2f331960
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-collaboration/UserEncryptionProvider.tsx
@@ -0,0 +1,39 @@
+import { createContext, useContext } from 'react';
+
+import { useAuth } from '@/features/auth';
+
+import { EncryptionError, useEncryption } from './hook/useEncryption';
+
+interface UserEncryptionContextValue {
+ encryptionLoading: boolean;
+ encryptionSettings: {
+ userId: string;
+ userPrivateKey: CryptoKey;
+ userPublicKey: CryptoKey;
+ } | null;
+ encryptionError: EncryptionError;
+}
+
+const UserEncryptionContext = createContext({
+ encryptionLoading: true,
+ encryptionSettings: null,
+ encryptionError: null,
+});
+
+export const UserEncryptionProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const { user } = useAuth();
+ const value = useEncryption(user?.id);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useUserEncryption = (): UserEncryptionContextValue =>
+ useContext(UserEncryptionContext);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useDocumentEncryption.tsx b/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useDocumentEncryption.tsx
index bbf05f6a..b8081346 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useDocumentEncryption.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useDocumentEncryption.tsx
@@ -2,18 +2,14 @@ import { useEffect, useState } from 'react';
import { decryptSymmetricKey } from '@/docs/doc-collaboration/encryption';
+import { useUserEncryption } from '../UserEncryptionProvider';
+
export type DocumentEncryptionError =
| 'missing_symmetric_key'
| 'decryption_failed'
| null;
export function useDocumentEncryption(
- encryptionLoading: boolean,
- encryptionSettings: {
- userId: string;
- userPrivateKey: CryptoKey;
- userPublicKey: CryptoKey;
- } | null,
isDocumentEncrypted: boolean | undefined,
userEncryptedSymmetricKey: string | undefined,
): {
@@ -23,6 +19,7 @@ export function useDocumentEncryption(
} | null;
documentEncryptionError: DocumentEncryptionError;
} {
+ const { encryptionLoading, encryptionSettings } = useUserEncryption();
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState<{
documentSymmetricKey: CryptoKey;
diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/index.ts b/src/frontend/apps/impress/src/features/docs/doc-collaboration/index.ts
index b1b8f460..0606ebba 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-collaboration/index.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-collaboration/index.ts
@@ -16,5 +16,9 @@ export {
useEncryption,
type EncryptionError,
} from './hook/useEncryption';
+export {
+ UserEncryptionProvider,
+ useUserEncryption,
+} from './UserEncryptionProvider';
export { useKeyFingerprint } from './hook/useKeyFingerprint';
export { usePublicKeyRegistry } from './hook/usePublicKeyRegistry';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx
index e70ebbad..0013659d 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx
@@ -78,7 +78,7 @@ describe('DocEditor', () => {
},
} as any;
- const { rerender } = render(, {
+ const { rerender } = render(, {
wrapper: AppWrapper,
});
@@ -90,7 +90,7 @@ describe('DocEditor', () => {
// Rerender with same doc to check that event is not tracked again
rerender(
- ,
+ ,
);
expect(TrackEventMock).toHaveBeenNthCalledWith(1, {
@@ -107,7 +107,6 @@ describe('DocEditor', () => {
id: 'test-doc-id-2',
computed_link_reach: LinkReach.RESTRICTED,
}}
- encryptionSettings={null}
documentEncryptionSettings={null}
/>,
);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
index 87c9ebf6..b91484f0 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
@@ -76,11 +76,6 @@ export const DocEditorContainer = ({
interface DocEditorProps {
doc: Doc;
- encryptionSettings: {
- userId: string;
- userPrivateKey: CryptoKey;
- userPublicKey: CryptoKey;
- } | null;
documentEncryptionSettings: {
documentSymmetricKey: CryptoKey;
} | null;
@@ -88,7 +83,6 @@ interface DocEditorProps {
export const DocEditor = ({
doc,
- encryptionSettings,
documentEncryptionSettings,
}: DocEditorProps) => {
const { isDesktop } = useResponsiveStore();
@@ -145,7 +139,6 @@ export const DocEditor = ({
docHeader={
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx
index 3577e557..54029d7f 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx
@@ -37,7 +37,7 @@ describe('DocToolBox - Licence', () => {
const { DocToolBox } = await import('../components/DocToolBox');
- render(, {
+ render(, {
wrapper: AppWrapper,
});
const optionsButton = await screen.findByLabelText('Export the document');
@@ -55,7 +55,7 @@ describe('DocToolBox - Licence', () => {
const { DocToolBox } = await import('../components/DocToolBox');
- render(, {
+ render(, {
wrapper: AppWrapper,
});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
index e3fa778d..bba76ad2 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
@@ -20,11 +20,6 @@ import { DocToolBox } from './DocToolBox';
interface DocHeaderProps {
doc: Doc;
- encryptionSettings: {
- userId: string;
- userPrivateKey: CryptoKey;
- userPublicKey: CryptoKey;
- } | null;
documentEncryptionSettings?: {
documentSymmetricKey: CryptoKey;
} | null;
@@ -32,7 +27,6 @@ interface DocHeaderProps {
export const DocHeader = ({
doc,
- encryptionSettings,
documentEncryptionSettings,
}: DocHeaderProps) => {
const { spacingsTokens } = useCunninghamTheme();
@@ -80,7 +74,6 @@ export const DocHeader = ({
{!isDeletedDoc && (
)}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index 4df2590e..1181d7ba 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -31,7 +31,10 @@ import {
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
-import { usePublicKeyRegistry } from '@/docs/doc-collaboration';
+import {
+ usePublicKeyRegistry,
+ useUserEncryption,
+} from '@/docs/doc-collaboration';
import { DocShareModal } from '@/docs/doc-share';
import {
KEY_LIST_DOC_VERSIONS,
@@ -47,11 +50,6 @@ const ModalExport = Export?.ModalExport;
interface DocToolBoxProps {
doc: Doc;
- encryptionSettings: {
- userId: string;
- userPrivateKey: CryptoKey;
- userPublicKey: CryptoKey;
- } | null;
documentEncryptionSettings?: {
documentSymmetricKey: CryptoKey;
} | null;
@@ -59,7 +57,6 @@ interface DocToolBoxProps {
export const DocToolBox = ({
doc,
- encryptionSettings,
documentEncryptionSettings,
}: DocToolBoxProps) => {
const { t } = useTranslation();
@@ -69,6 +66,7 @@ export const DocToolBox = ({
const { isChild, isTopRoot } = useDocUtils(doc);
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
+ const { encryptionSettings } = useUserEncryption();
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
@@ -336,7 +334,6 @@ export const DocToolBox = ({
{isModalEncryptOpen && (
setIsModalEncryptOpen(false)}
onSuccess={() => {
//
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx
index a1199c4e..fae97510 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx
@@ -21,6 +21,7 @@ import {
generateUserKeyPair,
getEncryptionDB,
prepareEncryptedSymmetricKeysForUsers,
+ useUserEncryption,
} from '@/docs/doc-collaboration';
import { createDocAttachment } from '@/docs/doc-editor/api';
import { toBase64 } from '@/docs/doc-editor';
@@ -121,18 +122,12 @@ const encryptRemoteAttachments = async (
interface ModalEncryptDocProps {
doc: Doc;
- encryptionSettings: {
- userId: string;
- userPrivateKey: CryptoKey;
- userPublicKey: CryptoKey;
- } | null;
onClose: () => void;
onSuccess?: (doc: Doc) => void;
}
export const ModalEncryptDoc = ({
doc,
- encryptionSettings,
onClose,
onSuccess,
}: ModalEncryptDocProps) => {
@@ -140,6 +135,7 @@ export const ModalEncryptDoc = ({
const { toast } = useToastProvider();
const { provider } = useProviderStore();
const { user } = useAuth();
+ const { encryptionSettings } = useUserEncryption();
const { mutateAsync: updateUser } = useUserUpdate();
const [isPending, setIsPending] = useState(false);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
index e8cdd0f1..7caf88bf 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
@@ -10,6 +10,7 @@ import {
ButtonCloseModal,
HorizontalSeparator,
Icon,
+ Loading,
Text,
} from '@/components';
import {
@@ -17,7 +18,11 @@ import {
QuickSearchData,
QuickSearchGroup,
} from '@/components/quick-search/';
-import { usePublicKeyRegistry } from '@/docs/doc-collaboration';
+import {
+ useDocumentEncryption,
+ usePublicKeyRegistry,
+ useUserEncryption,
+} from '@/docs/doc-collaboration';
import type { PublicKeyMismatch } from '@/docs/doc-collaboration/hook/usePublicKeyRegistry';
import { Doc } from '@/docs/doc-management';
import { User, useAuth } from '@/features/auth';
@@ -77,6 +82,28 @@ export const DocShareModal = ({
const { isDesktop } = useResponsiveStore();
const { user } = useAuth();
+
+ // When document encryption settings exist they should be passed as prop, on it will use this fallback
+ // that's because in some cases we want them to only be computed at this step (to avoid computing just when listed in a list)
+ const needsDerivation = !documentEncryptionSettings;
+ const { encryptionLoading, encryptionError } = useUserEncryption();
+ const {
+ documentEncryptionLoading,
+ documentEncryptionSettings: derivedEncryptionSettings,
+ documentEncryptionError,
+ } = useDocumentEncryption(
+ needsDerivation ? doc.is_encrypted : undefined,
+ needsDerivation ? doc.encrypted_document_symmetric_key_for_user : undefined,
+ );
+ const effectiveEncryptionSettings =
+ documentEncryptionSettings ?? derivedEncryptionSettings ?? null;
+ const isEncryptionDeriving =
+ needsDerivation && (encryptionLoading || documentEncryptionLoading);
+ const derivedEncryptionError =
+ needsDerivation && doc.is_encrypted
+ ? encryptionError || documentEncryptionError
+ : null;
+
const { mismatches: keyMismatches, acceptNewKey } = usePublicKeyRegistry(
doc.accesses_public_keys_per_user,
user?.id,
@@ -240,125 +267,168 @@ export const DocShareModal = ({
>
{liveAnnouncement}
-
+ {isEncryptionDeriving && }
+ {!isEncryptionDeriving && derivedEncryptionError && (
+
+
+
+ {t('Encryption keys unavailable')}
+
+
+ {t(
+ 'This is an encrypted document, but your current device does not have the required encryption keys to decrypt it.',
+ )}
+
+ {(encryptionError === 'missing_private_key' ||
+ encryptionError === 'missing_public_key') && (
+
+ {t(
+ 'This usually happens when you switch to a new device or browser without restoring your encryption backup.',
+ )}
+
+ )}
+ {documentEncryptionError === 'missing_symmetric_key' && (
+
+ {t(
+ 'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
+ )}
+
+ )}
+ {documentEncryptionError === 'decryption_failed' && (
+
+ {t(
+ 'Your encryption keys could not decrypt this document. This may happen if your keys were recreated. Ask the document owner to share it with you again.',
+ )}
+
+ )}
+
+ )}
+ {!isEncryptionDeriving && !derivedEncryptionError && (
-
- {canShare && selectedUsers.length > 0 && (
-
- {
- setUserQuery('');
- setInputValue('');
- setSelectedUsers([]);
- }}
- />
-
- )}
- {!canViewAccesses && }
-
-
-
- {!canViewAccesses && (
-
-
+
+ {canShare && selectedUsers.length > 0 && (
+
- {t(
- 'You can view this document but need additional access to see its members or modify settings.',
- )}
-
-
-
- )}
- {canViewAccesses && (
- {
- setInputValue(str);
- onFilter(str);
- }}
- inputValue={inputValue}
- showInput={canShare}
- loading={searchUsersQuery.isLoading}
- placeholder={t('Type a name or email')}
- >
- {showInheritedShareContent && (
- access.document.id !== doc.id,
- ) ?? []
- }
+ {
+ setUserQuery('');
+ setInputValue('');
+ setSelectedUsers([]);
+ }}
/>
- )}
- {showMemberSection && isRootDoc && (
-
-
-
-
+ )}
+ {!canViewAccesses && (
+
+ )}
+
+
+
+ {!canViewAccesses && (
+
+
+ {t(
+ 'You can view this document but need additional access to see its members or modify settings.',
+ )}
+
+
+
+ )}
+ {canViewAccesses && (
+ {
+ setInputValue(str);
+ onFilter(str);
+ }}
+ inputValue={inputValue}
+ showInput={canShare}
+ loading={searchUsersQuery.isLoading}
+ placeholder={t('Type a name or email')}
+ >
+ {showInheritedShareContent && (
+ access.document.id !== doc.id,
+ ) ?? []
+ }
+ />
+ )}
+ {showMemberSection && isRootDoc && (
+
+
+
+
+
+ )}
+
+ {!showMemberSection && canShare && (
+
-
- )}
+ )}
+
+ )}
+
+
- {!showMemberSection && canShare && (
-
- )}
-
+
+ {showFooter && (
+
)}
-
-
- {showFooter && }
-
-
+ )}
>
);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx
index 7dc0c8f5..df1c1609 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx
@@ -27,8 +27,12 @@ export const DocShareModalFooter = ({
>
-
-
+ {!doc.is_encrypted && (
+ <>
+
+
+ >
+ )}
page.results.length > 0);
diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
index 1e2c6569..6c9ec097 100644
--- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
+++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
@@ -23,7 +23,7 @@ import {
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
import {
useDocumentEncryption,
- useEncryption,
+ useUserEncryption,
} from '@/features/docs/doc-collaboration';
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
import { useSkeletonStore } from '@/features/skeletons';
@@ -93,17 +93,14 @@ const DocPage = ({ id }: DocProps) => {
},
);
- const { authenticated, user } = useAuth();
+ const { authenticated } = useAuth();
const [doc, setDoc] = useState();
- const { encryptionLoading, encryptionSettings, encryptionError } =
- useEncryption(user?.id);
+ const { encryptionLoading, encryptionError } = useUserEncryption();
const {
documentEncryptionLoading,
documentEncryptionSettings,
documentEncryptionError,
} = useDocumentEncryption(
- encryptionLoading,
- encryptionSettings,
doc?.is_encrypted,
doc?.encrypted_document_symmetric_key_for_user,
);
@@ -357,7 +354,6 @@ const DocPage = ({ id }: DocProps) => {
>