diff --git a/src/app/organisms/emoji-verification/EmojiVerification.jsx b/src/app/organisms/emoji-verification/EmojiVerification.jsx new file mode 100644 index 0000000..f56a467 --- /dev/null +++ b/src/app/organisms/emoji-verification/EmojiVerification.jsx @@ -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 = () => ( + <> + + Waiting for response from other device... + + ); + + if (sas !== null) { + return ( +
+ Confirm the emoji below are displayed on both devices, in the same order: +
+ {sas.sas.emoji.map((emoji) => ( +
+ {twemojify(emoji[0])} + {emoji[1]} +
+ ))} +
+
+ {process ? renderWait() : ( + <> + + + + )} +
+
+ ); + } + + return ( +
+ Click accept to start the verification process +
+ { + process + ? renderWait() + : + } +
+
+ ); +} +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 ( + + Emoji verification + + )} + contentOptions={} + onRequestClose={requestClose} + > + { + request !== null + ? + :
+ } +
+ ); +} + +export default EmojiVerification; diff --git a/src/app/organisms/emoji-verification/EmojiVerification.scss b/src/app/organisms/emoji-verification/EmojiVerification.scss new file mode 100644 index 0000000..4e6dc11 --- /dev/null +++ b/src/app/organisms/emoji-verification/EmojiVerification.scss @@ -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); + } +} diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx index f29a819..28cb47a 100644 --- a/src/app/organisms/pw/Dialogs.jsx +++ b/src/app/organisms/pw/Dialogs.jsx @@ -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'; @@ -20,6 +21,7 @@ function Dialogs() { + diff --git a/src/app/organisms/settings/DeviceManage.jsx b/src/app/organisms/settings/DeviceManage.jsx index b6ce307..a7409aa 100644 --- a/src/app/organisms/settings/DeviceManage.jsx +++ b/src/app/organisms/settings/DeviceManage.jsx @@ -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'; @@ -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; @@ -69,6 +70,7 @@ function DeviceManage() { const [truncated, setTruncated] = useState(true); const mountStore = useStore(); mountStore.setItem(true); + const isMeVerified = isCrossVerified(mx.deviceId); useEffect(() => { setProcessing([]); @@ -127,18 +129,41 @@ 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 ( + {displayName} - {` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`} + {`${displayName ? ' — ' : ''}${deviceId}`} + {isCurrentDevice && Current} )} options={ @@ -146,19 +171,27 @@ function DeviceManage() { ? : ( <> + {((isMeVerified && isVerified === false) || (isCurrentDevice && isVerified === false)) && } handleRename(device)} src={PencilIC} tooltip="Rename" /> handleRemove(device)} src={BinIC} tooltip="Remove session" /> ) } content={( - - Last activity - - {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')} - - {lastIP ? ` at ${lastIP}` : ''} - + <> + + Last activity + + {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')} + + {lastIP ? ` at ${lastIP}` : ''} + + {isCurrentDevice && ( + + {`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`} + + )} + )} /> ); @@ -200,7 +233,7 @@ function DeviceManage() { {noEncryption.length > 0 && (
Sessions without encryption support - {noEncryption.map((device) => renderDevice(device, true))} + {noEncryption.map((device) => renderDevice(device, null))}
)}
@@ -211,7 +244,7 @@ function DeviceManage() { if (truncated && index >= TRUNCATED_COUNT) return null; return renderDevice(device, true); }) - : No verified session + : No verified sessions } { verified.length > TRUNCATED_COUNT && (