From 40af663aa84b88536462368d620bbb4fadb72038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Rame=CC=81?= Date: Thu, 24 Sep 2026 23:07:02 +0200 Subject: [PATCH] wip --- .../docs/doc-collaboration/encryptionDB.ts | 4 - .../hook/useKeyFingerprint.tsx | 36 ----- .../hook/usePublicKeyRegistry.tsx | 132 --------------- .../features/docs/doc-collaboration/index.ts | 2 - .../doc-header/components/BoutonShare.tsx | 10 +- .../docs/doc-header/components/DocToolBox.tsx | 11 -- .../docs/doc-share/api/useCreateDocAccess.tsx | 5 + .../components/DocShareInvitation.tsx | 3 + .../doc-share/components/DocShareMember.tsx | 66 +++----- .../doc-share/components/DocShareModal.tsx | 153 +++++++++--------- .../doc-share/components/ModalKeyMismatch.tsx | 107 ------------ .../doc-share/components/SearchUserRow.tsx | 58 ++++--- .../docs-grid/components/DocsGridItem.tsx | 8 - .../components/DocsGridItemSharedButton.tsx | 8 +- .../apps/impress/src/i18n/translations.json | 11 +- 15 files changed, 145 insertions(+), 469 deletions(-) delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useKeyFingerprint.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/usePublicKeyRegistry.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/ModalKeyMismatch.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptionDB.ts b/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptionDB.ts index 728ffe2b0..aab6a9083 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptionDB.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-collaboration/encryptionDB.ts @@ -6,7 +6,6 @@ const DB_VERSION = 1; // Store names export const STORE_PRIVATE_KEY = 'privateKey'; export const STORE_PUBLIC_KEY = 'publicKey'; -export const STORE_KNOWN_PUBLIC_KEYS = 'knownPublicKeys'; let dbPromise: Promise | null = null; @@ -24,9 +23,6 @@ export function getEncryptionDB(): Promise { if (!db.objectStoreNames.contains(STORE_PUBLIC_KEY)) { db.createObjectStore(STORE_PUBLIC_KEY); } - if (!db.objectStoreNames.contains(STORE_KNOWN_PUBLIC_KEYS)) { - db.createObjectStore(STORE_KNOWN_PUBLIC_KEYS); - } }, }); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useKeyFingerprint.tsx b/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useKeyFingerprint.tsx deleted file mode 100644 index ce5f873af..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/useKeyFingerprint.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { useVaultClient } from '../vault'; - -/** - * Computes a SHA-256 fingerprint of a base64-encoded public key. - * Returns a formatted hex string like "A1B2 C3D4 E5F6 7890", or null - * if the key is not provided or still computing. - */ -export function useKeyFingerprint( - base64Key: string | null | undefined, -): string | null { - const { client: vaultClient } = useVaultClient(); - const [fingerprint, setFingerprint] = useState(null); - - useEffect(() => { - if (!base64Key || !vaultClient) { - setFingerprint(null); - return; - } - - let cancelled = false; - const raw = Uint8Array.from(atob(base64Key), (c) => c.charCodeAt(0)); - void vaultClient.computeKeyFingerprint(raw.buffer).then((fp) => { - if (!cancelled) { - setFingerprint(vaultClient.formatFingerprint(fp)); - } - }); - - return () => { - cancelled = true; - }; - }, [base64Key, vaultClient]); - - return fingerprint; -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/usePublicKeyRegistry.tsx b/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/usePublicKeyRegistry.tsx deleted file mode 100644 index 45b989a45..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-collaboration/hook/usePublicKeyRegistry.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; - -import { STORE_KNOWN_PUBLIC_KEYS, getEncryptionDB } from '../encryptionDB'; - -export interface PublicKeyMismatch { - userId: string; - knownKey: string; - currentKey: string; -} - -// module-level listener set to keep all hook instances in sync -const registryListeners = new Set<() => void>(); - -function notifyRegistryUpdated() { - registryListeners.forEach((fn) => fn()); -} - -/** - * TOFU (Trust On First Use) public key registry. - * - * - On first encounter, a user's public key is stored locally in IndexedDB. - * - On subsequent encounters, if the key differs from the stored one, it is - * flagged as a mismatch. - * - The caller can accept a new key via `acceptNewKey(userId)`, which updates - * the locally stored key. - * - * All instances stay in sync via a module-level listener set. - */ -export function usePublicKeyRegistry( - accessesPublicKeysPerUser: Record | undefined, - currentUserId?: string, -) { - const [mismatches, setMismatches] = useState([]); - const [loading, setLoading] = useState(true); - const [refreshTrigger, setRefreshTrigger] = useState(0); - - // listen for updates from other hook instances - useEffect(() => { - const handler = () => setRefreshTrigger((prev) => prev + 1); - - registryListeners.add(handler); - - return () => { - registryListeners.delete(handler); - }; - }, []); - - useEffect(() => { - if (!accessesPublicKeysPerUser) { - setMismatches([]); - setLoading(false); - return; - } - - let cancelled = false; - const accesses = accessesPublicKeysPerUser; - - async function checkKeys() { - try { - const db = await getEncryptionDB(); - const newMismatches: PublicKeyMismatch[] = []; - - for (const [userId, currentKey] of Object.entries(accesses)) { - // Skip the current user — they know about their own key changes - if (currentUserId && userId === currentUserId) { - // Still store the key so it stays up to date locally - await db.put(STORE_KNOWN_PUBLIC_KEYS, currentKey, `user:${userId}`); - continue; - } - - const knownKey: string | undefined = await db.get( - STORE_KNOWN_PUBLIC_KEYS, - `user:${userId}`, - ); - - if (!knownKey) { - // First time seeing this user's key — trust on first use - await db.put(STORE_KNOWN_PUBLIC_KEYS, currentKey, `user:${userId}`); - } else if (knownKey !== currentKey) { - newMismatches.push({ userId, knownKey, currentKey }); - } - } - - if (!cancelled) { - setMismatches(newMismatches); - } - } catch (error) { - console.error('usePublicKeyRegistry: failed to check keys', error); - } finally { - if (!cancelled) { - setLoading(false); - } - } - } - - setLoading(true); - void checkKeys(); - - return () => { - cancelled = true; - }; - }, [accessesPublicKeysPerUser, currentUserId, refreshTrigger]); - - const acceptNewKey = useCallback( - async (userId: string) => { - const mismatch = mismatches.find((m) => m.userId === userId); - if (!mismatch) { - return; - } - - const db = await getEncryptionDB(); - await db.put( - STORE_KNOWN_PUBLIC_KEYS, - mismatch.currentKey, - `user:${userId}`, - ); - - setMismatches((prev) => prev.filter((m) => m.userId !== userId)); - - // notify other instances to re-check - notifyRegistryUpdated(); - }, - [mismatches], - ); - - return { - mismatches, - hasMismatches: mismatches.length > 0, - loading, - acceptNewKey, - }; -} 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 c3704aad4..d76b4c03c 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,8 +16,6 @@ export { UserEncryptionProvider, useUserEncryption, } from './UserEncryptionProvider'; -export { useKeyFingerprint } from './hook/useKeyFingerprint'; -export { usePublicKeyRegistry } from './hook/usePublicKeyRegistry'; export { exportPrivateKeyAsJwk, importPrivateKeyFromJwk, diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx index dd9e4dd81..fca6c8213 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx @@ -10,7 +10,6 @@ import { Doc } from '@/docs/doc-management'; interface BoutonShareProps { displayNbAccess: boolean; doc: Doc; - hasKeyWarning?: boolean; isDisabled?: boolean; isHidden?: boolean; open: () => void; @@ -19,7 +18,6 @@ interface BoutonShareProps { export const BoutonShare = ({ displayNbAccess, doc, - hasKeyWarning, isDisabled, isHidden, open, @@ -60,10 +58,9 @@ export const BoutonShare = ({ - {onAcceptKey && ( - - )} - - } - > - - {t( - "This person's encryption key has changed. Verify it before continuing.", - )} - - {(knownFingerprint || currentFingerprint) && ( - - {knownFingerprint && ( - - )} - {currentFingerprint && ( - - )} - - )} - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx index 5d9020aca..5053d0fdc 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx @@ -1,4 +1,6 @@ -import { Box, Icon, Text } from '@/components'; +import { useTranslation } from 'react-i18next'; + +import { Box, BoxButton, Icon, Text } from '@/components'; import { QuickSearchItemContent, QuickSearchItemContentProps, @@ -11,11 +13,12 @@ type Props = { alwaysShowRight?: boolean; right?: QuickSearchItemContentProps['right']; isInvitation?: boolean; - /** A short status ("Verify key", "No encryption") shown as an icon with the text as tooltip. */ + /** A short status ("No encryption") shown as an icon with the text as tooltip. */ suffix?: string; - /** Material icon for the suffix; the shield-with-question mark by default. */ + /** Material icon for the suffix; the crossed shield by default. */ suffixIcon?: string; - onSuffixClick?: () => void; + /** Makes the avatar a button (the person's encryption identity). */ + onAvatarClick?: () => void; }; export const SearchUserRow = ({ @@ -24,9 +27,10 @@ export const SearchUserRow = ({ alwaysShowRight = false, isInvitation = false, suffix, - suffixIcon = 'gpp_maybe', - onSuffixClick, + suffixIcon = 'gpp_bad', + onAvatarClick, }: Props) => { + const { t } = useTranslation(); const hasFullName = !!user.full_name; const { spacingsTokens, colorsTokens } = useCunninghamTheme(); @@ -41,10 +45,31 @@ export const SearchUserRow = ({ $gap={spacingsTokens['xs']} className="--docs--search-user-row" > - + {onAvatarClick ? ( + { + e.stopPropagation(); + e.preventDefault(); + onAvatarClick(); + }} + > + + + ) : ( + + )} @@ -54,19 +79,10 @@ export const SearchUserRow = ({ { - e.stopPropagation(); - onSuffixClick(); - }, - role: 'button', - tabIndex: 0, - style: { cursor: 'pointer' }, - })} /> )} diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx index 4e4c0325f..5325f2b44 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx @@ -7,10 +7,8 @@ import { css } from 'styled-components'; import { Box, Icon, StyledLink, Text } from '@/components'; import { useConfig } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; -import { usePublicKeyRegistry } from '@/docs/doc-collaboration'; import { Doc, LinkReach, SimpleDocItem } from '@/docs/doc-management'; import { DocShareModal } from '@/docs/doc-share'; -import { useAuth } from '@/features/auth'; import { useDate } from '@/hooks'; import { useResponsiveStore } from '@/stores'; @@ -35,11 +33,6 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { const { flexLeft, flexRight } = useResponsiveDocGrid(); const { spacingsTokens } = useCunninghamTheme(); const shareModal = useModal(); - const { user } = useAuth(); - const { hasMismatches: hasKeyWarning } = usePublicKeyRegistry( - undefined, - user?.id, - ); const isPublic = doc.link_reach === LinkReach.PUBLIC; const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED; const isShared = isPublic || isAuthenticated; @@ -188,7 +181,6 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { doc={doc} handleClick={handleShareClick} disabled={isInTrashbin} - hasKeyWarning={hasKeyWarning} /> )} {isInTrashbin ? ( diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx index 70031aec3..0fd0c75ad 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx @@ -8,13 +8,11 @@ type Props = { doc: Doc; handleClick: () => void; disabled: boolean; - hasKeyWarning?: boolean; }; export const DocsGridItemSharedButton = ({ doc, handleClick, disabled, - hasKeyWarning, }: Props) => { const { t } = useTranslation(); const sharedCount = doc.nb_accesses_direct; @@ -46,14 +44,14 @@ export const DocsGridItemSharedButton = ({ event.stopPropagation(); handleClick(); }} - color={hasKeyWarning ? 'warning' : 'brand'} + color="brand" variant="secondary" size="nano" icon={ diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json index 30ad0ca4f..c1d0426fe 100644 --- a/src/frontend/apps/impress/src/i18n/translations.json +++ b/src/frontend/apps/impress/src/i18n/translations.json @@ -1026,24 +1026,17 @@ "The document owner is encrypting this document. Please wait.": "Le propriétaire du document est en train de le chiffrer. Veuillez patienter.", "The document owner is removing encryption from this document. Please wait.": "Le propriétaire du document est en train d'en retirer le chiffrement. Veuillez patienter.", "This document was encrypted with a different key": "Ce document a été chiffré avec une autre clé", - "Verify key": "Vérifier la clé", "No encryption": "Pas de chiffrement", "Encryption required": "Chiffrement requis", "This person has not enabled encryption yet, so the document cannot be shared with them. Ask them to enable encryption first.": "Cette personne n'a pas encore activé le chiffrement, le document ne peut donc pas être partagé avec elle. Demandez-lui d'activer le chiffrement d'abord.", "Understood": "Compris", - "Verify encryption key": "Vérifier la clé de chiffrement", - "This person's encryption key has changed. Verify it before continuing.": "La clé de chiffrement de cette personne a changé. Vérifiez-la avant de continuer.", - "We recommend verifying with this person directly (for example on a call) that they really changed their encryption key before proceeding.": "Nous vous recommandons de vérifier directement avec cette personne (par exemple lors d'un appel) qu'elle a bien changé sa clé de chiffrement avant de continuer.", - "Don't trust": "Ne pas faire confiance", - "Trust": "Faire confiance", - "Previously known:": "Clé connue :", - "Current key:": "Clé actuelle :", "Action needed": "Action requise", "Waiting for them to enable encryption. You will be able to accept them once they have.": "En attente de l'activation du chiffrement par cette personne. Vous pourrez l'accepter une fois que ce sera fait.", "Accept": "Accepter", "Accepting…": "Acceptation…", "Pending encryption": "Chiffrement en attente", - "Encryption settings": "Paramètres de chiffrement" + "Encryption settings": "Paramètres de chiffrement", + "Verify the identity of {{name}}": "Vérifier l'identité de {{name}}" } }, "it": {