mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-07 17:33:18 +00:00
wip
This commit is contained in:
@@ -202,7 +202,22 @@ class LobbyService:
|
|||||||
self.refresh_waiting_status(room.id, participant_id)
|
self.refresh_waiting_status(room.id, participant_id)
|
||||||
|
|
||||||
elif participant.status == LobbyParticipantStatus.ACCEPTED:
|
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(
|
livekit_config = utils.generate_livekit_config(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|||||||
@@ -66,6 +66,41 @@ export function getSymmetricKey(): Uint8Array | null {
|
|||||||
return _symmetricKey
|
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).
|
* Clear the stored symmetric key (e.g. on disconnect).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
encodePublicKey,
|
encodePublicKey,
|
||||||
decryptKeyFromAdmin,
|
decryptKeyFromAdmin,
|
||||||
setSymmetricKey,
|
setSymmetricKey,
|
||||||
|
saveEphemeralKeyPair,
|
||||||
|
loadEphemeralKeyPair,
|
||||||
} from '@/features/encryption/lobbyKeyExchange'
|
} from '@/features/encryption/lobbyKeyExchange'
|
||||||
|
|
||||||
export const WAIT_TIMEOUT_MS = 600000 // 10 minutes
|
export const WAIT_TIMEOUT_MS = 600000 // 10 minutes
|
||||||
@@ -88,9 +90,15 @@ export const useLobby = ({
|
|||||||
|
|
||||||
const startWaiting = useCallback(async () => {
|
const startWaiting = useCallback(async () => {
|
||||||
if (encryptionEnabled) {
|
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
|
ephemeralKeyRef.current = keyPair
|
||||||
ephemeralPublicKeyB64Ref.current = encodePublicKey(keyPair.publicKey)
|
ephemeralPublicKeyB64Ref.current = encodePublicKey(keyPair.publicKey)
|
||||||
|
if (!stored) {
|
||||||
|
saveEphemeralKeyPair(keyPair)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setStatus(ApiLobbyStatus.WAITING)
|
setStatus(ApiLobbyStatus.WAITING)
|
||||||
startWaitingTimeout()
|
startWaitingTimeout()
|
||||||
|
|||||||
@@ -22,8 +22,12 @@ import {
|
|||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import { RiHand } from '@remixicon/react'
|
import { RiHand } from '@remixicon/react'
|
||||||
import { useRaisedHand, useRaisedHandPosition } from '../hooks/useRaisedHand'
|
import { useRaisedHand, useRaisedHandPosition } from '../hooks/useRaisedHand'
|
||||||
import { EncryptionBadge, getTrustLevelFromAttributes } from '@/features/encryption'
|
import {
|
||||||
|
EncryptionBadge,
|
||||||
|
getTrustLevelFromAttributes,
|
||||||
|
} from '@/features/encryption'
|
||||||
import { useRoomData } from '../hooks/useRoomData'
|
import { useRoomData } from '../hooks/useRoomData'
|
||||||
|
import { RiLockFill } from '@remixicon/react'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { MutedMicIndicator } from './MutedMicIndicator'
|
import { MutedMicIndicator } from './MutedMicIndicator'
|
||||||
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
|
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
|
||||||
@@ -82,6 +86,15 @@ export const ParticipantTile: (
|
|||||||
const isEncrypted = useIsEncrypted(trackReference.participant)
|
const isEncrypted = useIsEncrypted(trackReference.participant)
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const isEncryptedRoom = roomData?.encryption_enabled ?? false
|
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 layoutContext = useMaybeLayoutContext()
|
||||||
|
|
||||||
const autoManageSubscription = useFeatureContext()?.autoSubscription
|
const autoManageSubscription = useFeatureContext()?.autoSubscription
|
||||||
@@ -164,6 +177,63 @@ export const ParticipantTile: (
|
|||||||
participant={trackReference.participant}
|
participant={trackReference.participant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{!disableMetadata && (
|
||||||
<div className="lk-participant-metadata">
|
<div className="lk-participant-metadata">
|
||||||
<HStack gap={0.25}>
|
<HStack gap={0.25}>
|
||||||
@@ -222,7 +292,9 @@ export const ParticipantTile: (
|
|||||||
<EncryptionBadge
|
<EncryptionBadge
|
||||||
isEncrypted={true}
|
isEncrypted={true}
|
||||||
trustLevel={getTrustLevelFromAttributes(
|
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