Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 888e69f

Browse files
author
Kerry
authored
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
1 parent 6150b86 commit 888e69f

File tree

12 files changed

+200
-105
lines changed

12 files changed

+200
-105
lines changed

cypress/e2e/settings/device-management.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ describe("Device manager", () => {
4949

5050
cy.get('[data-testid="current-session-section"]').within(() => {
5151
cy.contains('Unverified session').should('exist');
52-
cy.get('.mx_DeviceSecurityCard_actions [role="button"]').should('exist');
5352
});
5453

5554
// current session details opened

src/components/views/settings/devices/DeviceSecurityCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const VariationIcon: Record<DeviceSecurityVariation, React.FC<React.SVGProps<SVG
3232
[DeviceSecurityVariation.Inactive]: InactiveIcon,
3333
[DeviceSecurityVariation.Verified]: VerifiedIcon,
3434
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
35+
[DeviceSecurityVariation.Unverifiable]: UnverifiedIcon,
3536
};
3637

3738
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {

src/components/views/settings/devices/DeviceSecurityLearnMore.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ const securityCardContent: Record<DeviceSecurityVariation, {
5656
</p>
5757
</>,
5858
},
59+
// unverifiable uses single-session case
60+
// because it is only ever displayed on a single session detail
61+
[DeviceSecurityVariation.Unverifiable]: {
62+
title: _t('Unverified session'),
63+
description: <>
64+
<p>{ _t(`This session doesn't support encryption, so it can't be verified.`) }
65+
</p>
66+
<p>
67+
{ _t(
68+
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
69+
)
70+
}
71+
</p><p>
72+
{ _t(
73+
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
74+
)
75+
}
76+
</p>
77+
</>,
78+
},
5979
[DeviceSecurityVariation.Inactive]: {
6080
title: _t('Inactive sessions'),
6181
description: <>

src/components/views/settings/devices/DeviceVerificationStatusCard.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,53 @@ interface Props {
3030
onVerifyDevice?: () => void;
3131
}
3232

33-
export const DeviceVerificationStatusCard: React.FC<Props> = ({
34-
device,
35-
onVerifyDevice,
36-
}) => {
37-
const securityCardProps = device.isVerified ? {
38-
variation: DeviceSecurityVariation.Verified,
39-
heading: _t('Verified session'),
40-
description: <>
41-
{ _t('This session is ready for secure messaging.') }
42-
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
43-
</>,
44-
} : {
33+
const getCardProps = (device: ExtendedDevice): {
34+
variation: DeviceSecurityVariation;
35+
heading: string;
36+
description: React.ReactNode;
37+
} => {
38+
if (device.isVerified) {
39+
return {
40+
variation: DeviceSecurityVariation.Verified,
41+
heading: _t('Verified session'),
42+
description: <>
43+
{ _t('This session is ready for secure messaging.') }
44+
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
45+
</>,
46+
};
47+
}
48+
if (device.isVerified === null) {
49+
return {
50+
variation: DeviceSecurityVariation.Unverified,
51+
heading: _t('Unverified session'),
52+
description: <>
53+
{ _t(`This session doesn't support encryption and thus can't be verified.`) }
54+
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
55+
</>,
56+
};
57+
}
58+
59+
return {
4560
variation: DeviceSecurityVariation.Unverified,
4661
heading: _t('Unverified session'),
4762
description: <>
4863
{ _t('Verify or sign out from this session for best security and reliability.') }
4964
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
5065
</>,
5166
};
67+
};
68+
69+
export const DeviceVerificationStatusCard: React.FC<Props> = ({
70+
device,
71+
onVerifyDevice,
72+
}) => {
73+
const securityCardProps = getCardProps(device);
74+
5275
return <DeviceSecurityCard
5376
{...securityCardProps}
5477
>
55-
{ !device.isVerified && !!onVerifyDevice &&
78+
{ /* check for explicit false to exclude unverifiable devices */ }
79+
{ device.isVerified === false && !!onVerifyDevice &&
5680
<AccessibleButton
5781
kind='primary'
5882
onClick={onVerifyDevice}

src/components/views/settings/devices/FilteredDeviceList.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
2727
import DeviceSecurityCard from './DeviceSecurityCard';
2828
import {
2929
filterDevicesBySecurityRecommendation,
30+
FilterVariation,
3031
INACTIVE_DEVICE_AGE_DAYS,
3132
} from './filter';
3233
import SelectableDeviceTile from './SelectableDeviceTile';
@@ -47,8 +48,8 @@ interface Props {
4748
expandedDeviceIds: ExtendedDevice['device_id'][];
4849
signingOutDeviceIds: ExtendedDevice['device_id'][];
4950
selectedDeviceIds: ExtendedDevice['device_id'][];
50-
filter?: DeviceSecurityVariation;
51-
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
51+
filter?: FilterVariation;
52+
onFilterChange: (filter: FilterVariation | undefined) => void;
5253
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
5354
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
5455
saveDeviceName: DevicesState['saveDeviceName'];
@@ -68,12 +69,12 @@ const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right:
6869
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
6970
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
7071

71-
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
72+
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVariation) =>
7273
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
7374
.sort(sortDevicesByLatestActivityThenDisplayName);
7475

7576
const ALL_FILTER_ID = 'ALL';
76-
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
77+
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
7778

7879
const securityCardContent: Record<DeviceSecurityVariation, {
7980
title: string;
@@ -90,6 +91,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
9091
`sign out from those you don't recognize or use anymore.`,
9192
),
9293
},
94+
[DeviceSecurityVariation.Unverifiable]: {
95+
title: _t('Unverified session'),
96+
description: _t(
97+
`This session doesn't support encryption and thus can't be verified.`,
98+
),
99+
},
93100
[DeviceSecurityVariation.Inactive]: {
94101
title: _t('Inactive sessions'),
95102
description: _t(
@@ -100,8 +107,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
100107
},
101108
};
102109

103-
const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation =>
104-
Object.values<string>(DeviceSecurityVariation).includes(filter);
110+
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
111+
!!filter && ([
112+
DeviceSecurityVariation.Inactive,
113+
DeviceSecurityVariation.Unverified,
114+
DeviceSecurityVariation.Verified,
115+
] as string[]).includes(filter);
105116

106117
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
107118
if (isSecurityVariation(filter)) {
@@ -124,7 +135,7 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
124135
return null;
125136
};
126137

127-
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
138+
const getNoResultsMessage = (filter?: FilterVariation): string => {
128139
switch (filter) {
129140
case DeviceSecurityVariation.Verified:
130141
return _t('No verified sessions found.');
@@ -136,7 +147,7 @@ const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
136147
return _t('No sessions found.');
137148
}
138149
};
139-
interface NoResultsProps { filter?: DeviceSecurityVariation, clearFilter: () => void}
150+
interface NoResultsProps { filter?: FilterVariation, clearFilter: () => void}
140151
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
141152
<div className='mx_FilteredDeviceList_noResults'>
142153
{ getNoResultsMessage(filter) }
@@ -273,7 +284,7 @@ export const FilteredDeviceList =
273284
];
274285

275286
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
276-
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
287+
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as FilterVariation);
277288
};
278289

279290
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;

src/components/views/settings/devices/SecurityRecommendations.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import AccessibleButton from '../../elements/AccessibleButton';
2121
import SettingsSubsection from '../shared/SettingsSubsection';
2222
import DeviceSecurityCard from './DeviceSecurityCard';
2323
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
24-
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
24+
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
2525
import {
2626
DeviceSecurityVariation,
2727
ExtendedDevice,
@@ -31,7 +31,7 @@ import {
3131
interface Props {
3232
devices: DevicesDictionary;
3333
currentDeviceId: ExtendedDevice['device_id'];
34-
goToFilteredList: (filter: DeviceSecurityVariation) => void;
34+
goToFilteredList: (filter: FilterVariation) => void;
3535
}
3636

3737
const SecurityRecommendations: React.FC<Props> = ({

src/components/views/settings/devices/filter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,22 @@ const MS_DAY = 24 * 60 * 60 * 1000;
2222
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
2323
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
2424

25+
export type FilterVariation = DeviceSecurityVariation.Verified
26+
| DeviceSecurityVariation.Inactive
27+
| DeviceSecurityVariation.Unverified;
28+
2529
export const isDeviceInactive: DeviceFilterCondition = device =>
2630
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
2731

28-
const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
32+
const filters: Record<FilterVariation, DeviceFilterCondition> = {
2933
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
3034
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
3135
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
3236
};
3337

3438
export const filterDevicesBySecurityRecommendation = (
3539
devices: ExtendedDevice[],
36-
securityVariations: DeviceSecurityVariation[],
40+
securityVariations: FilterVariation[],
3741
) => {
3842
const activeFilters = securityVariations.map(variation => filters[variation]);
3943
if (!activeFilters.length) {

src/components/views/settings/devices/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ export enum DeviceSecurityVariation {
3232
Verified = 'Verified',
3333
Unverified = 'Unverified',
3434
Inactive = 'Inactive',
35+
// sessions that do not support encryption
36+
// eg a session that logged in via api to get an access token
37+
Unverifiable = 'Unverifiable'
3538
}

src/components/views/settings/tabs/user/SessionManagerTab.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ import { useOwnDevices } from '../../devices/useOwnDevices';
2929
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
3030
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
3131
import SecurityRecommendations from '../../devices/SecurityRecommendations';
32-
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
32+
import { ExtendedDevice } from '../../devices/types';
3333
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
3434
import SettingsTab from '../SettingsTab';
3535
import LoginWithQRSection from '../../devices/LoginWithQRSection';
3636
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
3737
import SettingsStore from '../../../../../settings/SettingsStore';
3838
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
3939
import QuestionDialog from '../../../dialogs/QuestionDialog';
40+
import { FilterVariation } from '../../devices/filter';
4041

4142
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
4243
const { finished } = Modal.createDialog(QuestionDialog, {
@@ -123,7 +124,7 @@ const SessionManagerTab: React.FC = () => {
123124
setPushNotifications,
124125
supportsMSC3881,
125126
} = useOwnDevices();
126-
const [filter, setFilter] = useState<DeviceSecurityVariation>();
127+
const [filter, setFilter] = useState<FilterVariation>();
127128
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
128129
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
129130
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
@@ -142,7 +143,7 @@ const SessionManagerTab: React.FC = () => {
142143
}
143144
};
144145

145-
const onGoToFilteredList = (filter: DeviceSecurityVariation) => {
146+
const onGoToFilteredList = (filter: FilterVariation) => {
146147
setFilter(filter);
147148
clearTimeout(scrollIntoViewTimeoutRef.current);
148149
// wait a tick for the filtered section to rerender with different height

src/i18n/strings/en_EN.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1801,6 +1801,10 @@
18011801
"Unverified sessions": "Unverified sessions",
18021802
"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.",
18031803
"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.",
1804+
"Unverified session": "Unverified session",
1805+
"This session doesn't support encryption, so it can't be verified.": "This session doesn't support encryption, so it can't be verified.",
1806+
"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.",
1807+
"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.",
18041808
"Inactive sessions": "Inactive sessions",
18051809
"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.",
18061810
"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 @@
18131817
"Unknown session type": "Unknown session type",
18141818
"Verified session": "Verified session",
18151819
"This session is ready for secure messaging.": "This session is ready for secure messaging.",
1816-
"Unverified session": "Unverified session",
1820+
"This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.",
18171821
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
18181822
"Verify session": "Verify session",
18191823
"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.",

0 commit comments

Comments
 (0)