move device verification setup and reset to new sdk also fix OAuth UIA

This commit is contained in:
Ajay Bura 2026-08-09 15:15:38 +05:30
commit 613d6006ca
12 changed files with 443 additions and 264 deletions

View file

@ -2,10 +2,10 @@ import React, { ReactNode } from 'react';
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 { OAuthStage, PasswordStage, SSOStage } from './uia-stages';
import { useClient } from '../hooks/useClient';
export const SUPPORTED_IN_APP_UIA_STAGES = [AuthType.Password, AuthType.Sso];
export const SUPPORTED_IN_APP_UIA_STAGES = [AuthType.Password, AuthType.Sso, AuthType.OAuth];
type ActionUIAProps = {
authData: AuthResponse;
@ -50,6 +50,9 @@ export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIA
submitAuthDict={action}
/>
)}
{stageToComplete.type === AuthType.OAuth && (
<OAuthStage stageData={stageToComplete} onCancel={onCancel} submitAuthDict={action} />
)}
</UIAFlowOverlay>
);
}

View file

@ -1,4 +1,11 @@
import React, { FormEventHandler, forwardRef, useCallback, useState } from 'react';
import React, {
FormEventHandler,
forwardRef,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
Dialog,
Header,
@ -12,154 +19,236 @@ import {
Chip,
color,
Spinner,
ProgressBar,
} from 'folds';
import FileSaver from 'file-saver';
import to from 'await-to-js';
import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
import { BackupManager } from 'neurone/lib/crypto/machine/backup';
import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
import {
AesHmacSha2,
AesHmacSha2KeyDescription,
DefaultKey,
KeyStorage,
Passphrase,
RecoveryKey,
Secret,
SECRET_STORAGE_ALGORITHM_V1_AES_HMAC_SHA2,
} from 'neurone/lib/crypto/ssss';
import {
MatrixRequestRetrier,
MatrixUIAData,
MatrixUIAHandler,
randomString,
sleep,
} from 'neurone/lib/utils';
import { postVersion, PostVersionBody } from 'neurone/lib/api/matrix/client/v3/roomKeys';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { postDeviceSigningUpload } from 'neurone/lib/api/matrix/client/v3/keys';
import { PasswordInput } from './password-input';
import { ContainerColor } from '../styles/ContainerColor.css';
import { copyToClipboard } from '../utils/dom';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { clearSecretStorageKeys } from '../../client/secretStorageKeys';
import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA';
import { useMatrixClient } from '../hooks/useMatrixClient';
import { useAlive } from '../hooks/useAlive';
import { UseStateProvider } from './UseStateProvider';
import { useClient } from '../hooks/useClient';
import { getMxIdLocalPart, getMxIdServer } from '../utils/matrix';
import { AccountDataType } from '../../types/matrix/accountData';
import { useAlive } from '../hooks/useAlive';
type UIACallback<T> = (
authDict: AuthDict | null
) => Promise<[IAuthData, undefined] | [undefined, T]>;
type PerformAction<T> = (authDict: AuthDict | null) => Promise<T>;
type UIAAction<T> = {
authData: IAuthData;
callback: UIACallback<T>;
cancelCallback: () => void;
type SetupProgressChecklist = {
bootstrapCrossSinging?: boolean;
putMasterAccountData?: boolean;
putSelfAccountData?: boolean;
putUserAcountData?: boolean;
verfiyOwnDevice?: boolean;
postBackupVersion?: boolean;
putBackupAccountData?: boolean;
putRecoveryKeyAccountData?: boolean;
putDefaultKeyAccountData?: boolean;
};
function makeUIAAction<T>(
authData: IAuthData,
performAction: PerformAction<T>,
resolve: (data: T) => void,
reject: (error?: any) => void
): UIAAction<T> {
const action: UIAAction<T> = {
authData,
callback: async (authDict) => {
const [error, data] = await to<T, MatrixError | Error>(performAction(authDict));
if (error instanceof MatrixError && error.httpStatus === 401) {
return [error.data as IAuthData, undefined];
}
if (error) {
reject(error);
throw error;
}
resolve(data);
return [undefined, data];
},
cancelCallback: reject,
};
return action;
}
type SetupVerificationProps = {
onComplete: (recoveryKey: string) => void;
};
function SetupVerification({ onComplete }: SetupVerificationProps) {
const mx = useMatrixClient();
const ct = useClient();
const uia = useMemo(() => new MatrixUIAHandler(), []);
const [uiaData, setUIAData] = useState<MatrixUIAData>();
const [setupProgress, setSetupProgress] = useState<SetupProgressChecklist>();
const alive = useAlive();
const [uiaAction, setUIAAction] = useState<UIAAction<void>>();
const [nextAuthData, setNextAuthData] = useState<IAuthData | null>(); // null means no next action.
useEffect(() => uia.onUIA.subscribe(setUIAData), [uia]);
const handleAction = useCallback(
async (authDict: AuthDict) => {
if (!uiaAction) {
throw new Error('Unexpected Error! UIA action is perform without data.');
}
if (alive()) {
setNextAuthData(null);
}
const [authData] = await uiaAction.callback(authDict);
if (alive() && authData) {
setNextAuthData(authData);
}
(authDict: object) => {
setUIAData(undefined);
uia.retry(authDict);
},
[uiaAction, alive]
[uia]
);
const resetUIA = useCallback(() => {
if (!alive()) return;
setUIAAction(undefined);
setNextAuthData(undefined);
}, [alive]);
const authUploadDeviceSigningKeys: UIAuthCallback<void> = useCallback(
(makeRequest) =>
new Promise<void>((resolve, reject) => {
makeRequest(null)
.then(() => {
resolve();
resetUIA();
})
.catch((error) => {
if (error instanceof MatrixError && error.httpStatus === 401) {
const authData = error.data as IAuthData;
const action = makeUIAAction(
authData,
makeRequest as PerformAction<void>,
resolve,
(err) => {
resetUIA();
reject(err);
}
);
if (alive()) {
setUIAAction(action);
} else {
reject(new Error('Authentication failed! Failed to setup device verification.'));
}
return;
}
reject(error);
});
}),
[alive, resetUIA]
);
const handleCancel = useCallback(() => {
setUIAData(undefined);
uia.clear();
}, [uia]);
const [setupState, setup] = useAsyncCallback<void, Error, [string | undefined]>(
useCallback(
async (passphrase) => {
const crypto = mx.getCrypto();
if (!crypto) throw new Error('Unexpected Error! Crypto module not found!');
setSetupProgress({});
const retrier = new MatrixRequestRetrier(sleep);
const recoveryKeyData = await crypto.createRecoveryKeyFromPassphrase(passphrase);
if (!recoveryKeyData.encodedPrivateKey) {
throw new Error('Unexpected Error! Failed to create recovery key.');
const defaultKeyId = randomString(16);
const passData = passphrase ? await Passphrase.create(passphrase) : undefined;
const recoveryKey = passData?.key ?? AesHmacSha2.generateRawKey();
const uploadDeviceSigningKeys = async (bodyStr: string) => {
const body = JSON.parse(bodyStr);
const resp = await retrier.execute(() =>
uia.execute((authData) =>
ct.requestAuthJSON(postDeviceSigningUpload({ body: { ...body, auth: authData } }))
)
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
};
await ct.machine.crossSigning.bootstrapCrossSigning(uploadDeviceSigningKeys);
const crossSigning = await ct.machine.crossSigning.exportKeys();
if (!crossSigning || !crossSigning.master || !crossSigning.self || !crossSigning.user) {
throw new Error('Failed to reset cross signing. No cross signing keys available.');
}
clearSecretStorageKeys();
if (alive()) setSetupProgress((v) => ({ ...v, bootstrapCrossSinging: true }));
await crypto.bootstrapSecretStorage({
createSecretStorageKey: async () => recoveryKeyData,
setupNewSecretStorage: true,
});
const masterEnc = await AesHmacSha2.encrypt(
recoveryKey,
AccountDataType.CrossSigningMaster,
crossSigning.master
);
const selfEnc = await AesHmacSha2.encrypt(
recoveryKey,
AccountDataType.CrossSigningSelf,
crossSigning.self
);
const userEnc = await AesHmacSha2.encrypt(
recoveryKey,
AccountDataType.CrossSigningUser,
crossSigning.user
);
await crypto.bootstrapCrossSigning({
authUploadDeviceSigningKeys,
setupNewCrossSigning: true,
});
const { userId } = ct.session;
const masterSecret: Secret = { encrypted: { [defaultKeyId]: masterEnc } };
const selfSecret: Secret = { encrypted: { [defaultKeyId]: selfEnc } };
const userSecret: Secret = { encrypted: { [defaultKeyId]: userEnc } };
await retrier.execute(() =>
ct.requestAuthJSON(
putAccountData({
pathParams: { userId, type: AccountDataType.CrossSigningMaster },
body: masterSecret,
})
)
);
if (alive()) setSetupProgress((v) => ({ ...v, putMasterAccountData: true }));
await retrier.execute(() =>
ct.requestAuthJSON(
putAccountData({
pathParams: { userId, type: AccountDataType.CrossSigningSelf },
body: selfSecret,
})
)
);
if (alive()) setSetupProgress((v) => ({ ...v, putSelfAccountData: true }));
await retrier.execute(() =>
ct.requestAuthJSON(
putAccountData({
pathParams: { userId, type: AccountDataType.CrossSigningUser },
body: userSecret,
})
)
);
if (alive()) setSetupProgress((v) => ({ ...v, putUserAcountData: true }));
await crypto.resetKeyBackup();
const device = await ct.machine.userTracker.getDevice(
ct.session.userId,
ct.session.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 ct.machine.outgoingRequests.queryKeys([ct.session.userId]);
}
if (alive()) setSetupProgress((v) => ({ ...v, verfiyOwnDevice: true }));
onComplete(recoveryKeyData.encodedPrivateKey);
const backupKey = BackupManager.createBackupKey();
await ct.megolmBackup.cacheDecryptionKey(backupKey.privateKey);
const backupEnc = await AesHmacSha2.encrypt(
recoveryKey,
AccountDataType.MegolmBackupV1,
backupKey.privateKey
);
const backupAuthData = {
public_key: backupKey.publicKey,
};
await ct.machine.sign(backupAuthData);
const backupVersion: PostVersionBody = {
algorithm: backupKey.algorithm,
auth_data: backupAuthData,
};
await retrier.execute(() => ct.requestAuthJSON(postVersion({ body: backupVersion })));
if (alive()) setSetupProgress((v) => ({ ...v, postBackupVersion: true }));
const backupSecret: Secret = { encrypted: { [defaultKeyId]: backupEnc } };
await retrier.execute(() =>
ct.requestAuthJSON(
putAccountData({
pathParams: { userId, type: AccountDataType.MegolmBackupV1 },
body: backupSecret,
})
)
);
if (alive()) setSetupProgress((v) => ({ ...v, putBackupAccountData: true }));
const { iv, mac } = await AesHmacSha2.driveKeyMetaData(recoveryKey);
const keyDescription: AesHmacSha2KeyDescription = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES_HMAC_SHA2,
passphrase: passData?.passphrase,
iv,
mac,
};
await retrier.execute(() =>
ct.requestAuthJSON(
putAccountData({
pathParams: { userId, type: KeyStorage.keyIdentifier(defaultKeyId) },
body: keyDescription,
})
)
);
if (alive()) setSetupProgress((v) => ({ ...v, putRecoveryKeyAccountData: true }));
const defaultKey: DefaultKey = {
key: defaultKeyId,
};
await retrier.execute(() =>
ct.requestAuthJSON(
putAccountData({
pathParams: { userId, type: KeyStorage.defaultKeyIdentifier() },
body: defaultKey,
})
)
);
if (alive()) setSetupProgress((v) => ({ ...v, putDefaultKeyAccountData: true }));
onComplete(RecoveryKey.encode(recoveryKey));
},
[mx, onComplete, authUploadDeviceSigningKeys]
[ct, uia, onComplete, alive]
)
);
@ -179,43 +268,79 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
setup(passphrase);
};
const setupSteps: {
key: keyof SetupProgressChecklist;
label: string;
}[] = useMemo(
() => [
{ key: 'bootstrapCrossSinging', label: 'Setting up cross-signing' },
{ key: 'putMasterAccountData', label: 'Securing your master signing key' },
{ key: 'putSelfAccountData', label: 'Securing your self signing key' },
{ key: 'putUserAcountData', label: 'Securing your user signing key' },
{ key: 'verfiyOwnDevice', label: 'Verifying this device' },
{ key: 'postBackupVersion', label: 'Creating your secure backup' },
{ key: 'putBackupAccountData', label: 'Securing your backup key' },
{ key: 'putRecoveryKeyAccountData', label: 'Saving your recovery key information' },
{ key: 'putDefaultKeyAccountData', label: 'Setting up your recovery key' },
],
[]
);
const completedSteps = setupSteps.filter(({ key }) => setupProgress?.[key] === true).length;
return (
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="400">
<Text size="T300">
Generate a <b>Recovery Key</b> for verifying identity if you do not have access to other
devices. Additionally, setup a passphrase as a memorable alternative.
</Text>
<Box direction="Column" gap="100">
<Text size="L400">Passphrase (Optional)</Text>
<PasswordInput name="passphraseInput" size="400" readOnly={loading} />
</Box>
<Button
type="submit"
disabled={loading}
before={loading && <Spinner size="200" variant="Primary" fill="Solid" />}
>
<Text size="B400">Continue</Text>
</Button>
{setupProgress ? (
<Box direction="Column" gap="400">
<Text size="L400">Setup progress</Text>
<Box style={{ paddingLeft: config.space.S200 }} direction="Column" gap="100">
{setupSteps.map(({ key, label }) => (
<Text size="T200" key={key}>
{setupProgress[key] ? '✅' : '⏳'} {label}
</Text>
))}
</Box>
<ProgressBar variant="Secondary" size="300" min={0} value={completedSteps} />
</Box>
) : (
<>
<Text size="T300">
Generate a <b>Recovery Key</b> for verifying identity if you do not have access to other
devices. Additionally, setup a passphrase as a memorable alternative.
</Text>
<Box direction="Column" gap="100">
<Text size="L400">Passphrase (Optional)</Text>
<PasswordInput name="passphraseInput" size="400" readOnly={loading} />
</Box>
<Button
type="submit"
disabled={loading}
before={loading && <Spinner size="200" variant="Primary" fill="Solid" />}
>
<Text size="B400">Continue</Text>
</Button>
</>
)}
{setupState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>{setupState.error ? setupState.error.message : 'Unexpected Error!'}</b>
</Text>
)}
{nextAuthData !== null && uiaAction && (
{uiaData && (
<ActionUIAFlowsLoader
authData={nextAuthData ?? uiaAction.authData}
authData={uiaData}
unsupported={() => (
<Text size="T200">
<Text size="T200" style={{ color: color.Critical.Main }}>
Authentication steps to perform this action are not supported by client.
</Text>
)}
>
{(ongoingFlow) => (
<ActionUIA
authData={nextAuthData ?? uiaAction.authData}
authData={uiaData}
ongoingFlow={ongoingFlow}
action={handleAction}
onCancel={uiaAction.cancelCallback}
onCancel={handleCancel}
/>
)}
</ActionUIAFlowsLoader>
@ -228,6 +353,7 @@ type RecoveryKeyDisplayProps = {
recoveryKey: string;
};
function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
const ct = useClient();
const [show, setShow] = useState(false);
const handleCopy = () => {
@ -235,10 +361,13 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
};
const handleDownload = () => {
const username = getMxIdLocalPart(ct.session.userId);
const servername = getMxIdServer(ct.session.userId);
const blob = new Blob([recoveryKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
FileSaver.saveAs(blob, `Recovery Key - ${username} (${servername}).txt`);
};
const safeToDisplayKey = show ? recoveryKey : recoveryKey.replace(/[^\s]/g, '*');
@ -270,12 +399,12 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
</Box>
</Box>
<Box direction="Column" gap="200">
<Button onClick={handleCopy}>
<Text size="B400">Copy</Text>
</Button>
<Button onClick={handleDownload} fill="Soft">
<Button onClick={handleDownload}>
<Text size="B400">Download</Text>
</Button>
<Button onClick={handleCopy} fill="Soft">
<Text size="B400">Copy</Text>
</Button>
</Box>
</Box>
);

View file

@ -1,11 +1,11 @@
import React, { FormEventHandler, useCallback } from 'react';
import { Box, Text, Button, Spinner, color } from 'folds';
import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api';
import {
AesHmacSha2,
AesHmacSha2KeyDescription,
Passphrase,
PassphraseInfo,
RecoveryKey,
} from 'neurone/lib/crypto/ssss';
import { PasswordInput } from './password-input';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
@ -129,7 +129,7 @@ export function SecretStorageRecoveryKey({
>(
useCallback(
async (recoveryKey) => {
const decodedRecoveryKey = decodeRecoveryKey(recoveryKey);
const decodedRecoveryKey = RecoveryKey.decode(recoveryKey);
const { mac: regeneratedMac } = await AesHmacSha2.driveKeyMetaData(
decodedRecoveryKey,

View file

@ -0,0 +1,78 @@
import { Box, Button, color, config, Dialog, Header, Icon, IconButton, Icons, Text } from 'folds';
import React, { useCallback } from 'react';
import { ObjectReader } from 'neurone/lib/utils';
import { StageComponentProps } from './types';
export function OAuthStage({ stageData, submitAuthDict, onCancel }: StageComponentProps) {
const { errorCode, error, session, info } = stageData;
const url = ObjectReader.string(info, 'url');
const handleSubmit = useCallback(() => {
submitAuthDict({
session,
});
}, [submitAuthDict, session]);
return (
<Dialog>
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Reset encryption identity</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box
style={{ padding: `0 ${config.space.S400} ${config.space.S400}` }}
direction="Column"
gap="400"
>
<Text size="T200">
To reset your end-to-end encryption cross-signing identity, you need to approve this
request from your account dashboard.
</Text>
<Box direction="Column" gap="100">
<Text size="T200">
<b>1.</b> Open your account dashboard and approve the request.
</Text>
<Text size="T200">
<b>2.</b> Return here after approving and Continue.
</Text>
</Box>
{errorCode && (
<Box alignItems="Center" gap="100" style={{ color: color.Critical.Main }}>
<Icon size="50" src={Icons.Warning} filled />
<Text size="T200">
<b>{`${errorCode}: ${error}`}</b>
</Text>
</Box>
)}
<Box direction="Column" gap="300">
<Button
variant="Secondary"
onClick={() => window.open(url, '_blank', 'noopener,noreferrer')}
>
<Text as="span" size="B400">
Open account dashboard
</Text>
</Button>
<Button variant="Primary" onClick={handleSubmit}>
<Text as="span" size="B400">
Continue
</Text>
</Button>
</Box>
</Box>
</Dialog>
);
}

View file

@ -6,3 +6,4 @@ export * from './ReCaptchaStage';
export * from './RegistrationTokenStage';
export * from './SSOStage';
export * from './TermsStage';
export * from './OAuthStage';

View file

@ -9,7 +9,6 @@ import { LocalBackup } from './LocalBackup';
import { DeviceLogoutBtn, DeviceKeyDetails, DeviceTile, DeviceTilePlaceholder } from './DeviceTile';
import {
DeviceVerificationOptions,
EnableVerification,
VerificationStatusBadge,
VerifyCurrentDeviceTile,
} from './Verification';
@ -93,17 +92,15 @@ export function Devices({ requestClose }: DevicesProps) {
title="Device Verification"
description="To verify device identity and grant access to encrypted messages."
after={
crossSigning ? (
<Box gap="200" alignItems="Center">
<Box gap="200" alignItems="Center">
{crossSigning && (
<VerificationStatusBadge
verificationStatus={verificationStatus}
otherUnverifiedCount={unverifiedDeviceCount}
/>
<DeviceVerificationOptions />
</Box>
) : (
<EnableVerification />
)
)}
<DeviceVerificationOptions enabled={!!crossSigning} />
</Box>
}
/>
</SequenceCard>

View file

@ -31,9 +31,6 @@ import {
DeviceVerificationSetup,
} from '../../../components/DeviceVerificationSetup';
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 = {
@ -222,46 +219,15 @@ export function VerifyOtherDeviceTile({ deviceId }: VerifyOtherDeviceTileProps)
);
}
export function EnableVerification() {
const [open, setOpen] = useState(false);
const handleCancel = useCallback(() => setOpen(false), []);
return (
<>
<Button size="300" radii="300" onClick={() => setOpen(true)}>
<Text as="span" size="B300">
Enable
</Text>
</Button>
{open && (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
clickOutsideDeactivates: false,
escapeDeactivates: false,
}}
>
<DeviceVerificationSetup onCancel={handleCancel} />
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
</>
);
}
export function DeviceVerificationOptions() {
export function DeviceVerificationOptions({ enabled }: { enabled: boolean }) {
const [menuCords, setMenuCords] = useState<RectCords>();
const authMetadata = useAuthMetadata();
const accountManagementActions = useAccountManagementActions();
const [reset, setReset] = useState(false);
const [setup, setSetup] = useState(false);
const handleCancelReset = useCallback(() => {
const handleCancel = useCallback(() => {
setReset(false);
setSetup(false);
}, []);
const handleMenu: MouseEventHandler<HTMLButtonElement> = (event) => {
@ -270,32 +236,31 @@ export function DeviceVerificationOptions() {
const handleReset = () => {
setMenuCords(undefined);
if (authMetadata) {
const authUrl = authMetadata.account_management_uri ?? authMetadata.issuer;
window.open(
withSearchParam(authUrl, {
action: accountManagementActions.crossSigningReset,
}),
'_blank'
);
return;
}
setReset(true);
};
const handleEnable = () => {
setSetup(true);
};
return (
<>
<IconButton
aria-pressed={!!menuCords}
variant="SurfaceVariant"
size="300"
radii="300"
onClick={handleMenu}
>
<Icon size="100" src={Icons.VerticalDots} />
</IconButton>
{enabled ? (
<IconButton
aria-pressed={!!menuCords}
variant="SurfaceVariant"
size="300"
radii="300"
onClick={handleMenu}
>
<Icon size="100" src={Icons.VerticalDots} />
</IconButton>
) : (
<Button size="300" radii="300" onClick={handleEnable}>
<Text as="span" size="B300">
Enable
</Text>
</Button>
)}
<PopOut
anchor={menuCords}
offset={5}
@ -332,6 +297,21 @@ export function DeviceVerificationOptions() {
</FocusTrap>
}
/>
{setup && (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
clickOutsideDeactivates: false,
escapeDeactivates: false,
}}
>
<DeviceVerificationSetup onCancel={handleCancel} />
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
{reset && (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
@ -342,7 +322,7 @@ export function DeviceVerificationOptions() {
escapeDeactivates: false,
}}
>
<DeviceVerificationReset onCancel={handleCancelReset} />
<DeviceVerificationReset onCancel={handleCancel} />
</FocusTrap>
</OverlayCenter>
</Overlay>

View file

@ -66,7 +66,7 @@ export const useUIAFlow = (authData: AuthResponse, uiaFlow: UIAFlow): UIAFlowInt
const nextStage = stages.find((stage) => !completed.includes(stage));
if (!nextStage) return undefined;
const info = params[nextStage];
const info = params[nextStage] as Record<string, unknown>;
return {
type: nextStage,
@ -86,7 +86,7 @@ export const useUIAFlow = (authData: AuthResponse, uiaFlow: UIAFlow): UIAFlowInt
(stageType: string): StageInfo | undefined => {
if (!hasStage(stageType)) return undefined;
return params[stageType];
return params[stageType] as Record<string, unknown>;
},
[hasStage, params]
);

View file

@ -8,17 +8,20 @@ export enum AuthType {
SsoUnstable = 'org.matrix.login.sso',
Dummy = 'm.login.dummy',
RegistrationToken = 'm.login.registration_token',
OAuth = 'm.oauth',
}
export type UIAFlow = {
stages: Array<string>;
};
export type UIAParams = Record<string, unknown>;
export type AuthResponse = {
session?: string;
completed?: string[];
flows?: UIAFlow[];
params?: Record<string, Record<string, any>>;
params?: UIAParams;
};
export const getSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] => {
@ -34,7 +37,6 @@ export const getUIACompleted = (authData: AuthResponse): string[] => {
return completed;
};
export type UIAParams = Record<string, Record<string, unknown>>;
export const getUIAParams = (authData: AuthResponse): UIAParams => {
const params = authData.params ?? {};
return params;
@ -89,7 +91,12 @@ export const requiredStageInFlows = (uiaFlows: UIAFlow[], stage: string) =>
export const getLoginTermUrl = (params: UIAParams): string | undefined => {
const terms = params[AuthType.Terms];
if (terms && 'policies' in terms && typeof terms.policies === 'object') {
if (
typeof terms === 'object' &&
terms &&
'policies' in terms &&
typeof terms.policies === 'object'
) {
if (terms.policies === null) return undefined;
if ('privacy_policy' in terms.policies && typeof terms.policies.privacy_policy === 'object') {
if (terms.policies.privacy_policy === null) return undefined;

View file

@ -51,6 +51,8 @@ export class MegolmBackup implements Disposable {
public readonly backupInProgress: ObservableValue<boolean>;
private cachedDecryptionKey?: string;
constructor(
private readonly machine: Machine,
private readonly request: MatrixRequest,
@ -93,10 +95,23 @@ export class MegolmBackup implements Disposable {
const versionChanged = prevBackup && prevBackup.version !== backupInfo.version;
if (versionChanged && this.keyBackup.read) {
this.machine.backupManager.disableBackup();
await this.machine.backupManager.disableBackup();
this.keyBackup.publish(this.publishKey, undefined);
await this.start(backupInfo);
return;
}
this.start(backupInfo);
if (this.keyBackup.read) return;
await this.start(backupInfo);
}
/**
* cache the decryption key when you are expecting a BackupVersionInfo update.
*
* @param secret backup private key
*/
public async cacheDecryptionKey(secret: string): Promise<void> {
this.cachedDecryptionKey = secret;
}
public async saveDecryptionKey(secret: string): Promise<void> {
@ -127,6 +142,14 @@ export class MegolmBackup implements Disposable {
this.backup(keyBackup);
if (this.cachedDecryptionKey) {
this.machine.backupManager.saveDecryptionKey(
Base64.decode(this.cachedDecryptionKey),
backupInfo
);
this.cachedDecryptionKey = undefined;
}
return keyBackup;
}

View file

@ -1,6 +1,5 @@
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
import { pushSessionToSW } from '../sw-session';
@ -28,7 +27,6 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
cryptoStore: legacyCryptoStore,
deviceId: session.deviceId,
timelineSupport: true,
cryptoCallbacks: cryptoCallbacks as any,
verificationMethods: ['m.sas.v1'],
});

View file

@ -1,37 +0,0 @@
const secretStorageKeys = new Map();
export function storePrivateKey(keyId, privateKey) {
if (privateKey instanceof Uint8Array === false) {
throw new Error('Unable to store, privateKey is invalid.');
}
secretStorageKeys.set(keyId, privateKey);
}
function hasPrivateKey(keyId) {
return secretStorageKeys.get(keyId) instanceof Uint8Array;
}
function getPrivateKey(keyId) {
return secretStorageKeys.get(keyId);
}
export function clearSecretStorageKeys() {
secretStorageKeys.clear();
}
async function getSecretStorageKey({ keys }) {
const keyIds = Object.keys(keys);
const keyId = keyIds.find(hasPrivateKey);
if (!keyId) return undefined;
const privateKey = getPrivateKey(keyId);
return [keyId, privateKey];
}
function cacheSecretStorageKey(keyId, keyInfo, privateKey) {
secretStorageKeys.set(keyId, privateKey);
}
export const cryptoCallbacks = {
getSecretStorageKey,
cacheSecretStorageKey,
};