Device manage - handle sessions that don't support encryption (#9717)

* add handling for unverifiable sessions

* test

* update types for filtervariation

* strict fixes

* avoid setting up cross signing in device man tests
This commit is contained in:
Kerry 2022-12-09 10:52:00 +13:00 committed by GitHub
parent 6150b86421
commit 888e69f39a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 105 deletions

View file

@ -32,6 +32,7 @@ const VariationIcon: Record<DeviceSecurityVariation, React.FC<React.SVGProps<SVG
[DeviceSecurityVariation.Inactive]: InactiveIcon,
[DeviceSecurityVariation.Verified]: VerifiedIcon,
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
[DeviceSecurityVariation.Unverifiable]: UnverifiedIcon,
};
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {

View file

@ -56,6 +56,26 @@ const securityCardContent: Record<DeviceSecurityVariation, {
</p>
</>,
},
// unverifiable uses single-session case
// because it is only ever displayed on a single session detail
[DeviceSecurityVariation.Unverifiable]: {
title: _t('Unverified session'),
description: <>
<p>{ _t(`This session doesn't support encryption, so it can't be verified.`) }
</p>
<p>
{ _t(
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
)
}
</p><p>
{ _t(
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
)
}
</p>
</>,
},
[DeviceSecurityVariation.Inactive]: {
title: _t('Inactive sessions'),
description: <>

View file

@ -30,18 +30,33 @@ interface Props {
onVerifyDevice?: () => void;
}
export const DeviceVerificationStatusCard: React.FC<Props> = ({
device,
onVerifyDevice,
}) => {
const securityCardProps = device.isVerified ? {
variation: DeviceSecurityVariation.Verified,
heading: _t('Verified session'),
description: <>
{ _t('This session is ready for secure messaging.') }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
</>,
} : {
const getCardProps = (device: ExtendedDevice): {
variation: DeviceSecurityVariation;
heading: string;
description: React.ReactNode;
} => {
if (device.isVerified) {
return {
variation: DeviceSecurityVariation.Verified,
heading: _t('Verified session'),
description: <>
{ _t('This session is ready for secure messaging.') }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
</>,
};
}
if (device.isVerified === null) {
return {
variation: DeviceSecurityVariation.Unverified,
heading: _t('Unverified session'),
description: <>
{ _t(`This session doesn't support encryption and thus can't be verified.`) }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
</>,
};
}
return {
variation: DeviceSecurityVariation.Unverified,
heading: _t('Unverified session'),
description: <>
@ -49,10 +64,19 @@ export const DeviceVerificationStatusCard: React.FC<Props> = ({
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
</>,
};
};
export const DeviceVerificationStatusCard: React.FC<Props> = ({
device,
onVerifyDevice,
}) => {
const securityCardProps = getCardProps(device);
return <DeviceSecurityCard
{...securityCardProps}
>
{ !device.isVerified && !!onVerifyDevice &&
{ /* check for explicit false to exclude unverifiable devices */ }
{ device.isVerified === false && !!onVerifyDevice &&
<AccessibleButton
kind='primary'
onClick={onVerifyDevice}

View file

@ -27,6 +27,7 @@ import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
import DeviceSecurityCard from './DeviceSecurityCard';
import {
filterDevicesBySecurityRecommendation,
FilterVariation,
INACTIVE_DEVICE_AGE_DAYS,
} from './filter';
import SelectableDeviceTile from './SelectableDeviceTile';
@ -47,8 +48,8 @@ interface Props {
expandedDeviceIds: ExtendedDevice['device_id'][];
signingOutDeviceIds: ExtendedDevice['device_id'][];
selectedDeviceIds: ExtendedDevice['device_id'][];
filter?: DeviceSecurityVariation;
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
filter?: FilterVariation;
onFilterChange: (filter: FilterVariation | undefined) => void;
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
saveDeviceName: DevicesState['saveDeviceName'];
@ -68,12 +69,12 @@ const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right:
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVariation) =>
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
.sort(sortDevicesByLatestActivityThenDisplayName);
const ALL_FILTER_ID = 'ALL';
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
const securityCardContent: Record<DeviceSecurityVariation, {
title: string;
@ -90,6 +91,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
`sign out from those you don't recognize or use anymore.`,
),
},
[DeviceSecurityVariation.Unverifiable]: {
title: _t('Unverified session'),
description: _t(
`This session doesn't support encryption and thus can't be verified.`,
),
},
[DeviceSecurityVariation.Inactive]: {
title: _t('Inactive sessions'),
description: _t(
@ -100,8 +107,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
},
};
const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation =>
Object.values<string>(DeviceSecurityVariation).includes(filter);
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
!!filter && ([
DeviceSecurityVariation.Inactive,
DeviceSecurityVariation.Unverified,
DeviceSecurityVariation.Verified,
] as string[]).includes(filter);
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
if (isSecurityVariation(filter)) {
@ -124,7 +135,7 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
return null;
};
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
const getNoResultsMessage = (filter?: FilterVariation): string => {
switch (filter) {
case DeviceSecurityVariation.Verified:
return _t('No verified sessions found.');
@ -136,7 +147,7 @@ const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
return _t('No sessions found.');
}
};
interface NoResultsProps { filter?: DeviceSecurityVariation, clearFilter: () => void}
interface NoResultsProps { filter?: FilterVariation, clearFilter: () => void}
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
<div className='mx_FilteredDeviceList_noResults'>
{ getNoResultsMessage(filter) }
@ -273,7 +284,7 @@ export const FilteredDeviceList =
];
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as FilterVariation);
};
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;

View file

@ -21,7 +21,7 @@ import AccessibleButton from '../../elements/AccessibleButton';
import SettingsSubsection from '../shared/SettingsSubsection';
import DeviceSecurityCard from './DeviceSecurityCard';
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
import {
DeviceSecurityVariation,
ExtendedDevice,
@ -31,7 +31,7 @@ import {
interface Props {
devices: DevicesDictionary;
currentDeviceId: ExtendedDevice['device_id'];
goToFilteredList: (filter: DeviceSecurityVariation) => void;
goToFilteredList: (filter: FilterVariation) => void;
}
const SecurityRecommendations: React.FC<Props> = ({

View file

@ -22,10 +22,14 @@ const MS_DAY = 24 * 60 * 60 * 1000;
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
export type FilterVariation = DeviceSecurityVariation.Verified
| DeviceSecurityVariation.Inactive
| DeviceSecurityVariation.Unverified;
export const isDeviceInactive: DeviceFilterCondition = device =>
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
const filters: Record<FilterVariation, DeviceFilterCondition> = {
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
@ -33,7 +37,7 @@ const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
export const filterDevicesBySecurityRecommendation = (
devices: ExtendedDevice[],
securityVariations: DeviceSecurityVariation[],
securityVariations: FilterVariation[],
) => {
const activeFilters = securityVariations.map(variation => filters[variation]);
if (!activeFilters.length) {

View file

@ -32,4 +32,7 @@ export enum DeviceSecurityVariation {
Verified = 'Verified',
Unverified = 'Unverified',
Inactive = 'Inactive',
// sessions that do not support encryption
// eg a session that logged in via api to get an access token
Unverifiable = 'Unverifiable'
}

View file

@ -29,7 +29,7 @@ import { useOwnDevices } from '../../devices/useOwnDevices';
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
import SecurityRecommendations from '../../devices/SecurityRecommendations';
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
import { ExtendedDevice } from '../../devices/types';
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
import SettingsTab from '../SettingsTab';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
@ -37,6 +37,7 @@ import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import SettingsStore from '../../../../../settings/SettingsStore';
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
import QuestionDialog from '../../../dialogs/QuestionDialog';
import { FilterVariation } from '../../devices/filter';
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
const { finished } = Modal.createDialog(QuestionDialog, {
@ -123,7 +124,7 @@ const SessionManagerTab: React.FC = () => {
setPushNotifications,
supportsMSC3881,
} = useOwnDevices();
const [filter, setFilter] = useState<DeviceSecurityVariation>();
const [filter, setFilter] = useState<FilterVariation>();
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
@ -142,7 +143,7 @@ const SessionManagerTab: React.FC = () => {
}
};
const onGoToFilteredList = (filter: DeviceSecurityVariation) => {
const onGoToFilteredList = (filter: FilterVariation) => {
setFilter(filter);
clearTimeout(scrollIntoViewTimeoutRef.current);
// wait a tick for the filtered section to rerender with different height