diff --git a/src/app/components/ActionUIA.tsx b/src/app/components/ActionUIA.tsx index b9cd122f..cced4f2f 100644 --- a/src/app/components/ActionUIA.tsx +++ b/src/app/components/ActionUIA.tsx @@ -1,33 +1,33 @@ import React, { ReactNode } from 'react'; -import { AuthDict, AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk'; -import { getUIAFlowForStages } from '../utils/matrix-uia'; +import { AuthResponse, AuthType, UIAFlow } from '../utils/matrix-uia'; import { useSupportedUIAFlows, useUIACompleted, useUIAFlow } from '../hooks/useUIAFlows'; import { UIAFlowOverlay } from './UIAFlowOverlay'; import { PasswordStage, SSOStage } from './uia-stages'; -import { useMatrixClient } from '../hooks/useMatrixClient'; +import { useClient } from '../hooks/useClient'; export const SUPPORTED_IN_APP_UIA_STAGES = [AuthType.Password, AuthType.Sso]; -export const pickUIAFlow = (uiaFlows: UIAFlow[]): UIAFlow | undefined => { - const passwordFlow = getUIAFlowForStages(uiaFlows, [AuthType.Password]); - if (passwordFlow) return passwordFlow; - return getUIAFlowForStages(uiaFlows, [AuthType.Sso]); -}; - type ActionUIAProps = { - authData: IAuthData; + authData: AuthResponse; ongoingFlow: UIAFlow; - action: (authDict: AuthDict) => void; + action: (authDict: object) => void; onCancel: () => void; }; export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIAProps) { - const mx = useMatrixClient(); + const ct = useClient(); const completed = useUIACompleted(authData); const { getStageToComplete } = useUIAFlow(authData, ongoingFlow); const stageToComplete = getStageToComplete(); if (!stageToComplete) return null; + + const ssoRedirectURL = new URL(`/auth/${AuthType.Sso}/fallback/web`, ct.session.baseUrl); + + if (stageToComplete.session) { + ssoRedirectURL.searchParams.set('session', stageToComplete.session); + } + return ( {stageToComplete.type === AuthType.Password && ( ReactNode; children: (ongoingFlow: UIAFlow) => ReactNode; }; diff --git a/src/app/components/BackupRestore.tsx b/src/app/components/BackupRestore.tsx index 068f437b..bd6289a3 100644 --- a/src/app/components/BackupRestore.tsx +++ b/src/app/components/BackupRestore.tsx @@ -1,6 +1,4 @@ import React, { MouseEventHandler, useCallback, useState } from 'react'; -import { useAtom } from 'jotai'; -import { CryptoApi, KeyBackupInfo } from 'matrix-js-sdk/lib/crypto-api'; import { Badge, Box, @@ -19,130 +17,113 @@ import { Text, } from 'folds'; import FocusTrap from 'focus-trap-react'; -import { BackupProgressStatus, backupRestoreProgressAtom } from '../state/backupRestore'; import { InfoCard } from './info-card'; -import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { - useKeyBackupInfo, - useKeyBackupStatus, - useKeyBackupSync, - useKeyBackupTrust, -} from '../hooks/useKeyBackup'; import { stopPropagation } from '../utils/keyboard'; -import { useRestoreBackupOnVerification } from '../hooks/useRestoreBackupOnVerification'; +import { bytesToSize } from '../utils/common'; +import { useObservableValue } from '../hooks/useObservableValue'; +import { useClient } from '../hooks/useClient'; +import { AsyncStatus, useAsyncCallbackValue } from '../hooks/useAsyncCallback'; type BackupStatusProps = { - enabled: boolean; + active: boolean; }; -function BackupStatus({ enabled }: BackupStatusProps) { +function BackupStatus({ active }: BackupStatusProps) { return ( - + - {enabled ? 'Connected' : 'Disconnected'} + {active ? 'Connected' : 'Disconnected'} ); } -type BackupSyncingProps = { - count: number; -}; -function BackupSyncing({ count }: BackupSyncingProps) { + +function BackupSyncing() { + const ct = useClient(); + const [countState] = useAsyncCallbackValue(useCallback(() => ct.machine.roomKeys.count(), [ct])); + + const count = countState.status === AsyncStatus.Success ? countState.data : undefined; + + const remaining = count ? count.total - count.backedUp : undefined; + return ( - Syncing ({count}) + Syncing ({remaining ?? '...'}) ); } -function BackupProgressFetching() { +function BackupProgressFetching({ + receivedBytes, + totalBytes, +}: { + receivedBytes: number; + totalBytes?: number; +}) { + const receviedStr = bytesToSize(receivedBytes); + const totalStr = totalBytes ? bytesToSize(totalBytes) : undefined; + return ( - Restoring: 0% + Downloading - + - + + {totalStr ? `${receivedBytes} / ${totalStr}` : receviedStr} + ); } type BackupProgressProps = { - total: number; - downloaded: number; + total?: number; + imported: number; }; -function BackupProgress({ total, downloaded }: BackupProgressProps) { +function BackupProgress({ total, imported }: BackupProgressProps) { return ( - Restoring: {`${Math.round(percent(0, total, downloaded))}%`} + + Restoring: {total ? `${Math.round(percent(0, total, imported))}%` : '0%'} + - + - - - {downloaded} / {total} - - - - ); -} - -type BackupTrustInfoProps = { - crypto: CryptoApi; - backupInfo: KeyBackupInfo; -}; -function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) { - const trust = useKeyBackupTrust(crypto, backupInfo); - - if (!trust) return null; - - return ( - - {trust.matchesDecryptionKey ? ( - - Backup has trusted decryption key. - - ) : ( - - Backup does not have trusted decryption key! - - )} - {trust.trusted ? ( - - Backup has trusted by signature. - - ) : ( - - Backup does not have trusted signature! - + {total && ( + + + {imported} / {total} + + )} ); } -type BackupRestoreTileProps = { - crypto: CryptoApi; -}; -export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) { - const [restoreProgress, setRestoreProgress] = useAtom(backupRestoreProgressAtom); - const restoring = - restoreProgress.status === BackupProgressStatus.Fetching || - restoreProgress.status === BackupProgressStatus.Loading; - - const backupEnabled = useKeyBackupStatus(crypto); - const backupInfo = useKeyBackupInfo(crypto); - const [remainingSession, syncFailure] = useKeyBackupSync(); +export function BackupRestoreTile() { + const ct = useClient(); + const backupInfo = useObservableValue(ct.backupInfo); + const keyBackup = useObservableValue(ct.megolmBackup.keyBackup); + const restoreState = useObservableValue(ct.megolmBackup.restoreState); + const backupInProgress = useObservableValue(ct.megolmBackup.backupInProgress); + const restoring = restoreState.type === 'download' || restoreState.type === 'import'; const [menuCords, setMenuCords] = useState(); @@ -150,19 +131,11 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; - const [restoreState, restoreBackup] = useAsyncCallback( - useCallback(async () => { - await crypto.restoreKeyBackup({ - progressCallback(progress) { - setRestoreProgress(progress); - }, - }); - }, [crypto, setRestoreProgress]) - ); - const handleRestore = () => { setMenuCords(undefined); - restoreBackup(); + if (keyBackup) { + ct.megolmBackup.restore(keyBackup); + } }; return ( @@ -171,111 +144,99 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) { title="Encryption Backup" after={ - {remainingSession === 0 ? ( - - ) : ( - - )} - - - - setMenuCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} + {backupInfo && ( + <> + {backupInProgress ? : } + - - - - - Version: {backupInfo?.version ?? 'NIL'} -
- Keys: {backupInfo?.count ?? 'NIL'} - - } - /> -
- -
-
- - } - /> + +
+ setMenuCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + + + + + Version: {backupInfo.version} +
+ Keys: {backupInfo.count} + + } + /> +
+ +
+
+ + } + /> + + )}
} > - {syncFailure && ( + {!backupInfo && ( - {syncFailure} + No key backup has been configured. )} - {!backupEnabled && backupInfo === null && ( + + {backupInfo && !keyBackup && ( - No backup present on server! + Cannot start key backup because the backup could not be verified. )} - {!syncFailure && !backupEnabled && backupInfo && ( - - )} - {restoreState.status === AsyncStatus.Loading && !restoring && } - {restoreProgress.status === BackupProgressStatus.Fetching && } - {restoreProgress.status === BackupProgressStatus.Loading && ( - )} - {restoreState.status === AsyncStatus.Error && ( + {restoreState.type === 'import' && ( + + )} + {restoreState.type === 'error' && ( - {restoreState.error.message} + {restoreState.error.data.error ?? restoreState.error.data.errcode} )} ); } - -export function AutoRestoreBackupOnVerification() { - useRestoreBackupOnVerification(); - - return null; -} diff --git a/src/app/components/DeviceVerificationStatus.ts b/src/app/components/DeviceVerificationStatus.ts index e8a6b0c1..3f168313 100644 --- a/src/app/components/DeviceVerificationStatus.ts +++ b/src/app/components/DeviceVerificationStatus.ts @@ -1,24 +1,21 @@ import { ReactNode } from 'react'; -import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api'; import { useDeviceVerificationStatus, VerificationStatus, } from '../hooks/useDeviceVerificationStatus'; type DeviceVerificationStatusProps = { - crypto?: CryptoApi; userId: string; deviceId: string; children: (verificationStatus: VerificationStatus) => ReactNode; }; export function DeviceVerificationStatus({ - crypto, userId, deviceId, children, }: DeviceVerificationStatusProps) { - const status = useDeviceVerificationStatus(crypto, userId, deviceId); + const status = useDeviceVerificationStatus(userId, deviceId); return children(status); } diff --git a/src/app/components/LogoutDialog.tsx b/src/app/components/LogoutDialog.tsx index 9fd9cdef..42206ead 100644 --- a/src/app/components/LogoutDialog.tsx +++ b/src/app/components/LogoutDialog.tsx @@ -2,6 +2,12 @@ import React, { forwardRef, useCallback } from 'react'; import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { useClient } from '../hooks/useClient'; +import { InfoCard } from './info-card'; +import { useCrossSigning } from '../hooks/useCrossSigning'; +import { + useDeviceVerificationStatus, + VerificationStatus, +} from '../hooks/useDeviceVerificationStatus'; type LogoutDialogProps = { handleClose: () => void; @@ -9,6 +15,10 @@ type LogoutDialogProps = { export const LogoutDialog = forwardRef( ({ handleClose }, ref) => { const ct = useClient(); + const hasEncryptedRoom = ct.encryptedRooms.read.size > 0; + const crossSigning = useCrossSigning(); + + const verificationStatus = useDeviceVerificationStatus(ct.session.userId, ct.session.deviceId); const [logoutState, logout] = useAsyncCallback( useCallback(async () => { @@ -33,8 +43,8 @@ export const LogoutDialog = forwardRef( - {/* {hasEncryptedRoom && - (crossSigningActive ? ( + {hasEncryptedRoom && + (crossSigning ? ( verificationStatus === VerificationStatus.Unverified && ( ( title="Alert" description="Enable device verification or export your encrypted data from settings to avoid losing access to your messages." /> - ))} */} + ))} You’re about to log out. Are you sure? {logoutState.status === AsyncStatus.Error && ( diff --git a/src/app/components/ManualVerification.tsx b/src/app/components/ManualVerification.tsx index f7cde92b..fdb8b94d 100644 --- a/src/app/components/ManualVerification.tsx +++ b/src/app/components/ManualVerification.tsx @@ -13,13 +13,15 @@ import { color, } from 'folds'; import FocusTrap from 'focus-trap-react'; +import { AesHmacSha2, AesHmacSha2KeyDescription } from 'neurone/lib/crypto/ssss'; +import { parseMatrixResp } from 'neurone/lib/matrix-request'; import { stopPropagation } from '../utils/keyboard'; import { SettingTile } from './setting-tile'; -import { SecretStorageKeyContent } from '../../types/matrix/accountData'; import { SecretStorageRecoveryKey, SecretStorageRecoveryPassphrase } from './SecretStorage'; -import { useMatrixClient } from '../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { storePrivateKey } from '../../client/secretStorageKeys'; +import { useClient } from '../hooks/useClient'; +import { getCrossSigningPrivateKeys, useCrossSigningSecretData } from '../hooks/useCrossSigning'; +import { AccountDataType } from '../../types/matrix/accountData'; export enum ManualVerificationMethod { RecoveryPassphrase = 'passphrase', @@ -112,7 +114,7 @@ export function ManualVerificationMethodSwitcher({ type ManualVerificationTileProps = { secretStorageKeyId: string; - secretStorageKeyContent: SecretStorageKeyContent; + secretStorageKeyContent: AesHmacSha2KeyDescription; options?: ReactNode; }; export function ManualVerificationTile({ @@ -120,7 +122,8 @@ export function ManualVerificationTile({ secretStorageKeyContent, options, }: ManualVerificationTileProps) { - const mx = useMatrixClient(); + const ct = useClient(); + const csSecretData = useCrossSigningSecretData(secretStorageKeyId); const hasPassphrase = !!secretStorageKeyContent.passphrase; const [method, setMethod] = useState( @@ -130,25 +133,53 @@ export function ManualVerificationTile({ ); const verifyAndRestoreBackup = useCallback( - async (recoveryKey: Uint8Array) => { - const crypto = mx.getCrypto(); - if (!crypto) { - throw new Error('Unexpected Error! Crypto object not found.'); + async (recoveryKey: Uint8Array) => { + const status = await ct.machine.crossSigning.getStatus(); + const machineHasKeys = status.master && status.self && status.user; + + if (!machineHasKeys && csSecretData) { + const crossSigningKeys = await getCrossSigningPrivateKeys(recoveryKey, csSecretData); + + const s = await ct.machine.crossSigning.importKeys(crossSigningKeys); + if (!s.master || !s.self || !s.user) { + throw new Error('Failed to import to CrossSigningSecrets keys!'); + } } - storePrivateKey(secretStorageKeyId, recoveryKey); + const { userId, deviceId } = ct.session; + const device = await ct.machine.userTracker.getDevice(userId, deviceId); + if (device && !device.crossSigningTrusted) { + const resp = await device.verify(); + const [err] = await parseMatrixResp(resp); + if (err) { + throw new Error(err.data.error ?? 'Failed to verify. Signature upload request failed.'); + } - await crypto.bootstrapCrossSigning({}); - await crypto.bootstrapSecretStorage({}); + // device.verify uploads signatures to mark device as verifed + // query keys to receive signs now instead of update via /sync + // if we don't do that device remains unverified until next sync device_list change + await ct.machine.outgoingRequests.queryKeys([ct.session.userId]); + } - await crypto.loadSessionBackupPrivateKeyFromSecretStorage(); + const backupKeyEncryptedData = ct.secrets.megolmBackup.read?.get(secretStorageKeyId); + if (!backupKeyEncryptedData) { + return; + } + const backupKey = await AesHmacSha2.decrypt( + recoveryKey, + AccountDataType.MegolmBackupV1, + backupKeyEncryptedData + ); + await ct.megolmBackup.saveDecryptionKey(backupKey); }, - [mx, secretStorageKeyId] + [ct, secretStorageKeyId, csSecretData] ); - const [verifyState, handleDecodedRecoveryKey] = useAsyncCallback( - verifyAndRestoreBackup - ); + const [verifyState, handleDecodedRecoveryKey] = useAsyncCallback< + void, + Error, + [Uint8Array] + >(verifyAndRestoreBackup); const verifying = verifyState.status === AsyncStatus.Loading; return ( @@ -174,7 +205,7 @@ export function ManualVerificationTile({ {method === ManualVerificationMethod.RecoveryKey && ( )} @@ -182,8 +213,8 @@ export function ManualVerificationTile({ secretStorageKeyContent.passphrase && ( )} diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index 423fb1ff..3e93b213 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -12,7 +12,6 @@ import { MSticker, MText, MVideo, - RedactedContent, UnsupportedContent, } from './message'; import { MsgType } from '../../types/matrix/common'; @@ -31,7 +30,6 @@ type RenderMessageContentProps = { htmlReactParserOptions: HTMLReactParserOptions; linkifyOpts: Opts; outlineAttachment?: boolean; - redacted?: boolean; }; export function RenderMessageContent({ displayName, @@ -45,10 +43,7 @@ export function RenderMessageContent({ htmlReactParserOptions, linkifyOpts, outlineAttachment, - redacted, }: RenderMessageContentProps) { - if (redacted) return ; - if (eventType === MessageEvent.Sticker) { return ( void; + keyDescription: AesHmacSha2KeyDescription; + passphraseInfo: PassphraseInfo; + onDecodedRecoveryKey: (recoveryKey: Uint8Array) => void; }; export function SecretStorageRecoveryPassphrase({ processing, - keyContent, - passphraseContent, + keyDescription, + passphraseInfo, onDecodedRecoveryKey, }: SecretStorageRecoveryPassphraseProps) { - const mx = useMatrixClient(); const alive = useAlive(); const [driveKeyState, submitPassphrase] = useAsyncCallback< - Uint8Array, + Uint8Array, Error, - Parameters + [passphrase: string] >( useCallback( - async (passphrase, salt, iterations, bits) => { - const decodedRecoveryKey = await deriveRecoveryKeyFromPassphrase( - passphrase, - salt, - iterations, - bits + async (passphrase) => { + const decodedRecoveryKey = await Passphrase.derive(passphrase, passphraseInfo); + + const { mac: regeneratedMac } = await AesHmacSha2.driveKeyMetaData( + decodedRecoveryKey, + keyDescription.iv ); - const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any); - - if (!match) { + if (regeneratedMac !== keyDescription.mac) { throw new Error('Invalid recovery passphrase.'); } return decodedRecoveryKey; }, - [mx, keyContent] + [keyDescription, passphraseInfo] ) ); @@ -64,8 +62,7 @@ export function SecretStorageRecoveryPassphrase({ const recoveryPassphrase = recoveryPassphraseInput.value.trim(); if (!recoveryPassphrase) return; - const { salt, iterations, bits } = passphraseContent; - submitPassphrase(recoveryPassphrase, salt, iterations, bits).then((decodedRecoveryKey) => { + submitPassphrase(recoveryPassphrase).then((decodedRecoveryKey) => { if (alive()) { recoveryPassphraseInput.value = ''; onDecodedRecoveryKey(decodedRecoveryKey); @@ -115,31 +112,37 @@ export function SecretStorageRecoveryPassphrase({ type SecretStorageRecoveryKeyProps = { processing?: boolean; - keyContent: SecretStorageKeyContent; - onDecodedRecoveryKey: (recoveryKey: Uint8Array) => void; + keyDescription: AesHmacSha2KeyDescription; + onDecodedRecoveryKey: (recoveryKey: Uint8Array) => void; }; export function SecretStorageRecoveryKey({ processing, - keyContent, + keyDescription, onDecodedRecoveryKey, }: SecretStorageRecoveryKeyProps) { - const mx = useMatrixClient(); const alive = useAlive(); - const [driveKeyState, submitRecoveryKey] = useAsyncCallback( + const [driveKeyState, submitRecoveryKey] = useAsyncCallback< + Uint8Array, + Error, + [string] + >( useCallback( async (recoveryKey) => { const decodedRecoveryKey = decodeRecoveryKey(recoveryKey); - const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any); + const { mac: regeneratedMac } = await AesHmacSha2.driveKeyMetaData( + decodedRecoveryKey, + keyDescription.iv + ); - if (!match) { + if (regeneratedMac !== keyDescription.mac) { throw new Error('Invalid recovery key.'); } return decodedRecoveryKey; }, - [mx, keyContent] + [keyDescription] ) ); diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index d0a99a8d..f3bc3c5f 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -16,6 +16,7 @@ import { RenderMessageContent } from '../RenderMessageContent'; import { Time } from './Time'; import { BubbleLayout, CompactLayout, MessageBase, ModernLayout } from './layout'; import { MessageLayout, MessageSpacing } from '../../state/settings'; +import { RedactedContent } from './MsgTypeRenderers'; type ReplyMessageProps = { username?: ReactNode; @@ -118,6 +119,8 @@ export function Reply({ const usernameColor = legacyUsernameColor ? colorMXID(sender ?? eventId) : tagColor; + const redactedEvent = redacted || !!rUnsigned.redactedBecause(replyEvent?.unsigned); + return ( {replyEvent !== undefined ? ( - {replyEvent === null ? ( - - - - ) : ( - - )} + {redactedEvent && } + {!redactedEvent && + (replyEvent === null ? ( + + + + ) : ( + + ))} ) : ( - {(editedEvent ?? event).type === MessageEvent.RoomMessageEncrypted ? ( - - {(decryptedEvent) => ( - - )} - - ) : ( - - )} + {redacted.has(event.event_id) && } + {!redacted.has(event.event_id) && + ((editedEvent ?? event).type === MessageEvent.RoomMessageEncrypted ? ( + + {(decryptedEvent) => ( + + )} + + ) : ( + + ))} {reactionJSX} {threadTimeline && ( diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx index 71b684f5..892dadea 100644 --- a/src/app/features/settings/devices/DeviceTile.tsx +++ b/src/app/features/settings/devices/DeviceTile.tsx @@ -1,4 +1,4 @@ -import React, { FormEventHandler, ReactNode, useCallback, useEffect, useState } from 'react'; +import React, { FormEventHandler, ReactNode, useCallback, useState } from 'react'; import { Box, Text, @@ -15,11 +15,11 @@ import { OverlayBackdrop, OverlayCenter, } from 'folds'; -import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api'; import FocusTrap from 'focus-trap-react'; -import { IMyDevice, MatrixError } from 'matrix-js-sdk'; +import { Device } from 'neurone/lib/api/types'; +import { putDevice } from 'neurone/lib/api/matrix/client/v3/devices'; +import { parseMatrixResp } from 'neurone/lib/matrix-request'; import { SettingTile } from '../../../components/setting-tile'; -import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils/time'; import { BreakWord } from '../../../styles/Text.css'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; @@ -29,6 +29,8 @@ import { LogoutDialog } from '../../../components/LogoutDialog'; import { stopPropagation } from '../../../utils/keyboard'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; +import { useClient } from '../../../hooks/useClient'; +import { useAlive } from '../../../hooks/useAlive'; export function DeviceTilePlaceholder() { return ( @@ -61,7 +63,7 @@ function DeviceActiveTime({ ts }: { ts: number }) { ); } -function DeviceDetails({ device }: { device: IMyDevice }) { +function DeviceDetails({ device }: { device: Device }) { return ( <> {typeof device.device_id === 'string' && ( @@ -78,58 +80,52 @@ function DeviceDetails({ device }: { device: IMyDevice }) { ); } -type DeviceKeyDetailsProps = { - crypto: CryptoApi; -}; -export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) { - const [keysState, loadKeys] = useAsyncCallback( - useCallback(() => { - const keys = crypto.getOwnDeviceKeys(); - return keys; - }, [crypto]) - ); - - useEffect(() => { - loadKeys(); - }, [loadKeys]); - - if (keysState.status === AsyncStatus.Error) return null; +export function DeviceKeyDetails() { + const ct = useClient(); return ( - - Device Key:{' '} - {keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : 'loading...'} - + <> + + Encryption Key: {ct.machine.getEncryptionKey()} + + + Singing Key: {ct.machine.getSigningKey()} + + ); } type DeviceRenameProps = { - device: IMyDevice; + device: Device; onCancel: () => void; onRename: () => void; refreshDeviceList: () => Promise; }; function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceRenameProps) { - const mx = useMatrixClient(); + const ct = useClient(); + const alive = useAlive(); - const [renameState, rename] = useAsyncCallback( + const [renameState, rename] = useAsyncCallback( useCallback( async (name: string) => { - await mx.setDeviceDetails(device.device_id, { display_name: name }); + const resp = await ct.requestAuthJSON( + putDevice({ + pathParams: { deviceId: device.device_id }, + body: { + display_name: name, + }, + }) + ); + const [err] = await parseMatrixResp(resp); + if (err) throw new Error(err.data.error ?? err.data.errcode); await refreshDeviceList(); }, - [mx, device.device_id, refreshDeviceList] + [ct, device.device_id, refreshDeviceList] ) ); const renaming = renameState.status === AsyncStatus.Loading; - useEffect(() => { - if (renameState.status === AsyncStatus.Success) { - onRename(); - } - }, [renameState, onRename]); - const handleSubmit: FormEventHandler = (evt) => { evt.preventDefault(); if (renaming) return; @@ -140,7 +136,11 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR const deviceName = nameInput.value.trim(); if (!deviceName || deviceName === device.display_name) return; - rename(deviceName); + rename(deviceName).then(() => { + if (alive()) { + onRename(); + } + }); }; return ( @@ -186,7 +186,7 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR {renameState.status === AsyncStatus.Error ? ( - {renameState.error.message} + {renameState.error instanceof Error ? renameState.error.message : 'Unknown Error'} ) : ( Device names are visible to public. @@ -260,7 +260,7 @@ export function DeviceDeleteBtn({ } type DeviceTileProps = { - device: IMyDevice; + device: Device; deleted?: boolean; refreshDeviceList: () => Promise; disabled?: boolean; diff --git a/src/app/features/settings/devices/Devices.tsx b/src/app/features/settings/devices/Devices.tsx index c957ab31..8ebcf361 100644 --- a/src/app/features/settings/devices/Devices.tsx +++ b/src/app/features/settings/devices/Devices.tsx @@ -5,10 +5,8 @@ import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList'; -import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { LocalBackup } from './LocalBackup'; import { DeviceLogoutBtn, DeviceKeyDetails, DeviceTile, DeviceTilePlaceholder } from './DeviceTile'; -import { OtherDevices } from './OtherDevices'; import { DeviceVerificationOptions, EnableVerification, @@ -24,8 +22,11 @@ import { useSecretStorageDefaultKeyId, useSecretStorageKeyContent, } from '../../../hooks/useSecretStorage'; -import { useCrossSigningActive } from '../../../hooks/useCrossSigning'; +import { useCrossSigning } from '../../../hooks/useCrossSigning'; import { BackupRestoreTile } from '../../../components/BackupRestore'; +import { useClient } from '../../../hooks/useClient'; +import { OtherDevices } from './OtherDevices'; +import { useObservableValue } from '../../../hooks/useObservableValue'; function DevicesPlaceholder() { return ( @@ -40,24 +41,20 @@ type DevicesProps = { requestClose: () => void; }; export function Devices({ requestClose }: DevicesProps) { - const mx = useMatrixClient(); - const crypto = mx.getCrypto(); - const crossSigningActive = useCrossSigningActive(); + const ct = useClient(); + const crossSigning = useCrossSigning(); + const keyBackup = useObservableValue(ct.megolmBackup.keyBackup); + const [devices, refreshDeviceList] = useDeviceList(); const [currentDevice, otherDevices] = useSplitCurrentDevice(devices); const verificationStatus = useDeviceVerificationStatus( - crypto, - mx.getSafeUserId(), + ct.session.userId, currentDevice?.device_id ); const otherDevicesId = useDeviceIds(otherDevices); - const unverifiedDeviceCount = useUnverifiedDeviceCount( - crypto, - mx.getSafeUserId(), - otherDevicesId - ); + const unverifiedDeviceCount = useUnverifiedDeviceCount(ct.session.userId, otherDevicesId); const defaultSecretStorageKeyId = useSecretStorageDefaultKeyId(); const defaultSecretStorageKeyContent = useSecretStorageKeyContent( @@ -96,18 +93,17 @@ export function Devices({ requestClose }: DevicesProps) { title="Device Verification" description="To verify device identity and grant access to encrypted messages." after={ - <> - - {crossSigningActive && ( - - - - - )} - + crossSigning ? ( + + + + + ) : ( + + ) } /> @@ -126,9 +122,9 @@ export function Devices({ requestClose }: DevicesProps) { refreshDeviceList={refreshDeviceList} options={} > - {crypto && } + - {crossSigningActive && + {crossSigning && verificationStatus === VerificationStatus.Unverified && defaultSecretStorageKeyId && defaultSecretStorageKeyContent && ( @@ -137,9 +133,7 @@ export function Devices({ requestClose }: DevicesProps) { secretStorageKeyContent={defaultSecretStorageKeyContent} /> )} - {crypto && verificationStatus === VerificationStatus.Verified && ( - - )} + {keyBackup && } ) : ( @@ -151,7 +145,7 @@ export function Devices({ requestClose }: DevicesProps) { devices={otherDevices} refreshDeviceList={refreshDeviceList} showVerification={ - crossSigningActive && verificationStatus === VerificationStatus.Verified + crossSigning && verificationStatus === VerificationStatus.Verified } /> )} diff --git a/src/app/features/settings/devices/OtherDevices.tsx b/src/app/features/settings/devices/OtherDevices.tsx index 4bd83dd6..8c491a98 100644 --- a/src/app/features/settings/devices/OtherDevices.tsx +++ b/src/app/features/settings/devices/OtherDevices.tsx @@ -1,12 +1,13 @@ import React, { useCallback, useState } from 'react'; import { Box, Button, config, Menu, Spinner, Text } from 'folds'; -import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk'; +import { MatrixResponseResult, parseMatrixResp } from 'neurone/lib/matrix-request'; +import { Device } from 'neurone/lib/api/types'; +import { postDeleteDevices } from 'neurone/lib/api/matrix/client/v3/deleteDevices'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA'; import { DeviceDeleteBtn, DeviceTile } from './DeviceTile'; import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback'; -import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useUIAMatrixError } from '../../../hooks/useUIAFlows'; import { DeviceVerificationStatus } from '../../../components/DeviceVerificationStatus'; import { VerifyOtherDeviceTile } from './Verification'; @@ -15,15 +16,15 @@ import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; import { withSearchParam } from '../../../pages/pathUtils'; import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; import { SettingTile } from '../../../components/setting-tile'; +import { useClient } from '../../../hooks/useClient'; type OtherDevicesProps = { - devices: IMyDevice[]; + devices: Device[]; refreshDeviceList: () => Promise; showVerification?: boolean; }; export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) { - const mx = useMatrixClient(); - const crypto = mx.getCrypto(); + const ct = useClient(); const authMetadata = useAuthMetadata(); const accountManagementActions = useAccountManagementActions(); @@ -69,31 +70,40 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O }); }, []); - const [deleteState, setDeleteState] = useState>({ + const [deleteState, setDeleteState] = useState< + AsyncState, unknown> + >({ status: AsyncStatus.Idle, }); const deleteDevices = useAsync( useCallback( - async (authDict?: AuthDict) => { - await mx.deleteMultipleDevices(Array.from(deleted), authDict); + async (authDict?: object) => { + const resp = await ct.requestAuthJSON( + postDeleteDevices({ body: { auth: authDict, devices: Array.from(deleted) } }) + ); + return parseMatrixResp(resp); }, - [mx, deleted] + [ct, deleted] ), useCallback( (state: typeof deleteState) => { if (state.status === AsyncStatus.Success) { - setDeleted(new Set()); - refreshDeviceList(); + const [err] = state.data; + if (!err) { + setDeleted(new Set()); + refreshDeviceList(); + } } setDeleteState(state); }, [refreshDeviceList] ) ); - const [authData, deleteError] = useUIAMatrixError( - deleteState.status === AsyncStatus.Error ? deleteState.error : undefined - ); + + const deleteError = deleteState.status === AsyncStatus.Error ? deleteState.error : undefined; + const [deleteRespError] = deleteState.status === AsyncStatus.Success ? deleteState.data : []; + const [authData, deleteMatrixError] = useUIAMatrixError(deleteRespError); const deleting = deleteState.status === AsyncStatus.Loading || authData !== undefined; const handleCancelDelete = () => setDeleted(new Set()); @@ -166,14 +176,10 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O } /> {showVerification && crypto && ( - + {(status) => status === VerificationStatus.Unverified && ( - + ) } @@ -196,9 +202,14 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O > - {deleteError ? ( + {deleteError || deleteMatrixError ? ( - Failed to logout devices! Please try again. {deleteError.message} + + Failed to logout devices! Please try again.{' '} + {deleteError instanceof Error + ? deleteError.message + : deleteMatrixError?.data.error ?? 'Unkonwn Error'} + ) : ( diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx index 6c7eab17..79aacb04 100644 --- a/src/app/features/settings/devices/Verification.tsx +++ b/src/app/features/settings/devices/Verification.tsx @@ -19,13 +19,12 @@ import { MenuItem, } from 'folds'; import FocusTrap from 'focus-trap-react'; -import { CryptoApi, VerificationRequest } from 'matrix-js-sdk/lib/crypto-api'; +import { InboundVerification, OutboundVerification } from 'neurone/lib/crypto/machine/verification'; +import { AesHmacSha2KeyDescription } from 'neurone/lib/crypto/ssss'; import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus'; import { InfoCard } from '../../../components/info-card'; import { ManualVerificationTile } from '../../../components/ManualVerification'; -import { SecretStorageKeyContent } from '../../../../types/matrix/accountData'; import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback'; -import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { DeviceVerification } from '../../../components/DeviceVerification'; import { DeviceVerificationReset, @@ -35,6 +34,7 @@ import { stopPropagation } from '../../../utils/keyboard'; import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; import { withSearchParam } from '../../../pages/pathUtils'; import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; +import { useClient } from '../../../hooks/useClient'; type VerificationStatusBadgeProps = { verificationStatus: VerificationStatus; @@ -98,7 +98,7 @@ function LearnStartVerificationFromOtherDevice() { type VerifyCurrentDeviceTileProps = { secretStorageKeyId: string; - secretStorageKeyContent: SecretStorageKeyContent; + secretStorageKeyContent: AesHmacSha2KeyDescription; }; export function VerifyCurrentDeviceTile({ secretStorageKeyId, @@ -163,20 +163,25 @@ export function VerifyCurrentDeviceTile({ } type VerifyOtherDeviceTileProps = { - crypto: CryptoApi; deviceId: string; }; -export function VerifyOtherDeviceTile({ crypto, deviceId }: VerifyOtherDeviceTileProps) { - const mx = useMatrixClient(); - const [requestState, setRequestState] = useState>({ +export function VerifyOtherDeviceTile({ deviceId }: VerifyOtherDeviceTileProps) { + const ct = useClient(); + const [requestState, setRequestState] = useState< + AsyncState + >({ status: AsyncStatus.Idle, }); - const requestVerification = useAsync( - useCallback(() => { - const requestPromise = crypto.requestDeviceVerification(mx.getSafeUserId(), deviceId); - return requestPromise; - }, [mx, crypto, deviceId]), + const requestVerification = useAsync( + useCallback(async () => { + const r = await ct.machine.verificationManager.requestDeviceVerification( + ct.session.userId, + deviceId, + ['m.sas.v1'] + ); + return r; + }, [ct, deviceId]), setRequestState ); @@ -217,23 +222,18 @@ export function VerifyOtherDeviceTile({ crypto, deviceId }: VerifyOtherDeviceTil ); } -type EnableVerificationProps = { - visible: boolean; -}; -export function EnableVerification({ visible }: EnableVerificationProps) { +export function EnableVerification() { const [open, setOpen] = useState(false); const handleCancel = useCallback(() => setOpen(false), []); return ( <> - {visible && ( - - )} + {open && ( }> diff --git a/src/app/hooks/useCrossSigning.ts b/src/app/hooks/useCrossSigning.ts index e3dc2c25..6f49e04f 100644 --- a/src/app/hooks/useCrossSigning.ts +++ b/src/app/hooks/useCrossSigning.ts @@ -1,9 +1,62 @@ -import { AccountDataEvent, SecretAccountData } from '../../types/matrix/accountData'; -import { useAccountData } from './useAccountData'; +import { AesHmacSha2, AesHmacSha2EncryptedData } from 'neurone/lib/crypto/ssss'; +import { useMemo } from 'react'; +import { CrossSigningKeys } from 'neurone/lib/crypto/machine'; +import { CrossSigningSecrets } from '../../client/context'; +import { useClient } from './useClient'; +import { useObservableValue } from './useObservableValue'; +import { AccountDataType } from '../../types/matrix/accountData'; -export const useCrossSigningActive = (): boolean => { - const masterEvent = useAccountData(AccountDataEvent.CrossSigningMaster); - const content = masterEvent?.getContent(); +export const useCrossSigning = (): CrossSigningSecrets | undefined => { + const ct = useClient(); - return !!content; + const crossSigning = useObservableValue(ct.secrets.crossSigning); + + return crossSigning; +}; + +export type CrossSigningSecretData = { + master: AesHmacSha2EncryptedData; + self: AesHmacSha2EncryptedData; + user: AesHmacSha2EncryptedData; +}; +export const useCrossSigningSecretData = (keyId: string): CrossSigningSecretData | undefined => { + const crossSigning = useCrossSigning(); + const data: CrossSigningSecretData | undefined = useMemo(() => { + const master = crossSigning?.master.get(keyId); + const self = crossSigning?.self.get(keyId); + const user = crossSigning?.user.get(keyId); + + if (!master || !self || !user) return undefined; + + return { + master, + self, + user, + }; + }, [crossSigning, keyId]); + + return data; +}; + +export const getCrossSigningPrivateKeys = async ( + recoveryKey: Uint8Array, + secretData: CrossSigningSecretData +): Promise => { + const master = await AesHmacSha2.decrypt( + recoveryKey, + AccountDataType.CrossSigningMaster, + secretData.master + ); + const self = await AesHmacSha2.decrypt( + recoveryKey, + AccountDataType.CrossSigningSelf, + secretData.self + ); + const user = await AesHmacSha2.decrypt( + recoveryKey, + AccountDataType.CrossSigningUser, + secretData.user + ); + + return { master, self, user }; }; diff --git a/src/app/hooks/useDeviceList.ts b/src/app/hooks/useDeviceList.ts index f89cb3d9..e4e6e288 100644 --- a/src/app/hooks/useDeviceList.ts +++ b/src/app/hooks/useDeviceList.ts @@ -1,30 +1,29 @@ import { useEffect, useCallback, useMemo } from 'react'; -import { IMyDevice } from 'matrix-js-sdk'; import { useQuery } from '@tanstack/react-query'; -import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto-api'; -import { useMatrixClient } from './useMatrixClient'; +import { Device } from 'neurone/lib/api/types'; +import { getDevices, GetDevicesOK } from 'neurone/lib/api/matrix/client/v3/devices'; +import { parseMatrixResp } from 'neurone/lib/matrix-request'; +import { useClient } from './useClient'; -export const useDeviceListChange = ( - onChange: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] -) => { - const mx = useMatrixClient(); - useEffect(() => { - mx.on(CryptoEvent.DevicesUpdated, onChange); - return () => { - mx.removeListener(CryptoEvent.DevicesUpdated, onChange); - }; - }, [mx, onChange]); +export const useDeviceListChange = (onChange: (users: string[]) => void) => { + const ct = useClient(); + + useEffect(() => ct.machine.events.onDevicesUpdated.subscribe(onChange), [ct, onChange]); }; const DEVICES_QUERY_KEY = ['devices']; -export function useDeviceList(): [undefined | IMyDevice[], () => Promise] { - const mx = useMatrixClient(); +export function useDeviceList(): [undefined | Device[], () => Promise] { + const ct = useClient(); const fetchDevices = useCallback(async () => { - const data = await mx.getDevices(); - return data.devices ?? []; - }, [mx]); + const resp = await ct.requestAuthJSON(getDevices({})); + const [err, result] = await parseMatrixResp(resp); + + if (err) console.error('Error loading devices!', err); + + return result?.devices ?? []; + }, [ct]); const { data: deviceList, refetch } = useQuery({ queryKey: DEVICES_QUERY_KEY, @@ -41,29 +40,30 @@ export function useDeviceList(): [undefined | IMyDevice[], () => Promise] useDeviceListChange( useCallback( (users) => { - const userId = mx.getUserId(); + const { userId } = ct.session; if (userId && users.includes(userId)) { refreshDeviceList(); } }, - [mx, refreshDeviceList] + [ct, refreshDeviceList] ) ); return [deviceList ?? undefined, refreshDeviceList]; } -export const useDeviceIds = (devices: IMyDevice[] | undefined): string[] => { +export const useDeviceIds = (devices: Device[] | undefined): string[] => { const devicesId = useMemo(() => devices?.map((device) => device.device_id) ?? [], [devices]); return devicesId; }; export const useSplitCurrentDevice = ( - devices: IMyDevice[] | undefined -): [IMyDevice | undefined, IMyDevice[] | undefined] => { - const mx = useMatrixClient(); - const currentDeviceId = mx.getDeviceId(); + devices: Device[] | undefined +): [Device | undefined, Device[] | undefined] => { + const ct = useClient(); + const currentDeviceId = ct.session.deviceId; + const currentDevice = useMemo( () => devices?.find((d) => d.device_id === currentDeviceId), [devices, currentDeviceId] diff --git a/src/app/hooks/useDeviceVerificationStatus.ts b/src/app/hooks/useDeviceVerificationStatus.ts index 9e723bd7..f6a49b58 100644 --- a/src/app/hooks/useDeviceVerificationStatus.ts +++ b/src/app/hooks/useDeviceVerificationStatus.ts @@ -1,10 +1,8 @@ import { useCallback, useEffect, useState } from 'react'; -import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api'; -import { verifiedDevice } from '../utils/matrix-crypto'; import { useAlive } from './useAlive'; import { fulfilledPromiseSettledResult } from '../utils/common'; -import { useMatrixClient } from './useMatrixClient'; import { useDeviceListChange } from './useDeviceList'; +import { useClient } from './useClient'; export enum VerificationStatus { Unknown, @@ -13,30 +11,32 @@ export enum VerificationStatus { Unsupported, } -export const useDeviceVerificationDetect = ( - crypto: CryptoApi | undefined, +export const useDeviceVerificationStatus = ( userId: string, - deviceId: string | undefined, - callback: (status: VerificationStatus) => void -): void => { - const mx = useMatrixClient(); + deviceId: string | undefined +): VerificationStatus => { + const [verificationStatus, setVerificationStatus] = useState(VerificationStatus.Unknown); + const ct = useClient(); const updateStatus = useCallback(async () => { if (crypto && deviceId) { - const data = await verifiedDevice(crypto, userId, deviceId); - if (data === null) { - callback(VerificationStatus.Unsupported); + const device = await ct.machine.userTracker.getDevice(userId, deviceId); + if (!device) { + setVerificationStatus(VerificationStatus.Unsupported); return; } - callback(data ? VerificationStatus.Verified : VerificationStatus.Unverified); + + setVerificationStatus( + device.crossSigningTrusted ? VerificationStatus.Verified : VerificationStatus.Unverified + ); return; } - callback(VerificationStatus.Unknown); - }, [crypto, deviceId, userId, callback]); + setVerificationStatus(VerificationStatus.Unknown); + }, [ct, deviceId, userId]); useEffect(() => { updateStatus(); - }, [mx, updateStatus, userId]); + }, [ct, userId, updateStatus]); useDeviceListChange( useCallback( @@ -48,36 +48,25 @@ export const useDeviceVerificationDetect = ( [userId, updateStatus] ) ); -}; - -export const useDeviceVerificationStatus = ( - crypto: CryptoApi | undefined, - userId: string, - deviceId: string | undefined -): VerificationStatus => { - const [verificationStatus, setVerificationStatus] = useState(VerificationStatus.Unknown); - - useDeviceVerificationDetect(crypto, userId, deviceId, setVerificationStatus); return verificationStatus; }; -export const useUnverifiedDeviceCount = ( - crypto: CryptoApi | undefined, - userId: string, - devices: string[] -): number | undefined => { +export const useUnverifiedDeviceCount = (userId: string, devices: string[]): number | undefined => { + const ct = useClient(); const [unverifiedCount, setUnverifiedCount] = useState(); const alive = useAlive(); const updateCount = useCallback(async () => { let count = 0; if (crypto) { - const promises = devices.map((deviceId) => verifiedDevice(crypto, userId, deviceId)); + const promises = devices.map((deviceId) => + ct.machine.userTracker.getDevice(userId, deviceId) + ); const result = await Promise.allSettled(promises); const settledResult = fulfilledPromiseSettledResult(result); - settledResult.forEach((status) => { - if (status === false) { + settledResult.forEach((device) => { + if (device && !device.verified) { count += 1; } }); @@ -85,7 +74,7 @@ export const useUnverifiedDeviceCount = ( if (alive()) { setUnverifiedCount(count); } - }, [crypto, userId, devices, alive]); + }, [ct, userId, devices, alive]); useDeviceListChange( useCallback( diff --git a/src/app/hooks/useKeyBackup.ts b/src/app/hooks/useKeyBackup.ts deleted file mode 100644 index e736e8dd..00000000 --- a/src/app/hooks/useKeyBackup.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { - BackupTrustInfo, - CryptoApi, - CryptoEvent, - CryptoEventHandlerMap, - KeyBackupInfo, -} from 'matrix-js-sdk/lib/crypto-api'; -import { useCallback, useEffect, useState } from 'react'; -import { useMatrixClient } from './useMatrixClient'; -import { useAlive } from './useAlive'; - -export const useKeyBackupStatusChange = ( - onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupStatus] -) => { - const mx = useMatrixClient(); - - useEffect(() => { - mx.on(CryptoEvent.KeyBackupStatus, onChange); - return () => { - mx.removeListener(CryptoEvent.KeyBackupStatus, onChange); - }; - }, [mx, onChange]); -}; - -export const useKeyBackupStatus = (crypto: CryptoApi): boolean => { - const alive = useAlive(); - const [status, setStatus] = useState(false); - - useEffect(() => { - crypto.getActiveSessionBackupVersion().then((v) => { - if (alive()) { - setStatus(typeof v === 'string'); - } - }); - }, [crypto, alive]); - - useKeyBackupStatusChange(setStatus); - - return status; -}; - -export const useKeyBackupSessionsRemainingChange = ( - onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupSessionsRemaining] -) => { - const mx = useMatrixClient(); - - useEffect(() => { - mx.on(CryptoEvent.KeyBackupSessionsRemaining, onChange); - return () => { - mx.removeListener(CryptoEvent.KeyBackupSessionsRemaining, onChange); - }; - }, [mx, onChange]); -}; - -export const useKeyBackupFailedChange = ( - onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupFailed] -) => { - const mx = useMatrixClient(); - - useEffect(() => { - mx.on(CryptoEvent.KeyBackupFailed, onChange); - return () => { - mx.removeListener(CryptoEvent.KeyBackupFailed, onChange); - }; - }, [mx, onChange]); -}; - -export const useKeyBackupDecryptionKeyCached = ( - onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupDecryptionKeyCached] -) => { - const mx = useMatrixClient(); - - useEffect(() => { - mx.on(CryptoEvent.KeyBackupDecryptionKeyCached, onChange); - return () => { - mx.removeListener(CryptoEvent.KeyBackupDecryptionKeyCached, onChange); - }; - }, [mx, onChange]); -}; - -export const useKeyBackupSync = (): [number, string | undefined] => { - const [remaining, setRemaining] = useState(0); - const [failure, setFailure] = useState(); - - useKeyBackupSessionsRemainingChange( - useCallback((count) => { - setRemaining(count); - setFailure(undefined); - }, []) - ); - - useKeyBackupFailedChange( - useCallback((f) => { - if (typeof f === 'string') { - setFailure(f); - setRemaining(0); - } - }, []) - ); - - return [remaining, failure]; -}; - -export const useKeyBackupInfo = (crypto: CryptoApi): KeyBackupInfo | undefined | null => { - const alive = useAlive(); - const [info, setInfo] = useState(); - - const fetchInfo = useCallback(() => { - crypto.getKeyBackupInfo().then((i) => { - if (alive()) { - setInfo(i); - } - }); - }, [crypto, alive]); - - useEffect(() => { - fetchInfo(); - }, [fetchInfo]); - - useKeyBackupStatusChange(fetchInfo); - - useKeyBackupSessionsRemainingChange( - useCallback( - (remainingCount) => { - if (remainingCount === 0) { - fetchInfo(); - } - }, - [fetchInfo] - ) - ); - - return info; -}; - -export const useKeyBackupTrust = ( - crypto: CryptoApi, - backupInfo: KeyBackupInfo -): BackupTrustInfo | undefined => { - const alive = useAlive(); - const [trust, setTrust] = useState(); - - const fetchTrust = useCallback(() => { - crypto.isKeyBackupTrusted(backupInfo).then((t) => { - if (alive()) { - setTrust(t); - } - }); - }, [crypto, alive, backupInfo]); - - useEffect(() => { - fetchTrust(); - }, [fetchTrust]); - - useKeyBackupStatusChange(fetchTrust); - - useKeyBackupDecryptionKeyCached(fetchTrust); - - return trust; -}; diff --git a/src/app/hooks/useRestoreBackupOnVerification.ts b/src/app/hooks/useRestoreBackupOnVerification.ts deleted file mode 100644 index 74497b0a..00000000 --- a/src/app/hooks/useRestoreBackupOnVerification.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useSetAtom } from 'jotai'; -import { useCallback } from 'react'; -import { backupRestoreProgressAtom } from '../state/backupRestore'; -import { useMatrixClient } from './useMatrixClient'; -import { useKeyBackupDecryptionKeyCached } from './useKeyBackup'; - -export const useRestoreBackupOnVerification = () => { - const setRestoreProgress = useSetAtom(backupRestoreProgressAtom); - - const mx = useMatrixClient(); - - useKeyBackupDecryptionKeyCached( - useCallback(() => { - const crypto = mx.getCrypto(); - if (!crypto) return; - - crypto.restoreKeyBackup({ - progressCallback(progress) { - setRestoreProgress(progress); - }, - }); - }, [mx, setRestoreProgress]) - ); -}; diff --git a/src/app/hooks/useSecretStorage.ts b/src/app/hooks/useSecretStorage.ts index 10eabeb9..6d746e7c 100644 --- a/src/app/hooks/useSecretStorage.ts +++ b/src/app/hooks/useSecretStorage.ts @@ -1,22 +1,23 @@ -import { - AccountDataEvent, - SecretStorageDefaultKeyContent, - SecretStorageKeyContent, -} from '../../types/matrix/accountData'; -import { useAccountData } from './useAccountData'; - -export const getSecretStorageKeyEventType = (key: string): string => `m.secret_storage.key.${key}`; +import { ObjectReader } from 'neurone/lib/utils'; +import { AesHmacSha2KeyDescription, KeyStorage } from 'neurone/lib/crypto/ssss'; +import { useAccountData } from './storage/useAccountData'; export const useSecretStorageDefaultKeyId = (): string | undefined => { - const defaultKeyEvent = useAccountData(AccountDataEvent.SecretStorageDefaultKey); - const defaultKeyId = defaultKeyEvent?.getContent().key; + const defaultKeyResult = useAccountData(KeyStorage.defaultKeyIdentifier()); + const defaultKeyEvent = defaultKeyResult.value; + const defaultKeyId = ObjectReader.string(defaultKeyEvent?.content, 'key'); return defaultKeyId; }; -export const useSecretStorageKeyContent = (keyId: string): SecretStorageKeyContent | undefined => { - const keyEvent = useAccountData(getSecretStorageKeyEventType(keyId)); - const secretStorageKey = keyEvent?.getContent(); +export const useSecretStorageKeyContent = ( + keyId: string +): AesHmacSha2KeyDescription | undefined => { + const keyResult = useAccountData(KeyStorage.keyIdentifier(keyId)); + const keyEvent = keyResult.value; + const secretStorageKey = keyEvent?.content + ? KeyStorage.getKeyDescription(keyEvent.content) + : undefined; return secretStorageKey; }; diff --git a/src/app/hooks/useUIAFlows.ts b/src/app/hooks/useUIAFlows.ts index 012298d4..f420936f 100644 --- a/src/app/hooks/useUIAFlows.ts +++ b/src/app/hooks/useUIAFlows.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { MatrixError } from 'matrix-js-sdk'; +import { MatrixError } from 'neurone/lib/matrix-request'; import { AuthResponse, AuthType, @@ -102,7 +102,7 @@ export const useUIAMatrixError = ( error?: MatrixError ): [undefined, undefined] | [AuthResponse, undefined] | [undefined, MatrixError] => { if (!error) return [undefined, undefined]; - if (error.httpStatus === 401) return [error.data as AuthResponse, undefined]; + if (error.status === 401) return [error.data as AuthResponse, undefined]; return [undefined, error]; }; diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 3d3c13df..b36847eb 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -57,7 +57,6 @@ import { ClientInitStorageAtom } from './client/ClientInitStorageAtom'; import { ClientNonUIFeatures } from './client/ClientNonUIFeatures'; import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager'; import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification'; -import { AutoRestoreBackupOnVerification } from '../components/BackupRestore'; import { RoomSettingsRenderer } from '../features/room-settings'; import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences'; import { SpaceSettingsRenderer } from '../features/space-settings'; @@ -141,8 +140,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) - {/* - */} + diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx index b275f6f3..a2e1b682 100644 --- a/src/app/pages/client/SidebarNav.tsx +++ b/src/app/pages/client/SidebarNav.tsx @@ -44,7 +44,7 @@ export function SidebarNav() { - {/* */} + diff --git a/src/app/pages/client/sidebar/UnverifiedTab.tsx b/src/app/pages/client/sidebar/UnverifiedTab.tsx index 0a2f117b..fb9f20a9 100644 --- a/src/app/pages/client/sidebar/UnverifiedTab.tsx +++ b/src/app/pages/client/sidebar/UnverifiedTab.tsx @@ -7,38 +7,29 @@ import { SidebarItemTooltip, } from '../../../components/sidebar'; import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList'; -import { useMatrixClient } from '../../../hooks/useMatrixClient'; import * as css from './UnverifiedTab.css'; import { useDeviceVerificationStatus, useUnverifiedDeviceCount, VerificationStatus, } from '../../../hooks/useDeviceVerificationStatus'; -import { useCrossSigningActive } from '../../../hooks/useCrossSigning'; +import { useCrossSigning } from '../../../hooks/useCrossSigning'; import { Modal500 } from '../../../components/Modal500'; import { Settings, SettingsPages } from '../../../features/settings'; +import { useClient } from '../../../hooks/useClient'; function UnverifiedIndicator() { - const mx = useMatrixClient(); + const ct = useClient(); - const crypto = mx.getCrypto(); const [devices] = useDeviceList(); - const [currentDevice, otherDevices] = useSplitCurrentDevice(devices); + const [, otherDevices] = useSplitCurrentDevice(devices); - const verificationStatus = useDeviceVerificationStatus( - crypto, - mx.getSafeUserId(), - currentDevice?.device_id - ); + const verificationStatus = useDeviceVerificationStatus(ct.session.userId, ct.session.deviceId); const unverified = verificationStatus === VerificationStatus.Unverified; const otherDevicesId = useDeviceIds(otherDevices); - const unverifiedDeviceCount = useUnverifiedDeviceCount( - crypto, - mx.getSafeUserId(), - otherDevicesId - ); + const unverifiedDeviceCount = useUnverifiedDeviceCount(ct.session.userId, otherDevicesId); const [settings, setSettings] = useState(false); const closeSettings = () => setSettings(false); @@ -86,9 +77,9 @@ function UnverifiedIndicator() { } export function UnverifiedTab() { - const crossSigningActive = useCrossSigningActive(); + const crossSigning = useCrossSigning(); - if (!crossSigningActive) return null; + if (!crossSigning) return null; return ; } diff --git a/src/app/state/backupRestore.ts b/src/app/state/backupRestore.ts deleted file mode 100644 index ad14e5d7..00000000 --- a/src/app/state/backupRestore.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { atom } from 'jotai'; -import { ImportRoomKeyProgressData, ImportRoomKeyStage } from 'matrix-js-sdk/lib/crypto-api'; - -export enum BackupProgressStatus { - Idle, - Fetching, - Loading, - Done, -} -export type ProgressData = { - downloaded: number; - successes: number; - failures: number; - total: number; -}; -export type IBackupProgress = - | { - status: BackupProgressStatus.Idle; - } - | { - status: BackupProgressStatus.Fetching; - } - | { - status: BackupProgressStatus.Loading; - data: ProgressData; - } - | { - status: BackupProgressStatus.Done; - }; - -const baseBackupRestoreProgressAtom = atom({ - status: BackupProgressStatus.Idle, -}); - -export const backupRestoreProgressAtom = atom< - IBackupProgress, - [ImportRoomKeyProgressData], - undefined ->( - (get) => get(baseBackupRestoreProgressAtom), - (get, set, progress) => { - if (progress.stage === ImportRoomKeyStage.Fetch) { - set(baseBackupRestoreProgressAtom, { - status: BackupProgressStatus.Fetching, - }); - return; - } - - if (progress.stage === ImportRoomKeyStage.LoadKeys) { - const { total, successes, failures } = progress; - - const downloaded = successes + failures; - if (downloaded === total) { - set(baseBackupRestoreProgressAtom, { - status: BackupProgressStatus.Done, - }); - return; - } - set(baseBackupRestoreProgressAtom, { - status: BackupProgressStatus.Loading, - data: { - downloaded, - successes, - failures, - total, - }, - }); - } - } -); diff --git a/src/app/utils/matrix-crypto.ts b/src/app/utils/matrix-crypto.ts deleted file mode 100644 index dda402f8..00000000 --- a/src/app/utils/matrix-crypto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api'; - -export const verifiedDevice = async ( - api: CryptoApi, - userId: string, - deviceId: string -): Promise => { - const status = await api.getDeviceVerificationStatus(userId, deviceId); - - if (!status) return null; - - const verified = status.crossSigningVerified; - return verified; -}; diff --git a/src/client/MegolmBackup.ts b/src/client/MegolmBackup.ts new file mode 100644 index 00000000..813da0eb --- /dev/null +++ b/src/client/MegolmBackup.ts @@ -0,0 +1,211 @@ +import { getKeys, GetKeysOK, PutKeysOK } from 'neurone/lib/api/matrix/client/v3/roomKeys'; +import { BackupVersionInfo } from 'neurone/lib/api/types'; +import { Machine } from 'neurone/lib/crypto/machine'; +import { KeyBackup, RoomKeysImportResult } from 'neurone/lib/crypto/machine/backup'; +import { MatrixError, MatrixRequest, parseMatrixResp } from 'neurone/lib/matrix-request'; +import { Base64 } from 'neurone/lib/utils'; +import { Disposable, Disposer } from 'neurone/lib/utils/disposable'; +import { ObservableValue } from 'neurone/lib/utils/observable'; +import { ProgressCallback, responseProgress } from './responseProgress'; + +export type ImportBackupIdle = { + type: 'idle'; +}; +export type ImportBackupDownload = { + type: 'download'; + receivedBytes: number; + totalBytes?: number; +}; +export type ImportBackupImport = { + type: 'import'; + progress?: { + imported: number; + total: number; + failures: number; + }; +}; +export type ImportBackupDone = { + type: 'done'; + result: RoomKeysImportResult; +}; +export type ImportBackupError = { + type: 'error'; + error: MatrixError; +}; + +export type BackupRestoreState = + | ImportBackupIdle + | ImportBackupDownload + | ImportBackupImport + | ImportBackupDone + | ImportBackupError; + +export class MegolmBackup implements Disposable { + private readonly disposer = new Disposer(); + + private readonly publishKey = crypto.randomUUID(); + + public readonly keyBackup: ObservableValue; + + public readonly restoreState: ObservableValue; + + public readonly backupInProgress: ObservableValue; + + constructor( + private readonly machine: Machine, + private readonly request: MatrixRequest, + private readonly backupInfo: ObservableValue + ) { + this.keyBackup = new ObservableValue(undefined, this.publishKey); + this.restoreState = new ObservableValue({ type: 'idle' }, this.publishKey); + this.backupInProgress = new ObservableValue(false, this.publishKey); + + this.handleBackupInfo(this.backupInfo.read); + + this.disposer.collect( + this.backupInfo.subscribe(this.handleBackupInfo.bind(this)), + + this.machine.events.onReceiveSecret.subscribe(({ name, value }) => { + if (name !== 'm.megolm_backup.v1') return; + this.saveDecryptionKey(value); + }), + + this.machine.events.onRoomKeysUpdated.subscribe(() => { + const keyBackup = this.keyBackup.read; + if (keyBackup) { + this.backup(keyBackup); + } + }) + ); + } + + private async handleBackupInfo(backupInfo?: BackupVersionInfo): Promise { + const prevBackup = this.keyBackup.read?.backupInfo; + + const backupRemoved = !backupInfo && prevBackup !== undefined; + if (backupRemoved) { + await this.machine.backupManager.disableBackup(); + this.keyBackup.publish(this.publishKey, undefined); + return; + } + + if (!backupInfo) return; + + const versionChanged = prevBackup && prevBackup.version !== backupInfo.version; + if (versionChanged && this.keyBackup.read) { + this.machine.backupManager.disableBackup(); + } + + this.start(backupInfo); + } + + public async saveDecryptionKey(secret: string): Promise { + const backupInfo = this.backupInfo.read; + if (!backupInfo) { + throw new Error( + 'Cannot save the backup recovery key because no backup version is configured.' + ); + } + + await this.machine.backupManager.saveDecryptionKey(Base64.decode(secret), this.backupInfo.read); + + const keyBackup = this.keyBackup.read ?? (await this.start(backupInfo)); + + this.restore(keyBackup); + } + + private async start(backupInfo: BackupVersionInfo): Promise { + const trusted = await this.machine.backupManager.trustedBackup(backupInfo); + if (!trusted) { + throw new Error( + 'Cannot start key backup because the provided backup version is not trusted.' + ); + } + + const keyBackup = await this.machine.backupManager.enableBackup(backupInfo); + this.keyBackup.publish(this.publishKey, keyBackup); + + this.backup(keyBackup); + + return keyBackup; + } + + private async backup(keyBackup: KeyBackup): Promise { + if (this.backupInProgress.read) { + return; + } + + this.backupInProgress.publish(this.publishKey, true); + + try { + while (true) { + // eslint-disable-next-line no-await-in-loop + const req = await keyBackup.backupRoomKeys(); + + // No more keys to back up. + if (!req) break; + + // eslint-disable-next-line no-await-in-loop + const [err, result] = await parseMatrixResp(req.response); + + if (err) throw new Error(err.data.error ?? err.data.errcode); + + console.debug(`Backed up ${result.count} room keys`); + } + } catch (e) { + console.error('Backup keys request failed!', e); + } finally { + this.backupInProgress.publish(this.publishKey, false); + } + } + + public async restore(keyBackup: KeyBackup): Promise { + this.restoreState.publish(this.publishKey, { + type: 'download', + receivedBytes: 0, + }); + const resp = await this.request( + getKeys({ searchParams: { version: keyBackup.backupInfo.version } }) + ); + + const handleDownloadProgress: ProgressCallback = (receivedBytes, totalBytes) => { + this.restoreState.publish(this.publishKey, { + type: 'download', + receivedBytes, + totalBytes, + }); + }; + await responseProgress(resp.clone(), handleDownloadProgress); + + const [error, result] = await parseMatrixResp(resp); + if (error) { + this.restoreState.publish(this.publishKey, { + type: 'error', + error, + }); + return; + } + + this.restoreState.publish(this.publishKey, { type: 'import' }); + const handleImportProgress = (imported: bigint, total: bigint, failures: bigint) => { + this.restoreState.publish(this.publishKey, { + type: 'import', + progress: { + imported: Number(imported), + total: Number(total), + failures: Number(failures), + }, + }); + }; + + const importResult = await keyBackup.importBackup(result.rooms, handleImportProgress); + this.restoreState.publish(this.publishKey, { + type: 'done', + result: importResult, + }); + } + + dispose(): void { + this.disposer.run(); + } +} diff --git a/src/client/context.ts b/src/client/context.ts index 96aeefc4..aa66f9a1 100644 --- a/src/client/context.ts +++ b/src/client/context.ts @@ -1,14 +1,18 @@ import { ComputedObservableValue, Observable, ObservableValue } from 'neurone/lib/utils/observable'; -import { MatrixRequest } from 'neurone/lib/matrix-request'; +import { getVersion, GetVersionOK } from 'neurone/lib/api/matrix/client/v3/roomKeys'; +import { MatrixRequest, parseMatrixResp } from 'neurone/lib/matrix-request'; import { HistoryVisibility, Machine } from 'neurone/lib/crypto/machine'; import { ObjectReader } from 'neurone/lib/utils'; import { Disposer } from 'neurone/lib/utils/disposable'; import { InboundVerification, OutboundVerification } from 'neurone/lib/crypto/machine/verification'; import { + AccountDataEvent, + BackupVersionInfo, MessageEventWithoutRoomId, RoomSummary, StateEventWithoutRoomId, } from 'neurone/lib/api/types'; +import { AesHmacSha2EncryptedData, SecretStorage } from 'neurone/lib/crypto/ssss'; import { InviteRoomStateObject, JoinRoomStateObject, @@ -24,6 +28,7 @@ import { GetTxnId } from './txn'; import { Timeline } from './room'; import { AccountDataType } from '../types/matrix/accountData'; import { DecryptionManager } from './decryption'; +import { MegolmBackup } from './MegolmBackup'; export type StateContext = { join: { @@ -74,6 +79,17 @@ export type RoomsContext = { leave: ObservableValue; }; +export type CrossSigningSecrets = { + master: Map; + self: Map; + user: Map; +}; + +export type Secrets = { + crossSigning: ObservableValue; + megolmBackup: ObservableValue | undefined>; +}; + export type ClientContext = { session: Session; storage: Storage; @@ -105,6 +121,10 @@ export type ClientContext = { pendingMemberRooms: ObservableValue; verificationRequest: Observable; + secrets: Secrets; + backupInfo: ObservableValue; + megolmBackup: MegolmBackup; + sync: SyncContext; requestAuthJSON: MatrixRequest; getTxnId: GetTxnId; @@ -895,3 +915,103 @@ export const createVerificationRequestObservable = () => { onVerificationRequest, }; }; + +export const createSecretsObservable = async ( + storage: Storage, + disposer: Disposer +): Promise => { + const masterObservable = await storage.accountData.observableRead.get( + AccountDataType.CrossSigningMaster + ); + const selfObservable = await storage.accountData.observableRead.get( + AccountDataType.CrossSigningSelf + ); + const userObservable = await storage.accountData.observableRead.get( + AccountDataType.CrossSigningUser + ); + + const backupObservable = await storage.accountData.observableRead.get( + AccountDataType.MegolmBackupV1 + ); + + const getData = (event: AccountDataEvent) => { + const kToD = new Map(); + + SecretStorage.getAllKeyId(event.content).forEach((keyId) => { + const d = SecretStorage.getEncryptedData(keyId, event.content); + if (!d) return; + kToD.set(keyId, d); + }); + + if (kToD.size === 0) return undefined; + + return kToD; + }; + + const crossSigning: ObservableValue = + new ComputedObservableValue(() => { + const masterEvent = masterObservable.read?.event; + const selfEvent = selfObservable.read?.event; + const userEvent = userObservable.read?.event; + + const master = masterEvent && getData(masterEvent); + const self = selfEvent && getData(selfEvent); + const user = userEvent && getData(userEvent); + + if (!master || !self || !user) return undefined; + + return { + master, + self, + user, + }; + }, [masterObservable, selfObservable, userObservable]); + + const megolmBackup: ObservableValue | undefined> = + new ComputedObservableValue(() => { + const event = backupObservable.read?.event; + if (!event) return undefined; + + return getData(event); + }, [backupObservable]); + + disposer.collect( + masterObservable, + selfObservable, + userObservable, + backupObservable, + crossSigning, + megolmBackup + ); + + return { + crossSigning, + megolmBackup, + }; +}; + +export const createBackupInfoObservable = async ( + request: MatrixRequest, + secrets: Secrets, + disposer: Disposer +): Promise> => { + const publishKey = crypto.randomUUID(); + const backupInfo = new ObservableValue(undefined, publishKey); + + const update = async () => { + const resp = await request(getVersion({})); + const [, result] = await parseMatrixResp(resp); + backupInfo.publish(publishKey, result); + }; + + update(); + + disposer.collect( + backupInfo, + secrets.megolmBackup.subscribe(() => { + update(); + }) + ); + + return backupInfo; +}; diff --git a/src/client/decryption/DecryptionManager.ts b/src/client/decryption/DecryptionManager.ts index 3b180183..b1867144 100644 --- a/src/client/decryption/DecryptionManager.ts +++ b/src/client/decryption/DecryptionManager.ts @@ -51,16 +51,16 @@ export class DecryptionManager implements Disposable { const { content } = event; const sessionId = ObjectReader.string(content, 'session_id'); - if (!sessionId) { - throw new Error('Malformed event! Missing session_id'); + if (sessionId) { + const existingEvent = eventIndex.get(roomId, sessionId, event.event_id); + if (existingEvent) return existingEvent; } - const existingEvent = eventIndex.get(roomId, sessionId, event.event_id); - if (existingEvent) return existingEvent; - const encryptedEvent = new EncryptedEvent(this.machine.roomEncryption, roomId, event); - eventIndex.store(roomId, sessionId, encryptedEvent); + if (sessionId) { + eventIndex.store(roomId, sessionId, encryptedEvent); + } return encryptedEvent; } diff --git a/src/client/initialize.ts b/src/client/initialize.ts index 91e6a276..a638ddab 100644 --- a/src/client/initialize.ts +++ b/src/client/initialize.ts @@ -3,6 +3,7 @@ import { Disposer } from 'neurone/lib/utils/disposable'; import { Session } from '../app/state/sessions'; import { ClientContext, + createSecretsObservable, createDirectRoomsObservable, createDirectRoomToUserObservable, createEncryptedRoomsObservable, @@ -23,6 +24,7 @@ import { createStateContext, createSyncTokenObservable, createVerificationRequestObservable, + createBackupInfoObservable, } from './context'; import { createStorage } from './storage'; import { getAuthJSONRequest } from './request'; @@ -34,6 +36,7 @@ import { LogoutManager } from './LogoutManager'; import { MachineManager } from './MachineManager'; import { DecryptionManager } from './decryption'; import { initializeCrypto } from './crypto'; +import { MegolmBackup } from './MegolmBackup'; export const initializeClient = async (session: Session): Promise => { const disposer = new Disposer(); @@ -71,6 +74,9 @@ export const initializeClient = async (session: Session): Promise const pendingMemberRooms = await createPendingMemberRoomObservable(storage, disposer); const { verificationRequest, onVerificationRequest } = createVerificationRequestObservable(); + const secrets = await createSecretsObservable(storage, disposer); + const backupInfo = await createBackupInfoObservable(requestAuthJSON, secrets, disposer); + const megolmBackup = new MegolmBackup(machine, requestAuthJSON, backupInfo); const getTxnId: GetTxnId = getTxnIdFunc(); @@ -129,6 +135,9 @@ export const initializeClient = async (session: Session): Promise pendingMemberRooms, verificationRequest, + secrets, + backupInfo, + megolmBackup, sync: { observable: syncObservable, controller: syncController, @@ -148,6 +157,7 @@ export const initializeClient = async (session: Session): Promise }); disposer.collect( + megolmBackup, decryption, syncObservable, syncController, diff --git a/src/types/matrix/accountData.ts b/src/types/matrix/accountData.ts index 5a0897b2..43a62998 100644 --- a/src/types/matrix/accountData.ts +++ b/src/types/matrix/accountData.ts @@ -13,8 +13,8 @@ export enum AccountDataEvent { SecretStorageDefaultKey = 'm.secret_storage.default_key', CrossSigningMaster = 'm.cross_signing.master', - CrossSigningSelf = 'm.cross_signing.self', - CrossSigningUser = 'm.cross_signing.user', + CrossSigningSelf = 'm.cross_signing.self_signing', + CrossSigningUser = 'm.cross_signing.user_signing', MegolmBackupV1 = 'm.megolm_backup.v1', } export enum AccountDataType { @@ -32,8 +32,8 @@ export enum AccountDataType { SecretStorageDefaultKey = 'm.secret_storage.default_key', CrossSigningMaster = 'm.cross_signing.master', - CrossSigningSelf = 'm.cross_signing.self', - CrossSigningUser = 'm.cross_signing.user', + CrossSigningSelf = 'm.cross_signing.self_signing', + CrossSigningUser = 'm.cross_signing.user_signing', MegolmBackupV1 = 'm.megolm_backup.v1', }