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

View file

@ -1801,6 +1801,10 @@
"Unverified sessions": "Unverified sessions",
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
"You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.",
"Unverified session": "Unverified session",
"This session doesn't support encryption, so it can't be verified.": "This session doesn't support encryption, so it can't be verified.",
"You won't be able to participate in rooms where encryption is enabled when using this session.": "You won't be able to participate in rooms where encryption is enabled when using this session.",
"For best security and privacy, it is recommended to use Matrix clients that support encryption.": "For best security and privacy, it is recommended to use Matrix clients that support encryption.",
"Inactive sessions": "Inactive sessions",
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
"Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.",
@ -1813,7 +1817,7 @@
"Unknown session type": "Unknown session type",
"Verified session": "Verified session",
"This session is ready for secure messaging.": "This session is ready for secure messaging.",
"Unverified session": "Unverified session",
"This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.",
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
"Verify session": "Verify session",
"For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.",