The BaseKeyProvider class manages encryption keys for LiveKit’s end-to-end encryption. It supports both shared key mode (single key for all participants) and per-participant key mode.
Initialization
Shared Key Mode
Use a single shared key for all participants:
let keyProvider = BaseKeyProvider(isSharedKey: true, sharedKey: "my-secret-key")
Or set the key later:
let keyProvider = BaseKeyProvider(isSharedKey: true)
keyProvider.setKey(key: "my-secret-key")
Per-Participant Key Mode
Use different keys for each participant:
let keyProvider = BaseKeyProvider(isSharedKey: false)
// Set keys for specific participants
keyProvider.setKey(key: "alice-key", participantId: "alice")
keyProvider.setKey(key: "bob-key", participantId: "bob")
Custom Options
Create with custom KeyProviderOptions:
let options = KeyProviderOptions(
sharedKey: true,
ratchetSalt: customSalt,
ratchetWindowSize: 16,
uncryptedMagicBytes: customMagicBytes,
failureTolerance: 5,
keyRingSize: 32
)
let keyProvider = BaseKeyProvider(options: options)
Properties
Configuration options for the key provider.
rtcKeyProvider
LKRTCFrameCryptorKeyProvider
Internal WebRTC key provider (for advanced use).
KeyProviderOptions
Whether to use a single shared key for all participants.
ratchetSalt
Data
default:"LKFrameEncryptionKey"
Salt used for key ratcheting.
Window size for key ratcheting.Default 0 disables automatic ratcheting for shared key mode.
Magic bytes added to unencrypted frames for identification.
Number of consecutive decryption failures to tolerate.-1 means unlimited tolerance.
Size of the key ring buffer.
Methods
setKey(key:participantId:index:)
Set an encryption key.
// Shared key mode
keyProvider.setKey(key: "my-secret-key")
// Per-participant mode
keyProvider.setKey(key: "participant-key", participantId: "participant-id")
// With specific index
keyProvider.setKey(key: "my-key", index: 5)
Parameters:
key: The encryption key as a string
participantId: Participant identifier (required for per-participant mode)
index: Key index in the key ring (optional, defaults to current key index)
ratchetKey(participantId:index:)
Ratchet (rotate) a key using a one-way function.
// Shared key mode
if let newKey = keyProvider.ratchetKey() {
print("Key ratcheted: \(newKey.base64EncodedString())")
}
// Per-participant mode
if let newKey = keyProvider.ratchetKey(participantId: "participant-id") {
print("Participant key ratcheted")
}
Returns: The new ratcheted key as Data?, or nil on failure.
Parameters:
participantId: Participant identifier (required for per-participant mode)
index: Key index to ratchet (optional)
exportKey(participantId:index:)
Export a key from the key provider.
// Shared key mode
if let keyData = keyProvider.exportKey() {
let keyString = String(data: keyData, encoding: .utf8)
print("Exported key: \(keyString ?? "<binary>")")
}
// Per-participant mode
if let keyData = keyProvider.exportKey(participantId: "participant-id") {
// Use the exported key
}
Returns: The key as Data?, or nil if the key doesn’t exist.
Parameters:
participantId: Participant identifier (required for per-participant mode)
index: Key index to export (optional)
getCurrentKeyIndex()
Get the current key index.
let index = keyProvider.getCurrentKeyIndex()
print("Current key index: \(index)")
Returns: The current key index as Int32.
setCurrentKeyIndex(_:)
Set the current key index.
keyProvider.setCurrentKeyIndex(5)
The index is automatically wrapped using modulo with keyRingSize.
setSifTrailer(_:)
Set the SIF (Selective Forwarding) trailer.
let trailer = Data([0x01, 0x02, 0x03])
keyProvider.setSifTrailer(trailer: trailer)
Usage Examples
Basic Shared Key Setup
import LiveKit
// Create key provider with shared key
let keyProvider = BaseKeyProvider(isSharedKey: true)
// Set the encryption key
keyProvider.setKey(key: "my-secret-encryption-key")
// Use with E2EE
let encryptionOptions = EncryptionOptions(
keyProvider: keyProvider,
encryptionType: .gcm
)
let roomOptions = RoomOptions(encryptionOptions: encryptionOptions)
let room = Room(roomOptions: roomOptions)
Key Rotation
class KeyRotationManager {
let keyProvider: BaseKeyProvider
init() {
keyProvider = BaseKeyProvider(isSharedKey: true)
keyProvider.setKey(key: "initial-key")
}
func rotateKey() {
// Generate new key (example using UUID)
let newKey = UUID().uuidString
// Update to next key index
let currentIndex = keyProvider.getCurrentKeyIndex()
keyProvider.setKey(key: newKey, index: currentIndex + 1)
keyProvider.setCurrentKeyIndex(currentIndex + 1)
print("Rotated to key index \(keyProvider.getCurrentKeyIndex())")
}
func scheduleRotation() {
// Rotate key every hour
Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { _ in
self.rotateKey()
}
}
}
Per-Participant Keys
class PerParticipantEncryption: RoomDelegate {
let keyProvider: BaseKeyProvider
let room: Room
init() {
// Use per-participant key mode
keyProvider = BaseKeyProvider(isSharedKey: false)
let encryptionOptions = EncryptionOptions(
keyProvider: keyProvider,
encryptionType: .gcm
)
let roomOptions = RoomOptions(encryptionOptions: encryptionOptions)
room = Room(roomOptions: roomOptions)
room.add(delegate: self)
}
// Set key when participant joins
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
guard let participantId = participant.identity?.stringValue else { return }
// Fetch key for this participant (from your backend)
fetchKeyForParticipant(participantId) { key in
self.keyProvider.setKey(key: key, participantId: participantId)
}
}
func fetchKeyForParticipant(_ id: String, completion: @escaping (String) -> Void) {
// Implementation to fetch key from your server
// ...
}
}
Key Ratcheting
class RatchetingKeyProvider {
let keyProvider: BaseKeyProvider
init() {
let options = KeyProviderOptions(
sharedKey: true,
ratchetWindowSize: 16 // Enable ratcheting with window size
)
keyProvider = BaseKeyProvider(options: options)
}
func setupInitialKey() {
keyProvider.setKey(key: "initial-secret-key")
}
func performRatchet() {
if let newKey = keyProvider.ratchetKey() {
print("Key ratcheted successfully")
print("New key (base64): \(newKey.base64EncodedString())")
} else {
print("Failed to ratchet key")
}
}
func exportCurrentKey() -> String? {
guard let keyData = keyProvider.exportKey() else {
return nil
}
return keyData.base64EncodedString()
}
}
Best Practices
-
Choose the Right Mode
- Use shared key mode for simplicity and when all participants should have the same access
- Use per-participant mode for fine-grained control and better security
-
Secure Key Storage
- Never hardcode keys in your app
- Fetch keys from your secure backend
- Use iOS Keychain for local key storage
-
Key Rotation
- Implement regular key rotation for better security
- Coordinate rotation across all participants
- Keep old keys in the ring temporarily for transition
-
Key Distribution
- Use your own secure channel to distribute keys
- Never send keys through the LiveKit data channel unencrypted
- Consider using key exchange protocols (e.g., Diffie-Hellman)
-
Error Handling
- Monitor E2EE state through
RoomDelegate
- Handle
missingKey state by requesting keys from backend
- Implement retry logic for key distribution failures