mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
wip
This commit is contained in:
@@ -202,7 +202,22 @@ class LobbyService:
|
||||
self.refresh_waiting_status(room.id, participant_id)
|
||||
|
||||
elif participant.status == LobbyParticipantStatus.ACCEPTED:
|
||||
# wrongly named, contains access token to join a room
|
||||
# If the joiner comes back with a different ephemeral key (e.g. browser
|
||||
# closed and reopened), they can no longer decrypt the encrypted symmetric
|
||||
# key. Reset them to WAITING so the admin re-accepts with the new key.
|
||||
if (
|
||||
ephemeral_public_key
|
||||
and participant.ephemeral_public_key
|
||||
and ephemeral_public_key != participant.ephemeral_public_key
|
||||
):
|
||||
participant = self.enter(
|
||||
room.id, participant_id, username,
|
||||
is_authenticated=request.user.is_authenticated,
|
||||
email=getattr(request.user, 'email', None) if request.user.is_authenticated else None,
|
||||
ephemeral_public_key=ephemeral_public_key,
|
||||
)
|
||||
return participant, None
|
||||
|
||||
livekit_config = utils.generate_livekit_config(
|
||||
room_id=room_id,
|
||||
user=request.user,
|
||||
|
||||
@@ -66,6 +66,41 @@ export function getSymmetricKey(): Uint8Array | null {
|
||||
return _symmetricKey
|
||||
}
|
||||
|
||||
const EPHEMERAL_KEY_STORAGE_KEY = 'meet-ephemeral-keypair'
|
||||
|
||||
/**
|
||||
* Persist the ephemeral keypair in sessionStorage so it survives page refreshes.
|
||||
* sessionStorage is cleared when the tab/browser closes — in that case the backend
|
||||
* resets the participant to WAITING so the admin re-accepts with a fresh key.
|
||||
*/
|
||||
export function saveEphemeralKeyPair(keyPair: { publicKey: Uint8Array; secretKey: Uint8Array }): void {
|
||||
try {
|
||||
sessionStorage.setItem(EPHEMERAL_KEY_STORAGE_KEY, JSON.stringify({
|
||||
publicKey: toBase64(keyPair.publicKey),
|
||||
secretKey: toBase64(keyPair.secretKey),
|
||||
}))
|
||||
} catch {
|
||||
// sessionStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the ephemeral keypair from sessionStorage.
|
||||
*/
|
||||
export function loadEphemeralKeyPair(): { publicKey: Uint8Array; secretKey: Uint8Array } | null {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(EPHEMERAL_KEY_STORAGE_KEY)
|
||||
if (!stored) return null
|
||||
const parsed = JSON.parse(stored)
|
||||
return {
|
||||
publicKey: fromBase64(parsed.publicKey),
|
||||
secretKey: fromBase64(parsed.secretKey),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the stored symmetric key (e.g. on disconnect).
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
encodePublicKey,
|
||||
decryptKeyFromAdmin,
|
||||
setSymmetricKey,
|
||||
saveEphemeralKeyPair,
|
||||
loadEphemeralKeyPair,
|
||||
} from '@/features/encryption/lobbyKeyExchange'
|
||||
|
||||
export const WAIT_TIMEOUT_MS = 600000 // 10 minutes
|
||||
@@ -88,9 +90,15 @@ export const useLobby = ({
|
||||
|
||||
const startWaiting = useCallback(async () => {
|
||||
if (encryptionEnabled) {
|
||||
const keyPair = await generateEphemeralKeyPair()
|
||||
// Restore keypair from sessionStorage (survives page refresh)
|
||||
// or generate a new one (backend will reset to WAITING if key changed)
|
||||
const stored = loadEphemeralKeyPair()
|
||||
const keyPair = stored ?? await generateEphemeralKeyPair()
|
||||
ephemeralKeyRef.current = keyPair
|
||||
ephemeralPublicKeyB64Ref.current = encodePublicKey(keyPair.publicKey)
|
||||
if (!stored) {
|
||||
saveEphemeralKeyPair(keyPair)
|
||||
}
|
||||
}
|
||||
setStatus(ApiLobbyStatus.WAITING)
|
||||
startWaitingTimeout()
|
||||
|
||||
@@ -22,8 +22,12 @@ import {
|
||||
import { Track } from 'livekit-client'
|
||||
import { RiHand } from '@remixicon/react'
|
||||
import { useRaisedHand, useRaisedHandPosition } from '../hooks/useRaisedHand'
|
||||
import { EncryptionBadge, getTrustLevelFromAttributes } from '@/features/encryption'
|
||||
import {
|
||||
EncryptionBadge,
|
||||
getTrustLevelFromAttributes,
|
||||
} from '@/features/encryption'
|
||||
import { useRoomData } from '../hooks/useRoomData'
|
||||
import { RiLockFill } from '@remixicon/react'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { MutedMicIndicator } from './MutedMicIndicator'
|
||||
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
|
||||
@@ -82,6 +86,15 @@ export const ParticipantTile: (
|
||||
const isEncrypted = useIsEncrypted(trackReference.participant)
|
||||
const roomData = useRoomData()
|
||||
const isEncryptedRoom = roomData?.encryption_enabled ?? false
|
||||
// Show overlay when we cannot decrypt a remote participant's frames
|
||||
const isDecryptionFailed =
|
||||
isEncryptedRoom && !isEncrypted && !trackReference.participant.isLocal
|
||||
// TODO: remove this force flag after CSS adjustments
|
||||
const forceDecryptionOverlay = true
|
||||
const showDecryptionError =
|
||||
!trackReference.participant.isLocal &&
|
||||
isEncryptedRoom &&
|
||||
(forceDecryptionOverlay || isDecryptionFailed)
|
||||
const layoutContext = useMaybeLayoutContext()
|
||||
|
||||
const autoManageSubscription = useFeatureContext()?.autoSubscription
|
||||
@@ -164,6 +177,63 @@ export const ParticipantTile: (
|
||||
participant={trackReference.participant}
|
||||
/>
|
||||
</div>
|
||||
{showDecryptionError && !isScreenShare && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<ParticipantPlaceholder
|
||||
participant={trackReference.participant}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '2.5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '0.6rem 1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.3rem',
|
||||
maxWidth: '85%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.4rem',
|
||||
color: '#f87171',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<RiLockFill size={14} />
|
||||
<span>Decryption failed</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
color: '#d1d5db',
|
||||
fontSize: '0.75rem',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
Unable to decrypt this participant's stream. One of
|
||||
you may need to leave and rejoin.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!disableMetadata && (
|
||||
<div className="lk-participant-metadata">
|
||||
<HStack gap={0.25}>
|
||||
@@ -222,7 +292,9 @@ export const ParticipantTile: (
|
||||
<EncryptionBadge
|
||||
isEncrypted={true}
|
||||
trustLevel={getTrustLevelFromAttributes(
|
||||
trackReference.participant.attributes as Record<string, string> | undefined
|
||||
trackReference.participant.attributes as
|
||||
| Record<string, string>
|
||||
| undefined
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user