From 5cbc88fbdbad18fdb391bf65e5588c7477a9bd13 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:36:30 +0530 Subject: [PATCH] receive verification request --- src/app/components/DeviceVerification.tsx | 131 +++++++++++----------- src/app/hooks/useVerificationRequest.ts | 88 +-------------- src/client/SyncResponseManager.ts | 33 ++++-- src/client/context.ts | 19 +++- src/client/initialize.ts | 14 ++- 5 files changed, 121 insertions(+), 164 deletions(-) diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx index d2502ccf..8e3641fd 100644 --- a/src/app/components/DeviceVerification.tsx +++ b/src/app/components/DeviceVerification.tsx @@ -1,11 +1,4 @@ -import { - ShowSasCallbacks, - VerificationPhase, - VerificationRequest, - Verifier, -} from 'matrix-js-sdk/lib/crypto-api'; import React, { CSSProperties, useCallback, useEffect, useState } from 'react'; -import { VerificationMethod } from 'matrix-js-sdk/lib/types'; import { Box, Button, @@ -23,13 +16,24 @@ import { } from 'folds'; import FocusTrap from 'focus-trap-react'; import { - useVerificationRequestPhase, - useVerificationRequestReceived, - useVerifierCancel, - useVerifierShowSas, -} from '../hooks/useVerificationRequest'; + CancelableVerificationStage, + CancelVerificationStage, + DoneVerificationStage, + InboundRequestStage, + InboundVerification, + OutboundRequestStage, + OutboundVerification, + ReadyVerificationStage, + SasAcceptStage, + SasCancelState, + SasMethod, + SasStartStage, + StartVerificationStage, +} from 'neurone/lib/crypto/machine/verification'; +import { useVerificationRequestReceived } from '../hooks/useVerificationRequest'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { ContainerColor } from '../styles/ContainerColor.css'; +import { useObservableValue } from '../hooks/useObservableValue'; const DialogHeaderStyles: CSSProperties = { padding: `0 ${config.space.S200} 0 ${config.space.S400}`, @@ -70,10 +74,12 @@ function VerificationWaitAccept() { } type VerificationAcceptProps = { - onAccept: () => Promise; + stage: InboundRequestStage; }; -function VerificationAccept({ onAccept }: VerificationAcceptProps) { - const [acceptState, accept] = useAsyncCallback(onAccept); +function VerificationAccept({ stage }: VerificationAcceptProps) { + const [acceptState, accept] = useAsyncCallback( + useCallback(() => stage.ready(['m.sas.v1']), [stage]) + ); const accepting = acceptState.status === AsyncStatus.Loading; return ( @@ -102,12 +108,12 @@ function VerificationWaitStart() { } type VerificationStartProps = { - onStart: () => Promise; + stage: ReadyVerificationStage; }; -function AutoVerificationStart({ onStart }: VerificationStartProps) { +function AutoVerificationStart({ stage }: VerificationStartProps) { useEffect(() => { - onStart(); - }, [onStart]); + stage.startSas(); + }, [stage]); return ( @@ -116,8 +122,8 @@ function AutoVerificationStart({ onStart }: VerificationStartProps) { ); } -function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) { - const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData])); +function CompareEmoji({ stage }: { stage: SasAcceptStage }) { + const [confirmState, confirm] = useAsyncCallback(useCallback(() => stage.match(), [stage])); const confirming = confirmState.status === AsyncStatus.Loading || confirmState.status === AsyncStatus.Success; @@ -135,17 +141,17 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) { wrap="Wrap" justifyContent="Center" > - {sasData.sas.emoji?.map(([emoji, name], index) => ( + {stage.emoji?.map((emoji, index) => ( - {emoji} - {name} + {emoji.emoji} + {emoji.description} ))} @@ -162,7 +168,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) { @@ -228,23 +237,18 @@ function VerificationCanceled({ onClose }: VerificationCanceledProps) { } type DeviceVerificationProps = { - request: VerificationRequest; + request: InboundVerification | OutboundVerification; onExit: () => void; }; export function DeviceVerification({ request, onExit }: DeviceVerificationProps) { - const phase = useVerificationRequestPhase(request); + const stage = useObservableValue(request.stage); const handleCancel = useCallback(() => { - if (request.phase !== VerificationPhase.Done && request.phase !== VerificationPhase.Cancelled) { - request.cancel(); + if (stage instanceof CancelableVerificationStage) { + stage.cancel(); } onExit(); - }, [request, onExit]); - - const handleAccept = useCallback(() => request.accept(), [request]); - const handleStart = useCallback(async () => { - await request.startVerification(VerificationMethod.Sas); - }, [request]); + }, [stage, onExit]); return ( }> @@ -266,30 +270,27 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps) - {phase === VerificationPhase.Requested && - (request.initiatedByMe ? ( - - ) : ( - - ))} - {phase === VerificationPhase.Ready && - (request.initiatedByMe ? ( - + {stage instanceof InboundRequestStage && } + {stage instanceof OutboundRequestStage && } + {stage instanceof ReadyVerificationStage && + (request instanceof OutboundRequestStage ? ( + ) : ( ))} - {phase === VerificationPhase.Started && - (request.verifier ? ( - + {stage instanceof StartVerificationStage && + (stage.method ? ( + ) : ( ))} - {phase === VerificationPhase.Done && } - {phase === VerificationPhase.Cancelled && ( - + + {stage instanceof DoneVerificationStage && } + {stage instanceof CancelVerificationStage && ( + )} @@ -300,7 +301,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps) } export function ReceiveSelfDeviceVerification() { - const [request, setRequest] = useState(); + const [request, setRequest] = useState(); useVerificationRequestReceived(setRequest); @@ -310,7 +311,7 @@ export function ReceiveSelfDeviceVerification() { if (!request) return null; - if (!request.isSelfVerification) { + if (!request.selfVerification) { return null; } diff --git a/src/app/hooks/useVerificationRequest.ts b/src/app/hooks/useVerificationRequest.ts index 5feb5232..f1e0a98a 100644 --- a/src/app/hooks/useVerificationRequest.ts +++ b/src/app/hooks/useVerificationRequest.ts @@ -1,87 +1,11 @@ -import { useCallback, useEffect, useState } from 'react'; -import { - CryptoEvent, - CryptoEventHandlerMap, - VerificationPhase, - VerificationRequest, - VerificationRequestEvent, - VerificationRequestEventHandlerMap, - Verifier, - VerifierEvent, - VerifierEventHandlerMap, -} from 'matrix-js-sdk/lib/crypto-api'; -import { useMatrixClient } from './useMatrixClient'; +import { useEffect } from 'react'; +import { InboundVerification, OutboundVerification } from 'neurone/lib/crypto/machine/verification'; +import { useClient } from './useClient'; export const useVerificationRequestReceived = ( - onRequest: CryptoEventHandlerMap[CryptoEvent.VerificationRequestReceived] + onRequest: (request: OutboundVerification | InboundVerification) => void ) => { - const mx = useMatrixClient(); + const ct = useClient(); - useEffect(() => { - mx.on(CryptoEvent.VerificationRequestReceived, onRequest); - return () => { - mx.removeListener(CryptoEvent.VerificationRequestReceived, onRequest); - }; - }, [mx, onRequest]); -}; - -export const useVerificationRequestChange = ( - request: VerificationRequest, - onChange: VerificationRequestEventHandlerMap[VerificationRequestEvent.Change] -) => { - useEffect(() => { - request.on(VerificationRequestEvent.Change, onChange); - return () => { - request.removeListener(VerificationRequestEvent.Change, onChange); - }; - }, [request, onChange]); -}; - -export const useVerificationRequestPhase = (request: VerificationRequest): VerificationPhase => { - const [phase, setPhase] = useState(() => request.phase); - - useVerificationRequestChange( - request, - useCallback(() => { - setPhase(request.phase); - }, [request]) - ); - - return phase; -}; - -export const useVerifierCancel = ( - verifier: Verifier, - onCallback: VerifierEventHandlerMap[VerifierEvent.Cancel] -) => { - useEffect(() => { - verifier.on(VerifierEvent.Cancel, onCallback); - return () => { - verifier.removeListener(VerifierEvent.Cancel, onCallback); - }; - }, [verifier, onCallback]); -}; - -export const useVerifierShowSas = ( - verifier: Verifier, - onCallback: VerifierEventHandlerMap[VerifierEvent.ShowSas] -) => { - useEffect(() => { - verifier.on(VerifierEvent.ShowSas, onCallback); - return () => { - verifier.removeListener(VerifierEvent.ShowSas, onCallback); - }; - }, [verifier, onCallback]); -}; - -export const useVerifierShowReciprocateQr = ( - verifier: Verifier, - onCallback: VerifierEventHandlerMap[VerifierEvent.ShowReciprocateQr] -) => { - useEffect(() => { - verifier.on(VerifierEvent.ShowReciprocateQr, onCallback); - return () => { - verifier.removeListener(VerifierEvent.ShowReciprocateQr, onCallback); - }; - }, [verifier, onCallback]); + useEffect(() => ct.verificationRequest.subscribe(onRequest), [ct, onRequest]); }; diff --git a/src/client/SyncResponseManager.ts b/src/client/SyncResponseManager.ts index 232c24f7..60654454 100644 --- a/src/client/SyncResponseManager.ts +++ b/src/client/SyncResponseManager.ts @@ -2,8 +2,10 @@ import { GetSyncOK, JoinRoom } from 'neurone/lib/api/matrix/client/v3/sync'; import { ObservableValue } from 'neurone/lib/utils/observable'; import { EncryptionSyncChanges } from 'neurone/lib/crypto/machine'; import { Disposable, Disposer } from 'neurone/lib/utils/disposable'; +import { ToDeviceDecrypted, ToDevicePlainText } from 'neurone/lib/crypto/machine/events'; import { roomState } from 'neurone/lib/utils/sync/v3/join'; import { ObjectReader } from 'neurone/lib/utils'; +import { InboundVerification, OutboundVerification } from 'neurone/lib/crypto/machine/verification'; import to from 'await-to-js'; import { ClientContext } from './context'; import { writeSync } from './syncWriter'; @@ -11,6 +13,12 @@ import { sessionValue } from './storage/collections'; import { syncLatestEvent } from './syncLatestEvent'; import { SyncValue } from './sync'; +export type SyncResponseManagerCallbacks = { + onWriteComplete: () => void; + onSyncToken: (token: string) => void; + onVerificationRequest: (verificationFlow: OutboundVerification | InboundVerification) => void; +}; + export class SyncResponseManager implements Disposable { private firstSync = true; @@ -18,22 +26,18 @@ export class SyncResponseManager implements Disposable { private readonly syncTokenObservable: ObservableValue; - private readonly onWriteComplete: () => void; - - private readonly onSyncToken: (token: string) => void; + private readonly callbacks: SyncResponseManagerCallbacks; private readonly disposer = new Disposer(); constructor( ct: ClientContext, syncTokenObservable: ObservableValue, - onWriteComplete: () => void, - onSyncToken: (token: string) => void + callbacks: SyncResponseManagerCallbacks ) { this.ct = ct; this.syncTokenObservable = syncTokenObservable; - this.onWriteComplete = onWriteComplete; - this.onSyncToken = onSyncToken; + this.callbacks = callbacks; this.disposer.collect( this.syncTokenObservable.subscribe(this.handleSyncToken.bind(this)), @@ -48,7 +52,7 @@ export class SyncResponseManager implements Disposable { // after syncLatestEvent completes to prevent race return; } - this.onSyncToken(sinceToken); + this.callbacks.onSyncToken(sinceToken); }; private async handleSync(syncValue: SyncValue) { @@ -70,7 +74,14 @@ export class SyncResponseManager implements Disposable { unusedFallbackKeys: data.device_unused_fallback_key_types, }; - await this.ct.machine.syncChanges.receive(encryptionChanges); + const proccessedToDeviceEvents = await this.ct.machine.syncChanges.receive(encryptionChanges); + proccessedToDeviceEvents + .filter((e) => e instanceof ToDeviceDecrypted || e instanceof ToDevicePlainText) + .forEach((proccessedEvent) => { + this.ct.machine.verificationManager.receiveToDeviceEvent(proccessedEvent.event, (v) => + this.callbacks.onVerificationRequest(v) + ); + }); // TODO: room key bundle detection from to device events; const encryptedRooms = this.filterEncryptedRoomResponse(data.rooms?.join ?? {}); @@ -81,7 +92,7 @@ export class SyncResponseManager implements Disposable { } private async afterWrite(nextBatch: string) { - this.onWriteComplete(); + this.callbacks.onWriteComplete(); await this.ct.machine.outgoingRequests.processAll(); @@ -95,7 +106,7 @@ export class SyncResponseManager implements Disposable { const [err] = await to(syncLatestEvent(this.ct, currentSyncToken)); if (err) console.error(err); this.firstSync = false; - this.onSyncToken(nextBatch); + this.callbacks.onSyncToken(nextBatch); } } diff --git a/src/client/context.ts b/src/client/context.ts index 45a1fd14..96aeefc4 100644 --- a/src/client/context.ts +++ b/src/client/context.ts @@ -1,8 +1,9 @@ -import { ComputedObservableValue, ObservableValue } from 'neurone/lib/utils/observable'; +import { ComputedObservableValue, Observable, ObservableValue } from 'neurone/lib/utils/observable'; import { MatrixRequest } 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 { MessageEventWithoutRoomId, RoomSummary, @@ -103,6 +104,7 @@ export type ClientContext = { roomToTimeline: ObservableValue>; pendingMemberRooms: ObservableValue; + verificationRequest: Observable; sync: SyncContext; requestAuthJSON: MatrixRequest; getTxnId: GetTxnId; @@ -878,3 +880,18 @@ export const createPendingMemberRoomObservable = async ( disposer.collect(pendingMemberRoomsObj, pendingMemberRoomsObservable); return pendingMemberRoomsObservable; }; + +export const createVerificationRequestObservable = () => { + const publishKey = crypto.randomUUID(); + const verificationRequest = new Observable( + publishKey + ); + const onVerificationRequest = (v: InboundVerification | OutboundVerification) => { + verificationRequest.publish(publishKey, v); + }; + + return { + verificationRequest, + onVerificationRequest, + }; +}; diff --git a/src/client/initialize.ts b/src/client/initialize.ts index e9b6ba48..91e6a276 100644 --- a/src/client/initialize.ts +++ b/src/client/initialize.ts @@ -22,6 +22,7 @@ import { createSpaceMapping, createStateContext, createSyncTokenObservable, + createVerificationRequestObservable, } from './context'; import { createStorage } from './storage'; import { getAuthJSONRequest } from './request'; @@ -69,6 +70,8 @@ export const initializeClient = async (session: Session): Promise const syncTokenObservable = await createSyncTokenObservable(storage, disposer); const pendingMemberRooms = await createPendingMemberRoomObservable(storage, disposer); + const { verificationRequest, onVerificationRequest } = createVerificationRequestObservable(); + const getTxnId: GetTxnId = getTxnIdFunc(); const [syncObservable, sync] = createSync(requestAuthJSON); @@ -125,6 +128,7 @@ export const initializeClient = async (session: Session): Promise roomToTimeline, pendingMemberRooms, + verificationRequest, sync: { observable: syncObservable, controller: syncController, @@ -137,12 +141,11 @@ export const initializeClient = async (session: Session): Promise const machineManager = new MachineManager(ct); - const syncResponseManager = new SyncResponseManager( - ct, - syncTokenObservable, + const syncResponseManager = new SyncResponseManager(ct, syncTokenObservable, { onWriteComplete, - onSyncToken - ); + onSyncToken, + onVerificationRequest, + }); disposer.collect( decryption, @@ -152,6 +155,7 @@ export const initializeClient = async (session: Session): Promise logoutObservable, logoutManager, machineManager, + verificationRequest, syncResponseManager );