Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session verification by emojis #513

Merged
merged 2 commits into from
May 1, 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
153 changes: 153 additions & 0 deletions src/app/organisms/emoji-verification/EmojiVerification.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './EmojiVerification.scss';
import { twemojify } from '../../../util/twemojify';

import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';

import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Spinner from '../../atoms/spinner/Spinner';
import Dialog from '../../molecules/dialog/Dialog';

import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';

function EmojiVerificationContent({ request, requestClose }) {
const [sas, setSas] = useState(null);
const [process, setProcess] = useState(false);
const mountStore = useStore();
mountStore.setItem(true);

const handleChange = () => {
if (request.done || request.cancelled) requestClose();
};

useEffect(() => {
mountStore.setItem(true);
if (request === null) return null;
const req = request;
req.on('change', handleChange);
return () => req.off('change', handleChange);
}, [request]);

const acceptRequest = async () => {
setProcess(true);
await request.accept();

const verifier = request.beginKeyVerification('m.sas.v1');
verifier.on('show_sas', (data) => {
if (!mountStore.getItem()) return;
setSas(data);
setProcess(false);
});
await verifier.verify();
};

const sasMismatch = () => {
sas.mismatch();
setProcess(true);
};

const sasConfirm = () => {
sas.confirm();
setProcess(true);
};

const renderWait = () => (
<>
<Spinner size="small" />
<Text>Waiting for response from other device...</Text>
</>
);

if (sas !== null) {
return (
<div className="emoji-verification__content">
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
<div className="emoji-verification__emojis">
{sas.sas.emoji.map((emoji) => (
<div className="emoji-verification__emoji-block" key={emoji[1]}>
<Text variant="h1">{twemojify(emoji[0])}</Text>
<Text>{emoji[1]}</Text>
</div>
))}
</div>
<div className="emoji-verification__buttons">
{process ? renderWait() : (
<>
<Button variant="primary" onClick={sasConfirm}>They match</Button>
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
</>
)}
</div>
</div>
);
}

return (
<div className="emoji-verification__content">
<Text>Click accept to start the verification process</Text>
<div className="emoji-verification__buttons">
{
process
? renderWait()
: <Button variant="primary" onClick={acceptRequest}>Accept</Button>
}
</div>
</div>
);
}
EmojiVerificationContent.propTypes = {
request: PropTypes.shape({}).isRequired,
requestClose: PropTypes.func.isRequired,
};

