mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-31 12:47:58 +00:00
working with libsodium (issue from RTC due to encrypted headers in packets?)
This commit is contained in:
@@ -1,42 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* E2EE Worker — Step 2d: uses crypto.subtle AES-GCM with the SAME frame format
|
* E2EE Worker — Step 3: libsodium XChaCha20-Poly1305 with preserved codec headers.
|
||||||
* as LiveKit's built-in FrameCryptor, including preserved unencrypted header bytes.
|
* Same frame format as LiveKit (unencrypted header bytes), but using libsodium
|
||||||
|
* instead of crypto.subtle. Hardcoded symmetric key, no VaultClient yet.
|
||||||
*
|
*
|
||||||
* Frame format (same as LiveKit):
|
* Frame format:
|
||||||
* [unencrypted header][ciphertext + GCM tag][IV (12B)][IV_LENGTH (1B)][key index (1B)]
|
* [unencrypted header][ciphertext + Poly1305 MAC (16B)][nonce (24B)][NONCE_LENGTH (1B)][key index (1B)]
|
||||||
*
|
|
||||||
* Unencrypted header sizes (VP8):
|
|
||||||
* - keyframe: 10 bytes
|
|
||||||
* - delta: 3 bytes
|
|
||||||
* - audio: 1 byte (Opus TOC)
|
|
||||||
*/
|
*/
|
||||||
|
import _sodium from 'libsodium-wrappers-sumo'
|
||||||
|
|
||||||
|
let sodium: typeof _sodium
|
||||||
|
let symmetricKey: Uint8Array | null = null
|
||||||
|
|
||||||
let encryptionKey: CryptoKey | null = null
|
|
||||||
const IV_LENGTH = 12
|
|
||||||
const KEY_INDEX = 0
|
const KEY_INDEX = 0
|
||||||
|
|
||||||
// Same constants as LiveKit's FrameCryptor
|
// Same as LiveKit's FrameCryptor constants
|
||||||
const UNENCRYPTED_BYTES = {
|
const UNENCRYPTED_BYTES = {
|
||||||
key: 10, // VP8 keyframe
|
key: 10, // VP8 keyframe
|
||||||
delta: 3, // VP8 delta frame
|
delta: 3, // VP8 delta frame
|
||||||
audio: 1, // Opus TOC byte
|
audio: 1, // Opus TOC byte
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUnencryptedBytes(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number {
|
async function init() {
|
||||||
// Audio frames don't have .type
|
await _sodium.ready
|
||||||
if (!('type' in frame)) {
|
sodium = _sodium
|
||||||
return UNENCRYPTED_BYTES.audio
|
|
||||||
}
|
|
||||||
return frame.type === 'key' ? UNENCRYPTED_BYTES.key : UNENCRYPTED_BYTES.delta
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeIV(ssrc: number, timestamp: number): Uint8Array {
|
const sodiumReady = init()
|
||||||
const iv = new ArrayBuffer(IV_LENGTH)
|
|
||||||
const view = new DataView(iv)
|
function getUnencryptedBytes(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number {
|
||||||
view.setUint32(0, ssrc, true)
|
if (!('type' in frame)) return UNENCRYPTED_BYTES.audio
|
||||||
view.setUint32(4, timestamp, true)
|
return frame.type === 'key' ? UNENCRYPTED_BYTES.key : UNENCRYPTED_BYTES.delta
|
||||||
view.setUint32(8, ssrc ^ timestamp, true)
|
|
||||||
return new Uint8Array(iv)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onmessage = async (ev: MessageEvent) => {
|
onmessage = async (ev: MessageEvent) => {
|
||||||
@@ -44,22 +37,22 @@ onmessage = async (ev: MessageEvent) => {
|
|||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'init':
|
case 'init':
|
||||||
|
await sodiumReady
|
||||||
postMessage({ kind: 'initAck', data: { enabled: true } })
|
postMessage({ kind: 'initAck', data: { enabled: true } })
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'setKey': {
|
case 'setKey':
|
||||||
encryptionKey = await crypto.subtle.importKey(
|
// Store raw symmetric key for libsodium (32 bytes)
|
||||||
'raw', data.key, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'],
|
symmetricKey = new Uint8Array(data.key)
|
||||||
)
|
|
||||||
postMessage({
|
postMessage({
|
||||||
kind: 'enable',
|
kind: 'enable',
|
||||||
data: { enabled: true, participantIdentity: data.participantIdentity },
|
data: { enabled: true, participantIdentity: data.participantIdentity },
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
|
||||||
|
|
||||||
case 'encode':
|
case 'encode':
|
||||||
case 'decode': {
|
case 'decode': {
|
||||||
|
await sodiumReady
|
||||||
const { readableStream, writableStream, trackId, participantIdentity } = data
|
const { readableStream, writableStream, trackId, participantIdentity } = data
|
||||||
const operation = kind
|
const operation = kind
|
||||||
let frameCount = 0
|
let frameCount = 0
|
||||||
@@ -67,81 +60,64 @@ onmessage = async (ev: MessageEvent) => {
|
|||||||
const transformStream = new TransformStream({
|
const transformStream = new TransformStream({
|
||||||
transform: async (frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame, controller: TransformStreamDefaultController) => {
|
transform: async (frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame, controller: TransformStreamDefaultController) => {
|
||||||
try {
|
try {
|
||||||
if (!encryptionKey) return // drop — key not ready
|
if (!symmetricKey) return // drop — key not ready
|
||||||
if (!frame.data || frame.data.byteLength === 0) {
|
if (!frame.data || frame.data.byteLength === 0) {
|
||||||
return controller.enqueue(frame)
|
return controller.enqueue(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unencryptedBytes = getUnencryptedBytes(frame)
|
||||||
|
|
||||||
if (operation === 'encode') {
|
if (operation === 'encode') {
|
||||||
// ── Encrypt (same as LiveKit FrameCryptor.encodeFunction) ──
|
// ── Encrypt ──
|
||||||
const iv = makeIV(
|
|
||||||
(frame as any).getMetadata?.().synchronizationSource ?? 0,
|
|
||||||
(frame as any).timestamp ?? 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
const unencryptedBytes = getUnencryptedBytes(frame)
|
|
||||||
const frameHeader = new Uint8Array(frame.data, 0, unencryptedBytes)
|
const frameHeader = new Uint8Array(frame.data, 0, unencryptedBytes)
|
||||||
|
const payload = new Uint8Array(frame.data, unencryptedBytes)
|
||||||
|
|
||||||
const ciphertext = await crypto.subtle.encrypt(
|
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES) // 24 bytes
|
||||||
{
|
const ciphertext = sodium.crypto_secretbox_easy(payload, nonce, symmetricKey)
|
||||||
name: 'AES-GCM',
|
|
||||||
iv,
|
|
||||||
additionalData: new Uint8Array(frame.data, 0, frameHeader.byteLength),
|
|
||||||
},
|
|
||||||
encryptionKey,
|
|
||||||
new Uint8Array(frame.data, unencryptedBytes),
|
|
||||||
)
|
|
||||||
|
|
||||||
// [header][ciphertext+tag][IV][IV_LENGTH][keyIndex]
|
// Trailer: [NONCE_LENGTH][KEY_INDEX]
|
||||||
const frameTrailer = new Uint8Array(2)
|
const trailer = new Uint8Array(2)
|
||||||
frameTrailer[0] = IV_LENGTH
|
trailer[0] = sodium.crypto_secretbox_NONCEBYTES // 24
|
||||||
frameTrailer[1] = KEY_INDEX
|
trailer[1] = KEY_INDEX
|
||||||
|
|
||||||
|
// [header][ciphertext + MAC][nonce][trailer]
|
||||||
const newData = new Uint8Array(
|
const newData = new Uint8Array(
|
||||||
frameHeader.byteLength + ciphertext.byteLength + iv.byteLength + frameTrailer.byteLength,
|
frameHeader.byteLength + ciphertext.byteLength + nonce.byteLength + trailer.byteLength,
|
||||||
)
|
)
|
||||||
newData.set(frameHeader)
|
let offset = 0
|
||||||
newData.set(new Uint8Array(ciphertext), frameHeader.byteLength)
|
newData.set(frameHeader, offset); offset += frameHeader.byteLength
|
||||||
newData.set(iv, frameHeader.byteLength + ciphertext.byteLength)
|
newData.set(ciphertext, offset); offset += ciphertext.byteLength
|
||||||
newData.set(frameTrailer, frameHeader.byteLength + ciphertext.byteLength + iv.byteLength)
|
newData.set(nonce, offset); offset += nonce.byteLength
|
||||||
|
newData.set(trailer, offset)
|
||||||
|
|
||||||
frame.data = newData.buffer
|
frame.data = newData.buffer
|
||||||
controller.enqueue(frame)
|
controller.enqueue(frame)
|
||||||
} else {
|
} else {
|
||||||
// ── Decrypt (same as LiveKit FrameCryptor.decodeFunction) ──
|
// ── Decrypt ──
|
||||||
const frameData = new Uint8Array(frame.data)
|
|
||||||
const unencryptedBytes = getUnencryptedBytes(frame)
|
|
||||||
const frameHeader = new Uint8Array(frame.data, 0, unencryptedBytes)
|
const frameHeader = new Uint8Array(frame.data, 0, unencryptedBytes)
|
||||||
|
|
||||||
// Read trailer
|
// Read trailer (last 2 bytes)
|
||||||
const frameTrailer = new Uint8Array(frame.data, frame.data.byteLength - 2, 2)
|
const trailer = new Uint8Array(frame.data, frame.data.byteLength - 2, 2)
|
||||||
const ivLength = frameTrailer[0]
|
const nonceLength = trailer[0]
|
||||||
|
|
||||||
// Extract IV
|
// Extract nonce
|
||||||
const iv = new Uint8Array(
|
const nonce = new Uint8Array(
|
||||||
frame.data,
|
frame.data,
|
||||||
frame.data.byteLength - ivLength - frameTrailer.byteLength,
|
frame.data.byteLength - nonceLength - trailer.byteLength,
|
||||||
ivLength,
|
nonceLength,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Extract ciphertext (between header and IV)
|
// Extract ciphertext (between header and nonce)
|
||||||
const ciphertextStart = frameHeader.byteLength
|
const ciphertextStart = frameHeader.byteLength
|
||||||
const ciphertextLength = frame.data.byteLength - frameHeader.byteLength - ivLength - frameTrailer.byteLength
|
const ciphertextLength = frame.data.byteLength - frameHeader.byteLength - nonceLength - trailer.byteLength
|
||||||
|
const ciphertext = new Uint8Array(frame.data, ciphertextStart, ciphertextLength)
|
||||||
|
|
||||||
const plaintext = await crypto.subtle.decrypt(
|
const plaintext = sodium.crypto_secretbox_open_easy(ciphertext, nonce, symmetricKey)
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv,
|
|
||||||
additionalData: new Uint8Array(frame.data, 0, frameHeader.byteLength),
|
|
||||||
},
|
|
||||||
encryptionKey,
|
|
||||||
new Uint8Array(frame.data, ciphertextStart, ciphertextLength),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reconstruct: [header][plaintext]
|
// Reconstruct: [header][plaintext]
|
||||||
const newData = new Uint8Array(frameHeader.byteLength + plaintext.byteLength)
|
const newData = new Uint8Array(frameHeader.byteLength + plaintext.byteLength)
|
||||||
newData.set(frameHeader)
|
newData.set(frameHeader)
|
||||||
newData.set(new Uint8Array(plaintext), frameHeader.byteLength)
|
newData.set(plaintext, frameHeader.byteLength)
|
||||||
frame.data = newData.buffer
|
frame.data = newData.buffer
|
||||||
|
|
||||||
controller.enqueue(frame)
|
controller.enqueue(frame)
|
||||||
|
|||||||
Reference in New Issue
Block a user