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

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

Merged
merged 9 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cypress/e2e/settings/device-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ describe("Device manager", () => {

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

// current session details opened
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: <>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,53 @@ 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: <>
{ _t('Verify or sign out from this session for best security and reliability.') }
<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}
Expand Down
29 changes: 20 additions & 9 deletions src/components/views/settings/devices/FilteredDeviceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'];
Expand All @@ -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;
Expand All @@ -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(
Expand All @@ -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)) {
Expand All @@ -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.');
Expand All @@ -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) }
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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> = ({
Expand Down
8 changes: 6 additions & 2 deletions src/components/views/settings/devices/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ 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,
};

export const filterDevicesBySecurityRecommendation = (
devices: ExtendedDevice[],
securityVariations: DeviceSecurityVariation[],
securityVariations: FilterVariation[],
) => {
const activeFilters = securityVariations.map(variation => filters[variation]);
if (!activeFilters.length) {
Expand Down
3 changes: 3 additions & 0 deletions src/components/views/settings/devices/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ 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';
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, {
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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.",
Expand Down
Loading