function useVisibilityToggle() {
const [request, setRequest] = useState(null);
const mx = initMatrix.matrixClient;

useEffect(() => {
const handleOpen = (req) => setRequest(req);
navigation.on(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
mx.on('crypto.verification.request', handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
mx.removeListener('crypto.verification.request', handleOpen);
};
}, []);

const requestClose = () => setRequest(null);

return [request, requestClose];
}

function EmojiVerification() {
const [request, requestClose] = useVisibilityToggle();

return (
<Dialog
isOpen={request !== null}
className="emoji-verification"
title={(
<Text variant="s1" weight="medium" primary>
Emoji verification
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{
request !== null
? <EmojiVerificationContent request={request} requestClose={requestClose} />
: <div />
}
</Dialog>
);
}

export default EmojiVerification;
35 changes: 35 additions & 0 deletions src/app/organisms/emoji-verification/EmojiVerification.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@use '../../partials/flex';
@use '../../partials/dir';

.emoji-verification {
&__content {
padding: var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
display: flex;
flex-direction: column;
gap: var(--sp-normal);
}

&__emojis {
margin: var(--sp-loose) 0;
display: flex;
align-items: center;
justify-content: space-around;
gap: var(--sp-extra-tight);
flex-wrap: wrap;
}

&__emoji-block {
@extend .cp-fx__column;
flex: 1;
align-items: center;
gap: var(--sp-extra-tight);
white-space: nowrap;
text-transform: capitalize;
}

&__buttons {
display: flex;
gap: var(--sp-normal);
}
}
2 changes: 2 additions & 0 deletions src/app/organisms/pw/Dialogs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExistin
import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
import EmojiVerification from '../emoji-verification/EmojiVerification';

import ReusableDialog from '../../molecules/dialog/ReusableDialog';

Expand All @@ -20,6 +21,7 @@ function Dialogs() {
<CreateRoom />
<SpaceAddExisting />
<Search />
<EmojiVerification />

<ReusableDialog />
</>
Expand Down
57 changes: 45 additions & 12 deletions src/app/organisms/settings/DeviceManage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import dateFormat from 'dateformat';

import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil';
import { openReusableDialog } from '../../../client/action/navigation';
import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';

import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
Expand All @@ -25,6 +25,7 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import { accessSecretStorage } from './SecretStorageAccess';

const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false;
Expand Down Expand Up @@ -69,6 +70,7 @@ function DeviceManage() {
const [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
const isMeVerified = isCrossVerified(mx.deviceId);

useEffect(() => {
setProcessing([]);
Expand Down Expand Up @@ -127,38 +129,69 @@ function DeviceManage() {
removeFromProcessing(device);
};

const verifyWithKey = async (device) => {
const keyData = await accessSecretStorage('Session verification');
if (!keyData) return;
addToProcessing(device);
await mx.checkOwnCrossSigningTrust();
};

const verifyWithEmojis = async (deviceId) => {
const req = await mx.requestVerification(mx.getUserId(), [deviceId]);
openEmojiVerification(req);
};

const verify = (deviceId, isCurrentDevice) => {
if (isCurrentDevice) {
verifyWithKey(deviceId);
return;
}
verifyWithEmojis(deviceId);
};

const renderDevice = (device, isVerified) => {
const deviceId = device.device_id;
const displayName = device.display_name;
const lastIP = device.last_seen_ip;
const lastTS = device.last_seen_ts;
const isCurrentDevice = mx.deviceId === deviceId;

return (
<SettingTile
key={deviceId}
title={(
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
</Text>
)}
options={
processing.includes(deviceId)
? <Spinner size="small" />
: (
<>
{((isMeVerified && isVerified === false) || (isCurrentDevice && isVerified === false)) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
</>
)
}
content={(
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
<>
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
{isCurrentDevice && (
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
</Text>
)}
</>
)}
/>
);
Expand Down Expand Up @@ -200,7 +233,7 @@ function DeviceManage() {
{noEncryption.length > 0 && (
<div>
<MenuHeader>Sessions without encryption support</MenuHeader>
{noEncryption.map((device) => renderDevice(device, true))}
{noEncryption.map((device) => renderDevice(device, null))}
</div>
)}
<div>
Expand All @@ -211,7 +244,7 @@ function DeviceManage() {
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
: <Text className="device-manage__info">No verified session</Text>
: <Text className="device-manage__info">No verified sessions</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
Expand Down
17 changes: 17 additions & 0 deletions src/app/organisms/settings/DeviceManage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@
& .setting-tile:last-of-type {
border-bottom: none;
}
& .setting-tile__options {
display: flex;
align-items: center;
gap: var(--sp-ultra-tight);
& .btn-positive {
padding: 6px var(--sp-tight);
min-width: 0;
}
}

&__current-label {
margin: 0 var(--sp-extra-tight);
padding: 2px var(--sp-ultra-tight);
color: var(--bg-surface);
background-color: var(--tc-surface-low);
border-radius: 4px;
}

&__rename {
padding: var(--sp-normal);
Expand Down
7 changes: 7 additions & 0 deletions src/client/action/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,10 @@ export function openReusableDialog(title, render, afterClose) {
afterClose,
});
}

export function openEmojiVerification(request) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
request,
});
}
3 changes: 3 additions & 0 deletions src/client/initMatrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class InitMatrix extends EventEmitter {
deviceId: secret.deviceId,
timelineSupport: true,
cryptoCallbacks,
verificationMethods: [
'm.sas.v1',
],
});

await this.matrixClient.initCrypto();
Expand Down
2 changes: 2 additions & 0 deletions src/client/state/cons.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const cons = {
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
},
room: {
JOIN: 'JOIN',
Expand Down Expand Up @@ -96,6 +97,7 @@ const cons = {
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
Expand Down
Loading