receive verification request

This commit is contained in:
Ajay Bura 2026-08-02 11:36:30 +05:30
commit 5cbc88fbdb
5 changed files with 121 additions and 164 deletions

View file

@ -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<void>;
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<void>;
stage: ReadyVerificationStage;
};
function AutoVerificationStart({ onStart }: VerificationStartProps) {
function AutoVerificationStart({ stage }: VerificationStartProps) {
useEffect(() => {
onStart();
}, [onStart]);
stage.startSas();
}, [stage]);
return (
<Box direction="Column" gap="400">
@ -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) => (
<Box
// eslint-disable-next-line react/no-array-index-key
key={`${emoji}${name}${index}`}
key={`${emoji.description}${index}`}
direction="Column"
gap="100"
justifyContent="Center"
alignItems="Center"
>
<Text size="H1">{emoji}</Text>
<Text size="T200">{name}</Text>
<Text size="H1">{emoji.emoji}</Text>
<Text size="T200">{emoji.description}</Text>
</Box>
))}
</Box>
@ -162,7 +168,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
<Button
variant="Primary"
fill="Soft"
onClick={() => sasData.mismatch()}
onClick={() => stage.mismatch()}
disabled={confirming}
>
<Text size="B400">Do not Match</Text>
@ -173,21 +179,23 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
}
type SasVerificationProps = {
verifier: Verifier;
sas: SasMethod;
onCancel: () => void;
};
function SasVerification({ verifier, onCancel }: SasVerificationProps) {
const [sasData, setSasData] = useState<ShowSasCallbacks>();
useVerifierShowSas(verifier, setSasData);
useVerifierCancel(verifier, onCancel);
function SasVerification({ sas, onCancel }: SasVerificationProps) {
const stage = useObservableValue(sas.stage);
useEffect(() => {
verifier.verify();
}, [verifier]);
if (stage instanceof SasStartStage) {
stage.accept();
}
if (stage instanceof SasCancelState) {
onCancel();
}
}, [stage, onCancel]);
if (sasData) {
return <CompareEmoji sasData={sasData} />;
if (stage instanceof SasAcceptStage && stage.emoji) {
return <CompareEmoji stage={stage} />;
}
return (
@ -214,12 +222,13 @@ function VerificationDone({ onExit }: VerificationDoneProps) {
}
type VerificationCanceledProps = {
stage: CancelVerificationStage;
onClose: () => void;
};
function VerificationCanceled({ onClose }: VerificationCanceledProps) {
function VerificationCanceled({ stage, onClose }: VerificationCanceledProps) {
return (
<Box direction="Column" gap="400">
<Text>Verification has been canceled.</Text>
<Text>Verification has been canceled. {stage.reason}</Text>
<Button variant="Secondary" fill="Soft" onClick={onClose}>
<Text size="B400">Close</Text>
</Button>
@ -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 (
<Overlay open backdrop={<OverlayBackdrop />}>
@ -266,30 +270,27 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
{phase === VerificationPhase.Requested &&
(request.initiatedByMe ? (
<VerificationWaitAccept />
) : (
<VerificationAccept onAccept={handleAccept} />
))}
{phase === VerificationPhase.Ready &&
(request.initiatedByMe ? (
<AutoVerificationStart onStart={handleStart} />
{stage instanceof InboundRequestStage && <VerificationAccept stage={stage} />}
{stage instanceof OutboundRequestStage && <VerificationWaitAccept />}
{stage instanceof ReadyVerificationStage &&
(request instanceof OutboundRequestStage ? (
<AutoVerificationStart stage={stage} />
) : (
<VerificationWaitStart />
))}
{phase === VerificationPhase.Started &&
(request.verifier ? (
<SasVerification verifier={request.verifier} onCancel={handleCancel} />
{stage instanceof StartVerificationStage &&
(stage.method ? (
<SasVerification sas={stage.method} onCancel={handleCancel} />
) : (
<VerificationUnexpected
message="Unexpected Error! Verification is started but verifier is missing."
onClose={handleCancel}
/>
))}
{phase === VerificationPhase.Done && <VerificationDone onExit={onExit} />}
{phase === VerificationPhase.Cancelled && (
<VerificationCanceled onClose={handleCancel} />
{stage instanceof DoneVerificationStage && <VerificationDone onExit={onExit} />}
{stage instanceof CancelVerificationStage && (
<VerificationCanceled stage={stage} onClose={handleCancel} />
)}
</Box>
</Dialog>
@ -300,7 +301,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
}
export function ReceiveSelfDeviceVerification() {
const [request, setRequest] = useState<VerificationRequest>();
const [request, setRequest] = useState<InboundVerification | OutboundVerification>();
useVerificationRequestReceived(setRequest);
@ -310,7 +311,7 @@ export function ReceiveSelfDeviceVerification() {
if (!request) return null;
if (!request.isSelfVerification) {
if (!request.selfVerification) {
return null;
}

View file

@ -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]);
};

View file

@ -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<string | undefined>;
private readonly onWriteComplete: () => void;
private readonly onSyncToken: (token: string) => void;
private readonly callbacks: SyncResponseManagerCallbacks;
private readonly disposer = new Disposer();
constructor(
ct: ClientContext,
syncTokenObservable: ObservableValue<string | undefined>,
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);
}
}

View file

@ -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<Map<string, Timeline>>;
pendingMemberRooms: ObservableValue<string[]>;
verificationRequest: Observable<InboundVerification | OutboundVerification>;
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<InboundVerification | OutboundVerification>(
publishKey
);
const onVerificationRequest = (v: InboundVerification | OutboundVerification) => {
verificationRequest.publish(publishKey, v);
};
return {
verificationRequest,
onVerificationRequest,
};
};

View file

@ -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<ClientContext>
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<ClientContext>
roomToTimeline,
pendingMemberRooms,
verificationRequest,
sync: {
observable: syncObservable,
controller: syncController,
@ -137,12 +141,11 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
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<ClientContext>
logoutObservable,
logoutManager,
machineManager,
verificationRequest,
syncResponseManager
);