/* Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { useEffect, useMemo } from "react"; import { logger } from "matrix-js-sdk/lib/logger"; import { setLocalStorageItemReactive, useLocalStorage, } from "../useLocalStorage"; import { getUrlParams, useUrlParams } from "../UrlParams"; import { E2eeType } from "./e2eeType"; import { useClient } from "../ClientContext"; /** * This setter will update the state for all `useRoomSharedKey` hooks * if the password is different from the one in local storage or if its not yet in the local storage. */ export function saveKeyForRoom(roomId: string, password: string): void { if ( localStorage.getItem(getRoomSharedKeyLocalStorageKey(roomId)) !== password ) setLocalStorageItemReactive( getRoomSharedKeyLocalStorageKey(roomId), password, ); } const getRoomSharedKeyLocalStorageKey = (roomId: string): string => `room-shared-key-${roomId}`; /** * An up-to-date shared key for the room. Either from local storage or the value from `setInitialValue`. * @param roomId The room ID we want the shared key for. * @param setInitialValue The value we get from the URL. The hook will overwrite the local storage value with this. * @returns [roomSharedKey, setRoomSharedKey] like a react useState hook. */ const useRoomSharedKey = ( roomId: string, setInitialValue?: string, ): [string | null, setKey: (key: string) => void] => { const [roomSharedKey, setRoomSharedKey] = useLocalStorage( getRoomSharedKeyLocalStorageKey(roomId), ); useEffect(() => { // If setInitialValue is available, update the local storage (usually the password from the url). // This will update roomSharedKey but wont update the returned value since // that already defaults to setInitialValue. if (setInitialValue) setRoomSharedKey(setInitialValue); }, [setInitialValue, setRoomSharedKey]); // make sure we never return the initial null value from `useLocalStorage` return [setInitialValue ?? roomSharedKey, setRoomSharedKey]; }; /** * The shared key for a room, preferring one supplied in the parameters Element * Call was started with over whatever is in local storage. */ function keyForRoom( roomId: string, paramsRoomId: string | null, password: string | null, ): string | null { if (roomId !== paramsRoomId) logger.warn( "requested key for a roomId which is not the current call room id (from the URL)", roomId, paramsRoomId, ); return ( password ?? localStorage.getItem(getRoomSharedKeyLocalStorageKey(roomId)) ); } export function getKeyForRoom(roomId: string): string | null { const { roomId: paramsRoomId, password } = getUrlParams(); return keyForRoom(roomId, paramsRoomId, password); } export type Unencrypted = { kind: E2eeType.NONE }; export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string }; export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT }; export type EncryptionSystem = Unencrypted | SharedSecret | PerParticipantE2EE; export function useRoomEncryptionSystem(roomId: string): EncryptionSystem { const { client } = useClient(); const { roomId: paramsRoomId, password } = useUrlParams(); const [storedPassword] = useRoomSharedKey( // TODO: this passes an already-prefixed key where a room ID is expected, so // the local storage key ends up prefixed twice and never matches what // saveKeyForRoom writes. Preserved as-is here to keep this commit a pure // refactor; the reactive read is effectively dead until it is fixed. getRoomSharedKeyLocalStorageKey(roomId), keyForRoom(roomId, paramsRoomId, password) ?? undefined, ); const room = client?.getRoom(roomId); const e2eeSystem = useMemo(() => { if (!room) return { kind: E2eeType.NONE }; if (storedPassword) return { kind: E2eeType.SHARED_KEY, secret: storedPassword, }; if (room.hasEncryptionStateEvent()) { return { kind: E2eeType.PER_PARTICIPANT }; } return { kind: E2eeType.NONE }; }, [room, storedPassword]); return e2eeSystem; }