wip encryption issue to access doc

This commit is contained in:
Thomas Ramé
2026-03-04 23:38:35 +01:00
parent 205960106b
commit fe34b93249
4 changed files with 166 additions and 39 deletions
@@ -1,7 +1,11 @@
import { useEffect, useState } from 'react';
import { decryptSymmetricKey } from '@/docs/doc-collaboration/encryption';
import assert from 'assert';
export type DocumentEncryptionError =
| 'missing_symmetric_key'
| 'decryption_failed'
| null;
export function useDocumentEncryption(
encryptionLoading: boolean,
@@ -17,11 +21,13 @@ export function useDocumentEncryption(
documentEncryptionSettings: {
documentSymmetricKey: CryptoKey;
} | null;
documentEncryptionError: DocumentEncryptionError;
} {
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState<{
documentSymmetricKey: CryptoKey;
} | null>(null);
const [error, setError] = useState<DocumentEncryptionError>(null);
useEffect(() => {
let cancelled = false;
@@ -35,24 +41,27 @@ export function useDocumentEncryption(
} else if (encryptionLoading || isDocumentEncrypted === undefined) {
setLoading(true);
setSettings(null);
setError(null);
return;
} else if (isDocumentEncrypted === false) {
setLoading(false);
setSettings(null);
setError(null);
return;
}
// TODO:
// TODO: if encrypted but there is no encrypted symmetric key for this user, we should display an error
// TODO: (maybe reuse the catch handler below?)
// TODO:
assert(
userEncryptedSymmetricKey,
'document encrypted symmetric key must exist',
);
if (!userEncryptedSymmetricKey) {
if (!cancelled) {
setError('missing_symmetric_key');
setSettings(null);
setLoading(false);
}
return;
}
try {
setLoading(true);
setError(null);
const userEncryptedSymmetricKeyArrayBuffer = Buffer.from(
userEncryptedSymmetricKey,
@@ -70,18 +79,14 @@ export function useDocumentEncryption(
} catch (err) {
console.error(err);
//
// TODO: this should display a global error since if encryption needed it should able
// to retrieve information (except if onboarding needed, but still...)
//
// maybe this should be a return value so the parent knows where to set the CTA
//
if (!cancelled) {
setError('decryption_failed');
setSettings(null);
}
} finally {
setLoading(false);
if (!cancelled) {
setLoading(false);
}
}
}
@@ -95,5 +100,6 @@ export function useDocumentEncryption(
return {
documentEncryptionLoading: loading,
documentEncryptionSettings: settings,
documentEncryptionError: error,
};
}
@@ -2,6 +2,11 @@ import { useEffect, useState } from 'react';
import { getEncryptionDB } from '../encryptionDB';
export type EncryptionError =
| 'missing_private_key'
| 'missing_public_key'
| null;
export function useEncryption(userId?: string): {
encryptionLoading: boolean;
encryptionSettings: {
@@ -9,6 +14,7 @@ export function useEncryption(userId?: string): {
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
encryptionError: EncryptionError;
} {
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState<{
@@ -16,6 +22,7 @@ export function useEncryption(userId?: string): {
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null>(null);
const [error, setError] = useState<EncryptionError>(null);
const enableEncryption: boolean = true; // TODO: this could be toggled for instances not needing encryption to save some requests
@@ -27,15 +34,18 @@ export function useEncryption(userId?: string): {
if (!userId) {
setLoading(true);
setSettings(null);
setError(null);
return;
} else if (enableEncryption === false) {
setLoading(false);
setSettings(null);
setError(null);
return;
}
try {
setLoading(true);
setError(null);
// We must first retrieve user keys locally
const encryptionDatabase = await getEncryptionDB();
@@ -46,7 +56,11 @@ export function useEncryption(userId?: string): {
);
if (!userPrivateKey) {
throw new Error('user has no local private key (needs onboarding)');
if (!cancelled) {
setError('missing_private_key');
setSettings(null);
}
return;
}
const userPublicKey = await encryptionDatabase.get(
@@ -55,7 +69,11 @@ export function useEncryption(userId?: string): {
);
if (!userPublicKey) {
throw new Error('user is missing his public key');
if (!cancelled) {
setError('missing_public_key');
setSettings(null);
}
return;
}
if (!cancelled) {
@@ -68,18 +86,13 @@ export function useEncryption(userId?: string): {
} catch (err) {
console.error(err);
//
// TODO: this should display a global error since if encryption needed it should able
// to retrieve information (except if onboarding needed, but still...)
//
// maybe this should be a return value so the parent knows where to set the CTA
//
if (!cancelled) {
setSettings(null);
}
} finally {
setLoading(false);
if (!cancelled) {
setLoading(false);
}
}
}
@@ -90,5 +103,9 @@ export function useEncryption(userId?: string): {
};
}, [userId, enableEncryption]);
return { encryptionLoading: loading, encryptionSettings: settings };
return {
encryptionLoading: loading,
encryptionSettings: settings,
encryptionError: error,
};
}
@@ -8,7 +8,13 @@ export {
encryptSymmetricKey,
} from './encryption';
export { getEncryptionDB } from './encryptionDB';
export { useDocumentEncryption } from './hook/useDocumentEncryption';
export { useEncryption } from './hook/useEncryption';
export {
useDocumentEncryption,
type DocumentEncryptionError,
} from './hook/useDocumentEncryption';
export {
useEncryption,
type EncryptionError,
} from './hook/useEncryption';
export { useKeyFingerprint } from './hook/useKeyFingerprint';
export { usePublicKeyRegistry } from './hook/usePublicKeyRegistry';
@@ -5,7 +5,9 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Icon, Loading, TextErrors } from '@/components';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { Box, Icon, Loading, StyledLink, Text, TextErrors } from '@/components';
import { DEFAULT_QUERY_RETRY } from '@/core';
import { DocEditor } from '@/docs/doc-editor';
import {
@@ -93,14 +95,18 @@ const DocPage = ({ id }: DocProps) => {
const { authenticated, user } = useAuth();
const [doc, setDoc] = useState<Doc>();
const { encryptionLoading, encryptionSettings } = useEncryption(user?.id);
const { documentEncryptionLoading, documentEncryptionSettings } =
useDocumentEncryption(
encryptionLoading,
encryptionSettings,
doc?.is_encrypted,
doc?.encrypted_document_symmetric_key_for_user,
);
const { encryptionLoading, encryptionSettings, encryptionError } =
useEncryption(user?.id);
const {
documentEncryptionLoading,
documentEncryptionSettings,
documentEncryptionError,
} = useDocumentEncryption(
encryptionLoading,
encryptionSettings,
doc?.is_encrypted,
doc?.encrypted_document_symmetric_key_for_user,
);
const { setCurrentDoc } = useDocStore();
const { addTask } = useBroadcastStore();
const queryClient = useQueryClient();
@@ -245,6 +251,98 @@ const DocPage = ({ id }: DocProps) => {
return <Loading />;
}
if (doc.is_encrypted && (encryptionError || documentEncryptionError)) {
return (
<Box $align="center" $margin="auto" $gap="md" $padding="2rem">
<Icon iconName="lock" $size="3rem" $theme="warning" />
<Text as="h2" $textAlign="center" $margin="0">
{t('Encryption keys unavailable')}
</Text>
<Box $maxWidth="500px" $gap="sm">
<Text $variation="secondary" $textAlign="center">
{t(
'This is an encrypted document, but your current device does not have the required encryption keys to decrypt it.',
)}
</Text>
{(encryptionError === 'missing_private_key' ||
encryptionError === 'missing_public_key') && (
<Text $variation="secondary" $textAlign="center">
{t(
'This usually happens when you switch to a new device or browser without restoring your encryption backup.',
)}
</Text>
)}
{documentEncryptionError === 'missing_symmetric_key' && (
<Text $variation="secondary" $textAlign="center">
{t(
'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
)}
</Text>
)}
{documentEncryptionError === 'decryption_failed' && (
<Text $variation="secondary" $textAlign="center">
{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.',
)}
</Text>
)}
</Box>
{(encryptionError === 'missing_private_key' ||
encryptionError === 'missing_public_key') && (
<Box $gap="xs" $maxWidth="500px">
<Box
$padding="sm"
$background="#f0f7ff"
$border="1px solid #c5ddf5"
$radius="4px"
$gap="xs"
>
<Text $weight="600" $size="sm">
{t('Restore from backup (recommended)')}
</Text>
<Text $size="sm" $variation="secondary">
{t(
'If you have previously exported your encryption backup, you can restore it in your account settings to regain access to all your encrypted documents.',
)}
</Text>
</Box>
<Box
$padding="sm"
$background="#fff8f0"
$border="1px solid #f5dfc5"
$radius="4px"
$gap="xs"
>
<Text $weight="600" $size="sm">
{t('Recreate encryption keys (not recommended)')}
</Text>
<Text $size="sm" $variation="secondary">
{t(
'Creating new encryption keys means you will lose access to all previously encrypted documents. Document owners will need to share them with you again.',
)}
</Text>
</Box>
</Box>
)}
<StyledLink href="/">
<Button
color="neutral"
icon={
<Icon iconName="home" $withThemeInherited />
}
>
{t('Back to home')}
</Button>
</StyledLink>
</Box>
);
}
return (
<>
<Head>