Skip to content

Commit b74ed1f

Browse files
Encrypted data channel (#781)
- Adds data channel encryption based on livekit/client-sdk-js#1595 - Adds `currentKey` state to `BaseKeyProvider` - It was missing comparing to other platforms, and we cannot explicitly default to `0` - Adds some integration tests, as most of the _crypto_ stuff is tested by webrtc - Existing hi-level tests e.g. data streams will get a `sharedKey` by default 🎉 ### Migration/deprecations - Replace `E2EEOptions` with `EncryptionOptions` (1:1) - Add `EncryptionType` parameter to `Room/ParticipantDelegate` methods --------- Co-authored-by: Hiroshi Horie <[email protected]>
1 parent 1feef41 commit b74ed1f

32 files changed

+1969
-937
lines changed

.changes/encrypted-dc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
minor type="added" "Added support for data channel encryption, deprecated existing E2EE options"

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ let package = Package(
2020
dependencies: [
2121
// LK-Prefixed Dynamic WebRTC XCFramework
2222
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "137.7151.09"),
23-
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
23+
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"),
2424
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
2525
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
2626
// Only used for DocC generation

[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
dependencies: [
2222
// LK-Prefixed Dynamic WebRTC XCFramework
2323
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "137.7151.09"),
24-
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
24+
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"),
2525
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
2626
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
2727
// Only used for DocC generation

Sources/LiveKit/Core/DataChannelPair.swift

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal import LiveKitWebRTC
2323

2424
protocol DataChannelDelegate: Sendable {
2525
func dataChannel(_ dataChannelPair: DataChannelPair, didReceiveDataPacket dataPacket: Livekit_DataPacket)
26+
func dataChannel(_ dataChannelPair: DataChannelPair, didFailToDecryptDataPacket dataPacket: Livekit_DataPacket, error: LiveKitError)
2627
}
2728

2829
class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
@@ -34,6 +35,8 @@ class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
3435

3536
var isOpen: Bool { _state.isOpen }
3637

38+
var e2eeManager: E2EEManager?
39+
3740
// MARK: - Private
3841

3942
private struct State {
@@ -87,7 +90,7 @@ class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
8790
func peek() -> PublishDataRequest? { queue.first }
8891

8992
mutating func enqueue(_ request: PublishDataRequest) {
90-
queue.append(request)
93+
queue.append(request.withoutContinuation())
9194
currentAmount += UInt64(request.data.data.count)
9295
}
9396

@@ -110,6 +113,10 @@ class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
110113
let data: LKRTCDataBuffer
111114
let sequence: UInt32
112115
let continuation: CheckedContinuation<Void, any Error>?
116+
117+
func withoutContinuation() -> Self {
118+
.init(data: data, sequence: sequence, continuation: nil)
119+
}
113120
}
114121

115122
private struct ChannelEvent: Sendable {
@@ -234,8 +241,9 @@ class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
234241
log("Wrong packet sequence while retrying: \(first.sequence) > \(lastSeq + 1), \(first.sequence - lastSeq - 1) packets missing", .warning)
235242
}
236243
while let request = buffer.dequeue() {
244+
assert(request.continuation == nil, "Continuation may fire multiple times while retrying causing crash")
237245
if request.sequence > lastSeq {
238-
let event = ChannelEvent(channelKind: .reliable, detail: .publishData(PublishDataRequest(data: request.data, sequence: request.sequence, continuation: nil)))
246+
let event = ChannelEvent(channelKind: .reliable, detail: .publishData(request))
239247
_state.eventContinuation?.yield(event)
240248
}
241249
}
@@ -315,7 +323,7 @@ class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
315323
}
316324

317325
func send(dataPacket packet: Livekit_DataPacket) async throws {
318-
let packet = withSequence(packet)
326+
let packet = try withEncryption(withSequence(packet))
319327
let serializedData = try packet.serializedData()
320328
let rtcData = RTC.createDataBuffer(data: serializedData)
321329

@@ -333,6 +341,20 @@ class DataChannelPair: NSObject, @unchecked Sendable, Loggable {
333341
}
334342
}
335343

344+
private func withEncryption(_ packet: Livekit_DataPacket) throws -> Livekit_DataPacket {
345+
guard let e2eeManager, e2eeManager.isDataChannelEncryptionEnabled,
346+
let payload = Livekit_EncryptedPacketPayload(dataPacket: packet) else { return packet }
347+
var packet = packet
348+
do {
349+
let payloadData = try payload.serializedData()
350+
let rtcEncryptedPacket = try e2eeManager.encrypt(data: payloadData)
351+
packet.encryptedPacket = Livekit_EncryptedPacket(rtcPacket: rtcEncryptedPacket)
352+
} catch {
353+
throw LiveKitError(.encryptionFailed, internalError: error)
354+
}
355+
return packet
356+
}
357+
336358
private func withSequence(_ packet: Livekit_DataPacket) -> Livekit_DataPacket {
337359
guard packet.kind == .reliable, packet.sequence == 0 else { return packet }
338360
var packet = packet
@@ -413,8 +435,29 @@ extension DataChannelPair: LKRTCDataChannelDelegate {
413435
}
414436
}
415437

416-
delegates.notify {
417-
$0.dataChannel(self, didReceiveDataPacket: dataPacket)
438+
if let encryptedPacket = dataPacket.encryptedPacketOrNil,
439+
let e2eeManager
440+
{
441+
do {
442+
let decryptedData = try e2eeManager.handle(encryptedData: encryptedPacket.toRTCEncryptedPacket(), participantIdentity: dataPacket.participantIdentity)
443+
let decryptedPayload = try Livekit_EncryptedPacketPayload(serializedBytes: decryptedData)
444+
445+
var dataPacket = dataPacket
446+
decryptedPayload.applyTo(&dataPacket)
447+
448+
delegates.notify { [dataPacket] in
449+
$0.dataChannel(self, didReceiveDataPacket: dataPacket)
450+
}
451+
} catch {
452+
log("Failed to decrypt data packet: \(error)", .error)
453+
delegates.notify {
454+
$0.dataChannel(self, didFailToDecryptDataPacket: dataPacket, error: LiveKitError(.decryptionFailed, internalError: error))
455+
}
456+
}
457+
} else {
458+
delegates.notify {
459+
$0.dataChannel(self, didReceiveDataPacket: dataPacket)
460+
}
418461
}
419462
}
420463
}

Sources/LiveKit/Core/Room+EngineDelegate.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,20 @@ extension Room {
180180
await publication.set(track: nil)
181181
}
182182

183-
func engine(_ engine: Room, didReceiveUserPacket packet: Livekit_UserPacket) {
183+
func engine(_ engine: Room, didReceiveUserPacket packet: Livekit_UserPacket, encryptionType: EncryptionType) {
184184
// participant could be null if data broadcasted from server
185185
let identity = Participant.Identity(from: packet.participantIdentity)
186186
let participant = _state.remoteParticipants[identity]
187187

188188
if case .connected = engine._state.connectionState {
189189
delegates.notify(label: { "room.didReceive data: \(packet.payload)" }) {
190-
$0.room?(self, participant: participant, didReceiveData: packet.payload, forTopic: packet.topic)
190+
$0.room?(self, participant: participant, didReceiveData: packet.payload, forTopic: packet.topic, encryptionType: encryptionType)
191191
}
192192

193193
if let participant {
194194
participant.delegates.notify(label: { "participant.didReceive data: \(packet.payload)" }) { [weak participant] delegate in
195195
guard let participant else { return }
196-
delegate.participant?(participant, didReceiveData: packet.payload, forTopic: packet.topic)
196+
delegate.participant?(participant, didReceiveData: packet.payload, forTopic: packet.topic, encryptionType: encryptionType)
197197
}
198198
}
199199
}

Sources/LiveKit/Core/Room.swift

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ public class Room: NSObject, @unchecked Sendable, ObservableObject, Loggable {
120120
let incomingStreamManager = IncomingStreamManager()
121121
lazy var outgoingStreamManager = OutgoingStreamManager { [weak self] packet in
122122
try await self?.send(dataPacket: packet)
123+
} encryptionProvider: { [weak self] in
124+
self?.e2eeManager?.dataChannelEncryptionType ?? .none
123125
}
124126

125127
// MARK: - PreConnect
@@ -340,15 +342,26 @@ public class Room: NSObject, @unchecked Sendable, ObservableObject, Loggable {
340342
_state.mutate { $0.connectOptions = connectOptions }
341343
}
342344

345+
await cleanUp()
346+
347+
try Task.checkCancellation()
348+
343349
// enable E2EE
344350
if let e2eeOptions = state.roomOptions.e2eeOptions {
345351
e2eeManager = E2EEManager(e2eeOptions: e2eeOptions)
346352
e2eeManager!.setup(room: self)
347-
}
353+
} else if let encryptionOptions = state.roomOptions.encryptionOptions {
354+
e2eeManager = E2EEManager(options: encryptionOptions)
355+
e2eeManager!.setup(room: self)
348356

349-
await cleanUp()
357+
subscriberDataChannel.e2eeManager = e2eeManager
358+
publisherDataChannel.e2eeManager = e2eeManager
359+
} else {
360+
e2eeManager = nil
350361

351-
try Task.checkCancellation()
362+
subscriberDataChannel.e2eeManager = nil
363+
publisherDataChannel.e2eeManager = nil
364+
}
352365

353366
_state.mutate { $0.connectionState = .connecting }
354367

@@ -620,15 +633,21 @@ extension Room: DataChannelDelegate {
620633
func dataChannel(_: DataChannelPair, didReceiveDataPacket dataPacket: Livekit_DataPacket) {
621634
switch dataPacket.value {
622635
case let .speaker(update): engine(self, didUpdateSpeakers: update.speakers)
623-
case let .user(userPacket): engine(self, didReceiveUserPacket: userPacket)
636+
case let .user(userPacket): engine(self, didReceiveUserPacket: userPacket, encryptionType: dataPacket.encryptedPacket.encryptionType.toLKType())
624637
case let .transcription(packet): room(didReceiveTranscriptionPacket: packet)
625638
case let .rpcResponse(response): room(didReceiveRpcResponse: response)
626639
case let .rpcAck(ack): room(didReceiveRpcAck: ack)
627640
case let .rpcRequest(request): room(didReceiveRpcRequest: request, from: dataPacket.participantIdentity)
628-
case let .streamHeader(header): Task { await incomingStreamManager.handle(header: header, from: dataPacket.participantIdentity) }
629-
case let .streamChunk(chunk): Task { await incomingStreamManager.handle(chunk: chunk) }
630-
case let .streamTrailer(trailer): Task { await incomingStreamManager.handle(trailer: trailer) }
641+
case let .streamHeader(header): Task { await incomingStreamManager.handle(header: header, from: dataPacket.participantIdentity, encryptionType: dataPacket.encryptedPacket.encryptionType.toLKType()) }
642+
case let .streamChunk(chunk): Task { await incomingStreamManager.handle(chunk: chunk, encryptionType: dataPacket.encryptedPacket.encryptionType.toLKType()) }
643+
case let .streamTrailer(trailer): Task { await incomingStreamManager.handle(trailer: trailer, encryptionType: dataPacket.encryptedPacket.encryptionType.toLKType()) }
631644
default: return
632645
}
633646
}
647+
648+
func dataChannel(_: DataChannelPair, didFailToDecryptDataPacket _: Livekit_DataPacket, error: LiveKitError) {
649+
delegates.notify {
650+
$0.room?(self, didFailToDecryptDataWithEror: error)
651+
}
652+
}
634653
}

Sources/LiveKit/Core/SignalClient.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ private extension SignalClient {
352352

353353
case .roomMoved:
354354
log("Received roomMoved message")
355+
356+
case .mediaSectionsRequirement:
357+
log("Received mediaSectionsRequirement message")
355358
}
356359
}
357360
}

Sources/LiveKit/DataStream/Incoming/IncomingStreamManager.swift

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ actor IncomingStreamManager: Loggable {
6161
// MARK: - Packet processing
6262

6363
/// Handles a data stream header.
64-
func handle(header: Livekit_DataStream.Header, from identityString: String) {
64+
func handle(header: Livekit_DataStream.Header, from identityString: String, encryptionType: EncryptionType) {
6565
let identity = Participant.Identity(from: identityString)
6666

67-
guard let streamInfo = Self.streamInfo(from: header) else {
67+
guard let streamInfo = Self.streamInfo(from: header, encryptionType: encryptionType) else {
6868
return
6969
}
7070
openStream(with: streamInfo, from: identity)
@@ -110,9 +110,18 @@ actor IncomingStreamManager: Loggable {
110110
}
111111

112112
/// Handles a data stream chunk.
113-
func handle(chunk: Livekit_DataStream.Chunk) {
113+
func handle(chunk: Livekit_DataStream.Chunk, encryptionType: EncryptionType) {
114114
guard !chunk.content.isEmpty, let descriptor = openStreams[chunk.streamID] else { return }
115115

116+
if descriptor.info.encryptionType != encryptionType {
117+
let error = StreamError.encryptionTypeMismatch(
118+
expected: descriptor.info.encryptionType,
119+
received: encryptionType
120+
)
121+
descriptor.continuation.finish(throwing: error)
122+
return
123+
}
124+
116125
let readLength = descriptor.readLength + chunk.content.count
117126

118127
if let totalLength = descriptor.info.totalLength {
@@ -126,10 +135,20 @@ actor IncomingStreamManager: Loggable {
126135
}
127136

128137
/// Handles a data stream trailer.
129-
func handle(trailer: Livekit_DataStream.Trailer) {
138+
func handle(trailer: Livekit_DataStream.Trailer, encryptionType: EncryptionType) {
130139
guard let descriptor = openStreams[trailer.streamID] else {
131140
return
132141
}
142+
143+
if descriptor.info.encryptionType != encryptionType {
144+
let error = StreamError.encryptionTypeMismatch(
145+
expected: descriptor.info.encryptionType,
146+
received: encryptionType
147+
)
148+
descriptor.continuation.finish(throwing: error)
149+
return
150+
}
151+
133152
if let totalLength = descriptor.info.totalLength {
134153
guard descriptor.readLength == totalLength else {
135154
descriptor.continuation.finish(throwing: StreamError.incomplete)
@@ -186,10 +205,10 @@ public typealias TextStreamHandler = @Sendable (TextStreamReader, Participant.Id
186205
// MARK: - From protocol types
187206

188207
extension IncomingStreamManager {
189-
static func streamInfo(from header: Livekit_DataStream.Header) -> StreamInfo? {
208+
static func streamInfo(from header: Livekit_DataStream.Header, encryptionType: EncryptionType) -> StreamInfo? {
190209
switch header.contentHeader {
191-
case let .byteHeader(byteHeader): ByteStreamInfo(header, byteHeader)
192-
case let .textHeader(textHeader): TextStreamInfo(header, textHeader)
210+
case let .byteHeader(byteHeader): ByteStreamInfo(header, byteHeader, encryptionType)
211+
case let .textHeader(textHeader): TextStreamInfo(header, textHeader, encryptionType)
193212
default: nil
194213
}
195214
}
@@ -198,14 +217,16 @@ extension IncomingStreamManager {
198217
extension ByteStreamInfo {
199218
convenience init(
200219
_ header: Livekit_DataStream.Header,
201-
_ byteHeader: Livekit_DataStream.ByteHeader
220+
_ byteHeader: Livekit_DataStream.ByteHeader,
221+
_ encryptionType: EncryptionType
202222
) {
203223
self.init(
204224
id: header.streamID,
205225
topic: header.topic,
206226
timestamp: header.timestampDate,
207227
totalLength: header.hasTotalLength ? Int(header.totalLength) : nil,
208228
attributes: header.attributes,
229+
encryptionType: encryptionType,
209230
// ---
210231
mimeType: header.mimeType,
211232
name: byteHeader.name
@@ -216,14 +237,16 @@ extension ByteStreamInfo {
216237
extension TextStreamInfo {
217238
convenience init(
218239
_ header: Livekit_DataStream.Header,
219-
_ textHeader: Livekit_DataStream.TextHeader
240+
_ textHeader: Livekit_DataStream.TextHeader,
241+
_ encryptionType: EncryptionType
220242
) {
221243
self.init(
222244
id: header.streamID,
223245
topic: header.topic,
224246
timestamp: header.timestampDate,
225247
totalLength: header.hasTotalLength ? Int(header.totalLength) : nil,
226248
attributes: header.attributes,
249+
encryptionType: encryptionType,
227250
// ---
228251
operationType: TextStreamInfo.OperationType(textHeader.operationType),
229252
version: Int(textHeader.version),

Sources/LiveKit/DataStream/Outgoing/OutgoingStreamManager.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import Foundation
1919
/// Manages state of outgoing data streams.
2020
actor OutgoingStreamManager: Loggable {
2121
typealias PacketHandler = @Sendable (Livekit_DataPacket) async throws -> Void
22+
typealias EncryptionProvider = @Sendable () -> EncryptionType
2223

2324
private nonisolated let packetHandler: PacketHandler
25+
private nonisolated let encryptionProvider: EncryptionProvider
2426

25-
init(packetHandler: @escaping PacketHandler) {
27+
init(packetHandler: @escaping PacketHandler, encryptionProvider: @escaping EncryptionProvider) {
2628
self.packetHandler = packetHandler
29+
self.encryptionProvider = encryptionProvider
2730
}
2831

2932
// MARK: - Opening streams
@@ -35,6 +38,7 @@ actor OutgoingStreamManager: Loggable {
3538
timestamp: Date(),
3639
totalLength: text.utf8.count, // Number of bytes in UTF-8 representation
3740
attributes: options.attributes,
41+
encryptionType: encryptionProvider(),
3842
operationType: .create,
3943
version: options.version,
4044
replyToStreamID: options.replyToStreamID,
@@ -61,6 +65,7 @@ actor OutgoingStreamManager: Loggable {
6165
timestamp: Date(),
6266
totalLength: fileInfo.size, // Not overridable
6367
attributes: options.attributes,
68+
encryptionType: encryptionProvider(),
6469
mimeType: options.mimeType ?? fileInfo.mimeType ?? Self.byteMimeType,
6570
name: options.name ?? fileInfo.name
6671
)
@@ -81,6 +86,7 @@ actor OutgoingStreamManager: Loggable {
8186
timestamp: Date(),
8287
totalLength: nil,
8388
attributes: options.attributes,
89+
encryptionType: encryptionProvider(),
8490
operationType: .create,
8591
version: options.version,
8692
replyToStreamID: options.replyToStreamID,
@@ -100,6 +106,7 @@ actor OutgoingStreamManager: Loggable {
100106
timestamp: Date(),
101107
totalLength: options.totalSize,
102108
attributes: options.attributes,
109+
encryptionType: encryptionProvider(),
103110
mimeType: options.mimeType ?? Self.byteMimeType,
104111
name: options.name
105112
)

Sources/LiveKit/DataStream/StreamError.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,7 @@ public enum StreamError: Error, Equatable {
4444

4545
/// Unable to read information about the file to send.
4646
case fileInfoUnavailable
47+
48+
/// Encryption type mismatch between stream header and chunk/trailer.
49+
case encryptionTypeMismatch(expected: EncryptionType, received: EncryptionType)
4750
}

0 commit comments

Comments
 (0)