Web client: E2EE on by default, trust shields, identity verification, QR
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
- New rooms (public too) and direct messages are end-to-end encrypted by default. - Per-message trust: "Not verified" for sessions you haven't verified, and what is wrong for unsigned sessions, mismatched keys or plain text in an encrypted room (src/matrix/trust.ts). - Verify a person's identity in a room you share (your DM first), and QR-code verification both ways: show ours, scan theirs with the camera (qrcode-generator, jsqr). - Warnings when someone's identity changes, with acknowledge, verify again or withdraw verification. - Encrypted thumbnails for large images. - A tab-only session encrypts its crypto store with a key kept in sessionStorage; stores unused for two weeks are deleted. - Reset encryption fixed: new secret storage first, so the new cross-signing keys survive a stale /keys/query answer; status checks no longer query our own keys mid-setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b6f3be6ac4
commit
69ce927b6d
19 files changed
+845
-112
No files matched your search
@@ -26,9 +26,8 @@ watch(open, (v) => {
|
||||
error.value = ''
|
||||
})
|
||||
|
||||
// End-to-end encrypted by default for a private room. A public one is
|
||||
// readable by whoever joins anyway, and history is lost to newcomers.
|
||||
watch(isPublic, (v) => (encrypted.value = !v && chat.canEncrypt))
|
||||
// End-to-end encrypted by default, public rooms too; turning it off is a
|
||||
// choice made here, once: it can't be turned off later.
|
||||
|
||||
// A public room's address, from its name: "Homelab ops" → homelab-ops.
|
||||
watch(name, (n, old) => {
|
||||
@@ -97,6 +96,10 @@ async function submit() {
|
||||
</p>
|
||||
<p class="small m-0 text-ink-muted">
|
||||
<template v-if="!chat.canEncrypt">End-to-end encryption didn’t start in this browser.</template>
|
||||
<template v-else-if="encrypted && isPublic">
|
||||
Only the people in the room can read its messages, not the homeserver. People who join later can’t read what was sent
|
||||
before they joined. It can’t be turned off later.
|
||||
</template>
|
||||
<template v-else-if="encrypted">Only the people in the room can read its messages, not the homeserver. It can’t be turned off later.</template>
|
||||
<template v-else>The homeserver can read this room’s messages.</template>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import qrcode from 'qrcode-generator'
|
||||
import { computed } from 'vue'
|
||||
|
||||
// A QR code of raw bytes, drawn as one SVG path: dark modules on white,
|
||||
// with the quiet zone scanners need, in either theme.
|
||||
|
||||
const props = withDefaults(defineProps<{ data: Uint8ClampedArray; size?: number; label: string }>(), { size: 220 })
|
||||
|
||||
const code = computed(() => {
|
||||
const qr = qrcode(0, 'L')
|
||||
let s = ''
|
||||
for (const b of props.data) s += String.fromCharCode(b)
|
||||
qr.addData(s, 'Byte')
|
||||
qr.make()
|
||||
const n = qr.getModuleCount()
|
||||
let d = ''
|
||||
for (let r = 0; r < n; r++) {
|
||||
for (let c = 0; c < n; c++) if (qr.isDark(r, c)) d += `M${c + 4} ${r + 4}h1v1h-1z`
|
||||
}
|
||||
return { n: n + 8, d }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg :width="size" :height="size" :viewBox="`0 0 ${code.n} ${code.n}`" role="img" :aria-label="label" shape-rendering="crispEdges" class="rounded-md">
|
||||
<rect :width="code.n" :height="code.n" fill="#ffffff" />
|
||||
<path :d="code.d" fill="#000000" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import jsQR from 'jsqr'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { WBanner, WSpinner } from '@warp/ui'
|
||||
|
||||
// The camera, looking for a QR code. Frames are read on a timer, decoded
|
||||
// here in the page, and the camera is switched off as soon as a code is
|
||||
// found or the scanner goes away.
|
||||
|
||||
const emit = defineEmits<{ found: [data: Uint8ClampedArray] }>()
|
||||
|
||||
const video = ref<HTMLVideoElement>()
|
||||
const error = ref('')
|
||||
const starting = ref(true)
|
||||
let stream: MediaStream | null = null
|
||||
let timer = 0
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
function stop() {
|
||||
window.clearInterval(timer)
|
||||
stream?.getTracks().forEach((t) => t.stop())
|
||||
stream = null
|
||||
}
|
||||
|
||||
function scan() {
|
||||
const v = video.value
|
||||
if (!v || v.readyState < 2 || !v.videoWidth) return
|
||||
// Scaled down: decoding is the slow part, and codes are large on screen.
|
||||
const scale = Math.min(1, 640 / v.videoWidth)
|
||||
canvas.width = Math.round(v.videoWidth * scale)
|
||||
canvas.height = Math.round(v.videoHeight * scale)
|
||||
const g = canvas.getContext('2d', { willReadFrequently: true })
|
||||
if (!g) return
|
||||
g.drawImage(v, 0, 0, canvas.width, canvas.height)
|
||||
const img = g.getImageData(0, 0, canvas.width, canvas.height)
|
||||
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' })
|
||||
if (code?.binaryData.length) {
|
||||
stop()
|
||||
emit('found', Uint8ClampedArray.from(code.binaryData))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (!navigator.mediaDevices?.getUserMedia) throw new Error('This browser can’t use a camera here.')
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false })
|
||||
if (!video.value) return stop()
|
||||
video.value.srcObject = stream
|
||||
await video.value.play()
|
||||
timer = window.setInterval(scan, 250)
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e instanceof DOMException && e.name === 'NotAllowedError'
|
||||
? 'Camera access was turned down. Allow it in the browser’s site settings, or compare emoji instead.'
|
||||
: e instanceof Error
|
||||
? e.message
|
||||
: 'Can’t start the camera.'
|
||||
} finally {
|
||||
starting.value = false
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(stop)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<WBanner v-if="error" tone="warning" class="w-full">{{ error }}</WBanner>
|
||||
<div v-else class="relative aspect-square w-full max-w-[280px] overflow-hidden rounded-md bg-surface-sunken">
|
||||
<video ref="video" class="h-full w-full object-cover" muted playsinline />
|
||||
<span v-if="starting" class="absolute inset-0 flex items-center justify-center"><WSpinner /></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -213,9 +213,20 @@ async function confirmDelete() {
|
||||
:title="formatDateTime(item.ts)"
|
||||
>
|
||||
<MessageBody :item="item" @view="(source, name) => (viewing = { source, name })" />
|
||||
<template v-if="item.status === 'failed'" #meta>
|
||||
<button type="button" class="font-semibold text-teal-text hover:underline" @click="retry(item)">Retry</button>
|
||||
<button type="button" class="font-semibold text-ink-muted hover:underline" @click="discard(item)">Delete</button>
|
||||
<template v-if="item.status === 'failed' || item.shield" #meta>
|
||||
<span
|
||||
v-if="item.shield"
|
||||
:class="['inline-flex items-center gap-1', item.shield.tone === 'danger' ? 'text-danger' : 'text-warning']"
|
||||
:title="item.shield.text"
|
||||
>
|
||||
<WIcon name="shield-alert" :size="14" />
|
||||
<span>{{ item.shield.tone === 'danger' ? item.shield.text : 'Not verified' }}</span>
|
||||
<span v-if="item.shield.tone !== 'danger'" class="wp-sr">: {{ item.shield.text }}</span>
|
||||
</span>
|
||||
<template v-if="item.status === 'failed'">
|
||||
<button type="button" class="font-semibold text-teal-text hover:underline" @click="retry(item)">Retry</button>
|
||||
<button type="button" class="font-semibold text-ink-muted hover:underline" @click="discard(item)">Delete</button>
|
||||
</template>
|
||||
</template>
|
||||
</WMessageBubble>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { WBadge, WBanner, WButton, WDialog, WIcon, WSpinner } from '@warp/ui'
|
||||
import { errorMessage } from '@/lib/format'
|
||||
import { client } from '@/matrix/client'
|
||||
import { sessionsOf, type SessionInfo } from '@/matrix/crypto'
|
||||
import { identityOf, sessionsOf, type IdentityInfo, type SessionInfo } from '@/matrix/crypto'
|
||||
import { useSecurity } from '@/stores/security'
|
||||
|
||||
// Someone's sessions, as their published keys list them, and whether this
|
||||
@@ -14,6 +14,8 @@ const props = defineProps<{ userId: string; name: string }>()
|
||||
|
||||
const security = useSecurity()
|
||||
const sessions = ref<SessionInfo[] | null>(null)
|
||||
const identity = ref<IdentityInfo | null>(null)
|
||||
const verifyingUser = ref(false)
|
||||
const error = ref('')
|
||||
const starting = ref('')
|
||||
const mine = computed(() => props.userId === client().getUserId())
|
||||
@@ -21,7 +23,9 @@ const mine = computed(() => props.userId === client().getUserId())
|
||||
async function load() {
|
||||
error.value = ''
|
||||
try {
|
||||
sessions.value = await sessionsOf(props.userId)
|
||||
const [s, id] = await Promise.all([sessionsOf(props.userId), mine.value ? null : identityOf(props.userId)])
|
||||
sessions.value = s
|
||||
identity.value = id
|
||||
} catch (e) {
|
||||
error.value = errorMessage(e)
|
||||
}
|
||||
@@ -38,6 +42,19 @@ watch(
|
||||
() => open.value && void load(),
|
||||
)
|
||||
|
||||
async function verifyUser() {
|
||||
verifyingUser.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await security.verifyUser(props.userId)
|
||||
open.value = false
|
||||
} catch (e) {
|
||||
error.value = errorMessage(e)
|
||||
} finally {
|
||||
verifyingUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function verify(s: SessionInfo) {
|
||||
starting.value = s.deviceId
|
||||
error.value = ''
|
||||
@@ -59,8 +76,20 @@ async function verify(s: SessionInfo) {
|
||||
</p>
|
||||
<p v-else class="small m-0 text-ink-muted">
|
||||
Each place <span class="mxid">{{ userId }}</span> is signed in has its own keys. Verify a session to be sure messages from it are
|
||||
really theirs: you’ll both compare seven emoji, so do it together, in person or on a call.
|
||||
really theirs: one of you scans the other’s QR code, or you both compare seven emoji, so do it together, in person or on a call.
|
||||
</p>
|
||||
<!-- The person, all their sessions at once -->
|
||||
<div v-if="identity?.hasIdentity" class="flex items-center gap-3 rounded-md bg-surface-sunken px-3 py-3">
|
||||
<WIcon :name="identity.verified ? 'shield-check' : 'shield-alert'" :class="identity.verified ? 'text-teal-text' : 'text-ink-muted'" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="body-strong m-0">{{ identity.verified ? `You verified ${name}` : `You haven’t verified ${name}` }}</p>
|
||||
<p class="small m-0 text-ink-muted">
|
||||
<template v-if="identity.verified">Every session they verify is trusted too.</template>
|
||||
<template v-else>Verify them once, in your direct message, and every session they verify is trusted too.</template>
|
||||
</p>
|
||||
</div>
|
||||
<WButton v-if="!identity.verified" size="sm" variant="primary" :loading="verifyingUser" @click="verifyUser">Verify {{ name }}</WButton>
|
||||
</div>
|
||||
<div v-if="!sessions && !error" class="flex justify-center py-4"><WSpinner /></div>
|
||||
<p v-else-if="sessions && !sessions.length" class="body m-0 text-ink-muted">
|
||||
No sessions with encryption keys. They can’t read end-to-end encrypted messages until they sign in with a client that supports it.
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
import {
|
||||
VerificationPhase,
|
||||
VerifierEvent,
|
||||
type ShowQrCodeCallbacks,
|
||||
type ShowSasCallbacks,
|
||||
type Verifier,
|
||||
} from 'matrix-js-sdk/lib/crypto-api'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { WBanner, WButton, WDialog, WIcon, WSpinner } from '@warp/ui'
|
||||
import QrCode from '@/components/QrCode.vue'
|
||||
import QrScanner from '@/components/QrScanner.vue'
|
||||
import { errorMessage } from '@/lib/format'
|
||||
import { client } from '@/matrix/client'
|
||||
import { useSecurity } from '@/stores/security'
|
||||
|
||||
// One verification, from the request to the emoji to "Verified": either
|
||||
// this session asked (Verify session) or someone else did. Both sides see
|
||||
// the same seven emoji when nobody is in the middle.
|
||||
// One verification, from the request to "Verified": either this session
|
||||
// asked (Verify session) or someone else did. Once both are ready, either
|
||||
// side scans the other's QR code, or both compare seven emoji, which match
|
||||
// when nobody is in the middle.
|
||||
|
||||
const security = useSecurity()
|
||||
|
||||
@@ -33,6 +37,11 @@ const open = computed({
|
||||
})
|
||||
|
||||
const sas = shallowRef<ShowSasCallbacks | null>(null)
|
||||
const reciprocate = shallowRef<ShowQrCodeCallbacks | null>(null)
|
||||
const qr = shallowRef<Uint8ClampedArray | null>(null)
|
||||
const scanning = ref(false)
|
||||
/** We scanned their code: waiting for them to confirm. */
|
||||
const scanned = ref(false)
|
||||
const confirmed = ref(false)
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
@@ -40,6 +49,10 @@ let verifier: Verifier | null = null
|
||||
|
||||
function reset() {
|
||||
sas.value = null
|
||||
reciprocate.value = null
|
||||
qr.value = null
|
||||
scanning.value = false
|
||||
scanned.value = false
|
||||
confirmed.value = false
|
||||
busy.value = false
|
||||
error.value = ''
|
||||
@@ -59,34 +72,80 @@ const otherName = computed(() => {
|
||||
return r.otherUserId
|
||||
})
|
||||
|
||||
// Runs the verifier once there is one: the initiator starts emoji
|
||||
// verification as soon as the other side is ready.
|
||||
// What the other side can do with QR codes.
|
||||
const theyScan = computed(() => {
|
||||
void security.version
|
||||
return Boolean(req.value?.otherPartySupportsMethod('m.qr_code.scan.v1'))
|
||||
})
|
||||
const theyShow = computed(() => {
|
||||
void security.version
|
||||
return Boolean(req.value?.otherPartySupportsMethod('m.qr_code.show.v1')) && Boolean(navigator.mediaDevices?.getUserMedia)
|
||||
})
|
||||
|
||||
// Once ready: our QR code for them to scan, if they can. Without QR codes
|
||||
// either way, the initiator goes straight to the emoji.
|
||||
let qrFor: unknown = null
|
||||
watch(
|
||||
[req, phase],
|
||||
async () => {
|
||||
const r = req.value
|
||||
if (!r) return
|
||||
if (r.phase === VerificationPhase.Ready && r.initiatedByMe && !r.verifier && !busy.value) {
|
||||
busy.value = true
|
||||
try {
|
||||
await r.startVerification('m.sas.v1')
|
||||
} catch (e) {
|
||||
error.value = errorMessage(e)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
if (r.phase === VerificationPhase.Ready && qrFor !== r) {
|
||||
qrFor = r
|
||||
if (theyScan.value) qr.value = (await r.generateQRCode().catch(() => undefined)) ?? null
|
||||
if (!qr.value && !theyShow.value && r.initiatedByMe && !r.verifier) await startEmoji()
|
||||
}
|
||||
const v = r.verifier
|
||||
if (v && v !== verifier) {
|
||||
verifier = v
|
||||
v.on(VerifierEvent.ShowSas, (cb) => (sas.value = cb))
|
||||
v.on(VerifierEvent.ShowReciprocateQr, (cb) => (reciprocate.value = cb))
|
||||
if (v.getShowSasCallbacks()) sas.value = v.getShowSasCallbacks()
|
||||
if (v.getReciprocateQrCodeCallbacks()) reciprocate.value = v.getReciprocateQrCodeCallbacks()
|
||||
v.verify().catch(() => undefined)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function startEmoji() {
|
||||
const r = req.value
|
||||
if (!r || r.verifier || busy.value) return
|
||||
busy.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await r.startVerification('m.sas.v1')
|
||||
} catch (e) {
|
||||
error.value = errorMessage(e)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onScanned(data: Uint8ClampedArray) {
|
||||
scanning.value = false
|
||||
busy.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
// The verifier that comes of it is picked up and run above.
|
||||
await req.value?.scanQRCode(data)
|
||||
scanned.value = true
|
||||
} catch (e) {
|
||||
error.value = `That code didn’t work: ${errorMessage(e)}`
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reciprocateYes() {
|
||||
confirmed.value = true
|
||||
reciprocate.value?.confirm()
|
||||
}
|
||||
|
||||
function reciprocateNo() {
|
||||
reciprocate.value?.cancel()
|
||||
}
|
||||
|
||||
async function accept() {
|
||||
busy.value = true
|
||||
error.value = ''
|
||||
@@ -134,7 +193,12 @@ const cancelText = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const title = computed(() => (req.value?.isSelfVerification ? 'Verify this session' : 'Verify session'))
|
||||
const title = computed(() => {
|
||||
const r = req.value
|
||||
if (r?.isSelfVerification) return 'Verify this session'
|
||||
// In a room: the person, all their sessions; to-device: one session.
|
||||
return r?.roomId ? `Verify ${otherName.value}` : 'Verify session'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -159,16 +223,52 @@ const title = computed(() => (req.value?.isSelfVerification ? 'Verify this sessi
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="body-strong">{{ otherName }}</span><template v-if="otherName !== req.otherUserId">
|
||||
(<span class="mxid">{{ req.otherUserId }}</span>)</template> wants to verify your session.
|
||||
(<span class="mxid">{{ req.otherUserId }}</span>)</template> wants to verify {{ req.roomId ? 'you' : 'your session' }}.
|
||||
</span>
|
||||
</p>
|
||||
<p class="small m-0 text-ink-muted">You’ll both compare seven emoji. Accept only if you expected this.</p>
|
||||
<p class="small m-0 text-ink-muted">One of you scans the other’s QR code, or you both compare seven emoji. Accept only if you expected this.</p>
|
||||
</div>
|
||||
|
||||
<!-- Ready, about to compare -->
|
||||
<!-- We scanned their code -->
|
||||
<div v-else-if="scanned && phase !== VerificationPhase.Done && phase !== VerificationPhase.Cancelled" class="flex flex-col items-center gap-3 py-2 text-center">
|
||||
<WIcon name="shield-check" :size="40" class="text-teal-text" />
|
||||
<p class="body m-0">The code checks out. Waiting for {{ otherName }} to confirm on their side…</p>
|
||||
</div>
|
||||
|
||||
<!-- Ready: pick a way, unless one was picked already -->
|
||||
<div v-else-if="phase === VerificationPhase.Ready && !req.verifier && (qr || theyShow)" class="flex flex-col items-center gap-4 text-center">
|
||||
<template v-if="scanning">
|
||||
<p class="body m-0">Point the camera at the QR code {{ otherName }} shows.</p>
|
||||
<QrScanner @found="onScanned" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="qr">
|
||||
<p class="body m-0">Scan this code with {{ req.isSelfVerification ? 'your other session' : `${otherName}’s session` }}.</p>
|
||||
<QrCode :data="qr" label="QR code to verify this session" />
|
||||
</template>
|
||||
<p v-else class="body m-0">Scan the QR code on {{ req.isSelfVerification ? 'your other session' : `${otherName}’s screen` }}, or compare emoji.</p>
|
||||
</template>
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<WButton v-if="theyShow && !scanning" icon="image" :disabled="busy" @click="scanning = true">Scan their code</WButton>
|
||||
<WButton v-if="scanning" variant="ghost" @click="scanning = false">Stop scanning</WButton>
|
||||
<WButton :loading="busy" @click="startEmoji">Compare emoji instead</WButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- They scanned our code: did it work on their side? -->
|
||||
<div v-else-if="phase === VerificationPhase.Started && reciprocate" class="flex flex-col items-center gap-3 py-2 text-center">
|
||||
<WIcon name="shield-check" :size="40" class="text-teal-text" />
|
||||
<p class="body m-0">
|
||||
{{ req.isSelfVerification ? 'Your other session' : otherName }} scanned the code. Does
|
||||
{{ req.isSelfVerification ? 'it' : 'their screen' }} show a shield with a check mark?
|
||||
</p>
|
||||
<p v-if="confirmed" class="small m-0 flex items-center gap-2 text-ink-muted"><WSpinner :size="14" /> Finishing…</p>
|
||||
</div>
|
||||
|
||||
<!-- Working -->
|
||||
<div v-else-if="(phase === VerificationPhase.Ready || phase === VerificationPhase.Started) && !sas" class="flex flex-col items-center gap-4 py-2 text-center">
|
||||
<WSpinner :size="28" />
|
||||
<p class="body m-0">Getting the emoji ready…</p>
|
||||
<p class="body m-0">{{ req.chosenMethod === 'm.reciprocate.v1' ? 'Checking the code…' : 'Getting the emoji ready…' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Compare the emoji -->
|
||||
@@ -194,6 +294,7 @@ const title = computed(() => (req.value?.isSelfVerification ? 'Verify this sessi
|
||||
<p class="body-strong m-0">Verified</p>
|
||||
<p class="body m-0 text-ink-muted">
|
||||
<template v-if="req.isSelfVerification">This session is verified. Your other sessions trust it, and it can read your encrypted messages.</template>
|
||||
<template v-else-if="req.roomId">You verified {{ otherName }}. Every session they verify is trusted too.</template>
|
||||
<template v-else>
|
||||
You verified {{ otherName }}’s session<template v-if="req.otherDeviceId"> <span class="mxid">{{ req.otherDeviceId }}</span></template>.
|
||||
</template>
|
||||
@@ -211,6 +312,10 @@ const title = computed(() => (req.value?.isSelfVerification ? 'Verify this sessi
|
||||
<WButton variant="ghost" :disabled="busy" @click="open = false">Decline</WButton>
|
||||
<WButton variant="primary" :loading="busy" @click="accept">Accept</WButton>
|
||||
</template>
|
||||
<template v-else-if="phase === VerificationPhase.Started && reciprocate && !confirmed">
|
||||
<WButton variant="danger" @click="reciprocateNo">No</WButton>
|
||||
<WButton variant="primary" icon="check" @click="reciprocateYes">Yes, it does</WButton>
|
||||
</template>
|
||||
<template v-else-if="phase === VerificationPhase.Started && sas && !confirmed">
|
||||
<WButton variant="danger" :disabled="busy" @click="mismatch">They don’t match</WButton>
|
||||
<WButton variant="primary" icon="check" :loading="busy" @click="match">They match</WButton>
|
||||
|
||||
Reference in new issue
Block a user