diff --git a/ci/package.json b/ci/package.json index 43bbd365..50e995a4 100644 --- a/ci/package.json +++ b/ci/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@livekit/react-native": "*", - "@livekit/react-native-webrtc": "^137.0.1", + "@livekit/react-native-webrtc": "^137.0.2", "livekit-client": "^2.15.4", "react": "18.2.0", "react-native": "0.74.2" diff --git a/ci/yarn.lock b/ci/yarn.lock index 270823c7..ea143b77 100644 --- a/ci/yarn.lock +++ b/ci/yarn.lock @@ -2174,16 +2174,16 @@ __metadata: languageName: node linkType: hard -"@livekit/react-native-webrtc@npm:^137.0.1": - version: 137.0.1 - resolution: "@livekit/react-native-webrtc@npm:137.0.1" +"@livekit/react-native-webrtc@npm:^137.0.2": + version: 137.0.2 + resolution: "@livekit/react-native-webrtc@npm:137.0.2" dependencies: base64-js: 1.5.1 debug: 4.3.4 event-target-shim: 6.0.2 peerDependencies: react-native: ">=0.60.0" - checksum: 3341aafe7d9d1deb345e47247faa020b2cb5f1adbded135a244f0f9285eed720c9f62325bbce0e21c1da755ae24ae6c69eb07728cece709560f615908bde43a9 + checksum: c23467701c7dfb74e3b223dd955b20ba1586c2186383f5b2595fff9b65a36b2db0b660de9935e009b69bc5e0bbff047d0f598495f8464daae8db5f8d9f9b7c24 languageName: node linkType: hard @@ -3872,7 +3872,7 @@ __metadata: "@babel/preset-env": ^7.20.0 "@babel/runtime": ^7.20.0 "@livekit/react-native": "*" - "@livekit/react-native-webrtc": ^137.0.1 + "@livekit/react-native-webrtc": ^137.0.2 "@react-native/babel-preset": 0.74.84 "@react-native/eslint-config": 0.74.84 "@react-native/metro-config": 0.74.84 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d552b8de..802b4d35 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -29,9 +29,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - livekit-react-native-webrtc (137.0.1): + - livekit-react-native-webrtc (137.0.2): - React-Core - - WebRTC-SDK (= 137.7151.02) + - WebRTC-SDK (= 137.7151.04) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -1220,7 +1220,7 @@ PODS: - ReactCommon/turbomodule/core - Yoga - SocketRocket (0.7.0) - - WebRTC-SDK (137.7151.02) + - WebRTC-SDK (137.7151.04) - Yoga (0.0.0) DEPENDENCIES: @@ -1426,7 +1426,7 @@ SPEC CHECKSUMS: glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f hermes-engine: 01d3e052018c2a13937aca1860fbedbccd4a41b7 livekit-react-native: 22180f283c63416a81f8765555fccc7a33f0a044 - livekit-react-native-webrtc: 0ffe5a13d196f65d717f958a111399f4f6383102 + livekit-react-native-webrtc: 3bb1be767c4e489f69bca662eba1c5e7b1e1be0d RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 RCTDeprecation: b03c35057846b685b3ccadc9bfe43e349989cdb2 RCTRequired: 194626909cfa8d39ca6663138c417bc6c431648c @@ -1480,7 +1480,7 @@ SPEC CHECKSUMS: RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca RNScreens: b32a9ff15bea7fcdbe5dff6477bc503f792b1208 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b + WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e Yoga: ae3c32c514802d30f687a04a6a35b348506d411f PODFILE CHECKSUM: b5aad0c7d12b2ea501eb822f98f00ca01d154bd9 diff --git a/example/package.json b/example/package.json index b6fa8297..812757d3 100644 --- a/example/package.json +++ b/example/package.json @@ -10,7 +10,7 @@ "postinstall": "patch-package" }, "dependencies": { - "@livekit/react-native-webrtc": "^137.0.1", + "@livekit/react-native-webrtc": "^137.0.2", "@react-native-async-storage/async-storage": "^1.17.10", "@react-navigation/native": "^6.0.8", "@react-navigation/native-stack": "^6.5.0", diff --git a/example/src/PreJoinPage.tsx b/example/src/PreJoinPage.tsx index 27afccf1..77fb3930 100644 --- a/example/src/PreJoinPage.tsx +++ b/example/src/PreJoinPage.tsx @@ -14,8 +14,9 @@ import type { RootStackParamList } from './App'; import { useTheme } from '@react-navigation/native'; import AsyncStorage from '@react-native-async-storage/async-storage'; -const DEFAULT_URL = 'wss://example.livekit.cloud'; -const DEFAULT_TOKEN = 'your-token-here'; +const DEFAULT_URL = 'ws://192.168.11.3:7880'; +const DEFAULT_TOKEN = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjA1MjM4NTksImlkZW50aXR5IjoicGhvbmUiLCJpc3MiOiJBUElUTFdySzh0YndyNDciLCJuYmYiOjE3NTc5MzE4NTksInN1YiI6InBob25lIiwidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.jpvzL9Mcqu1tS3dpITO-ffAyjzZtEvnq_p9ehD5B7RM'; const DEFAULT_E2EE = false; const DEFAULT_E2EE_KEY = ''; diff --git a/example/src/RoomPage.tsx b/example/src/RoomPage.tsx index a4b14b0e..de45e930 100644 --- a/example/src/RoomPage.tsx +++ b/example/src/RoomPage.tsx @@ -67,7 +67,10 @@ export const RoomPage = ({ }; }, []); - let { e2eeManager } = useRNE2EEManager({ sharedKey: e2eeKey }); + let { e2eeManager } = useRNE2EEManager({ + sharedKey: e2eeKey, + dataChannelEncryption: true, + }); let e2eeOptions = e2ee ? { e2eeManager } : undefined; return ( @@ -79,7 +82,7 @@ export const RoomPage = ({ adaptiveStream: { pixelDensity: 'screen' }, e2ee: e2eeOptions, }} - audio={true} + audio={false} video={true} > @@ -107,24 +110,22 @@ const RoomView = ({ navigation, e2ee }: RoomViewProps) => { }, [room, e2ee]); useIOSAudioManagement(room, true); - // Setup room listeners - const { send } = useDataChannel( - (dataMessage: ReceivedDataMessage) => { - //@ts-ignore - let decoder = new TextDecoder('utf-8'); - let message = decoder.decode(dataMessage.payload); + // Setup room listeners + useEffect(() => { + room.registerTextStreamHandler('lk.chat', async (reader, participant) => { + let message = await reader.readAll(); let title = 'Received Message'; - if (dataMessage.from != null) { - title = 'Received Message from ' + dataMessage.from?.identity; + if (participant != null) { + title = 'Received Message from ' + participant.identity; } Toast.show({ type: 'success', text1: title, text2: message, }); - } - ); + }); + }, [room]); const tracks = useTracks( [ @@ -229,17 +230,14 @@ const RoomView = ({ navigation, e2ee }: RoomViewProps) => { localParticipant.setScreenShareEnabled(enabled); } }} - sendData={(message: string) => { + sendData={async (message: string) => { Toast.show({ type: 'success', text1: 'Sending Message', text2: message, }); - //@ts-ignore - let encoder = new TextEncoder(); - let encodedData = encoder.encode(message); - send(encodedData, { reliable: true }); + room.localParticipant.sendText(message, { topic: 'lk.chat' }); }} onSimulate={(scenario) => { room.simulateScenario(scenario); diff --git a/example/yarn.lock b/example/yarn.lock index e2da58a4..f0270af8 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -2172,16 +2172,16 @@ __metadata: languageName: node linkType: hard -"@livekit/react-native-webrtc@npm:^137.0.1": - version: 137.0.1 - resolution: "@livekit/react-native-webrtc@npm:137.0.1" +"@livekit/react-native-webrtc@npm:^137.0.2": + version: 137.0.2 + resolution: "@livekit/react-native-webrtc@npm:137.0.2" dependencies: base64-js: 1.5.1 debug: 4.3.4 event-target-shim: 6.0.2 peerDependencies: react-native: ">=0.60.0" - checksum: 3341aafe7d9d1deb345e47247faa020b2cb5f1adbded135a244f0f9285eed720c9f62325bbce0e21c1da755ae24ae6c69eb07728cece709560f615908bde43a9 + checksum: c23467701c7dfb74e3b223dd955b20ba1586c2186383f5b2595fff9b65a36b2db0b660de9935e009b69bc5e0bbff047d0f598495f8464daae8db5f8d9f9b7c24 languageName: node linkType: hard @@ -6103,8 +6103,8 @@ __metadata: linkType: hard "livekit-client@npm:^2.15.4": - version: 2.15.4 - resolution: "livekit-client@npm:2.15.4" + version: 2.15.7 + resolution: "livekit-client@npm:2.15.7" dependencies: "@livekit/mutex": 1.1.1 "@livekit/protocol": 1.39.3 @@ -6117,7 +6117,7 @@ __metadata: webrtc-adapter: ^9.0.1 peerDependencies: "@types/dom-mediacapture-record": ^1 - checksum: 7ae2e2f5326c8f7925b359277fd12ddeefb0bc5069b0563a9e184f2b860351bf91733c1fa5fd7b65467b643776368058d485af888e2771fd68c61c409b950e05 + checksum: e9740963eef9ddb7f50604d2918b7ca9e696c1d190b968cbf2cb75997b1e0d4fce25c795ae60b1c92785cd9a679b6d606688092f31a6a1aaefc27ec92fc87f0f languageName: node linkType: hard @@ -6128,7 +6128,7 @@ __metadata: "@babel/core": ^7.20.0 "@babel/preset-env": ^7.20.0 "@babel/runtime": ^7.20.0 - "@livekit/react-native-webrtc": ^137.0.1 + "@livekit/react-native-webrtc": ^137.0.2 "@react-native-async-storage/async-storage": ^1.17.10 "@react-native/babel-preset": 0.74.84 "@react-native/eslint-config": 0.74.84 diff --git a/package.json b/package.json index 03a389c3..f78ecc60 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,13 @@ ], "dependencies": { "@livekit/components-react": "^2.8.1", + "@livekit/mutex": "^1.1.1", "array.prototype.at": "^1.1.1", "event-target-shim": "6.0.2", "events": "^3.3.0", "loglevel": "^1.8.0", "promise.allsettled": "^1.0.5", - "react-native-quick-base64": "2.1.1", + "react-native-quick-base64": "^2.2.1", "react-native-url-polyfill": "^1.3.0", "typed-emitter": "^2.1.0", "web-streams-polyfill": "^4.1.0", @@ -59,7 +60,7 @@ "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@commitlint/config-conventional": "^16.2.1", - "@livekit/react-native-webrtc": "^137.0.1", + "@livekit/react-native-webrtc": "^137.0.2", "@react-native/babel-preset": "0.74.84", "@react-native/eslint-config": "0.74.84", "@react-native/metro-config": "0.74.84", @@ -86,7 +87,7 @@ "typescript": "5.0.4" }, "peerDependencies": { - "@livekit/react-native-webrtc": "^137.0.1", + "@livekit/react-native-webrtc": "^137.0.2", "livekit-client": "^2.15.4", "react": "*", "react-native": "*" diff --git a/src/e2ee/RNE2EEManager.ts b/src/e2ee/RNE2EEManager.ts index b7c0eef1..186fd0ef 100644 --- a/src/e2ee/RNE2EEManager.ts +++ b/src/e2ee/RNE2EEManager.ts @@ -1,7 +1,10 @@ import { + RTCDataPacketCryptor, + RTCDataPacketCryptorFactory, RTCFrameCryptorAlgorithm, RTCFrameCryptorFactory, RTCRtpReceiver, + type RTCEncryptedPacket, type RTCFrameCryptor, type RTCRtpSender, } from '@livekit/react-native-webrtc'; @@ -16,6 +19,9 @@ import { type BaseE2EEManager, type E2EEManagerCallbacks, EncryptionEvent, + type DecryptDataResponseMessage, + type EncryptDataResponseMessage, + Mutex, } from 'livekit-client'; import type RNKeyProvider from './RNKeyProvider'; import type RTCEngine from 'livekit-client/dist/src/room/RTCEngine'; @@ -36,11 +42,32 @@ export default class RNE2EEManager RTCFrameCryptorAlgorithm.kAesGcm; private encryptionEnabled: boolean = false; + private dataChannelEncryptionEnabled: boolean = false; - constructor(keyProvider: RNKeyProvider) { + private dataPacketCryptorLock = new Mutex(); + private dataPacketCryptor: RTCDataPacketCryptor | undefined = undefined; + constructor( + keyProvider: RNKeyProvider, + dcEncryptionEnabled: boolean = false + ) { super(); this.keyProvider = keyProvider; this.encryptionEnabled = false; + this.dataChannelEncryptionEnabled = dcEncryptionEnabled; + } + + get isEnabled(): boolean { + return this.encryptionEnabled; + } + get isDataChannelEncryptionEnabled(): boolean { + console.log( + 'isDataChannelEncryptionEnabled?', + this.isEnabled, + this.encryptionEnabled, + this.dataChannelEncryptionEnabled, + this.isEnabled && this.dataChannelEncryptionEnabled + ); + return this.isEnabled && this.dataChannelEncryptionEnabled; } setup(room: Room) { @@ -78,7 +105,16 @@ export default class RNE2EEManager await frameCryptor.dispose(); } } - ); + ) + .on(RoomEvent.SignalConnected, () => { + if (!this.room) { + throw new TypeError(`expected room to be present on signal connect`); + } + this.setParticipantCryptorEnabled( + this.room.localParticipant.isE2EEEnabled, + this.room.localParticipant.identity + ); + }); } private async setupE2EESender( @@ -133,6 +169,91 @@ export default class RNE2EEManager this.keyProvider.setSifTrailer(trailer); } + private async getDataPacketCryptor(): Promise { + let dataPacketCryptor = this.dataPacketCryptor; + if (dataPacketCryptor) { + return dataPacketCryptor; + } + + let unlock = await this.dataPacketCryptorLock.lock(); + + try { + dataPacketCryptor = this.dataPacketCryptor; + if (dataPacketCryptor) { + return dataPacketCryptor; + } + + dataPacketCryptor = + await RTCDataPacketCryptorFactory.createDataPacketCryptor( + this.algorithm, + this.keyProvider.rtcKeyProvider + ); + + this.dataPacketCryptor = dataPacketCryptor; + return dataPacketCryptor; + } finally { + unlock(); + } + } + async encryptData( + data: Uint8Array + ): Promise { + let room = this.room; + if (!room) { + throw new Error("e2eemanager isn't setup with room!"); + } + + let participantId = room.localParticipant.identity; + + let dataPacketCryptor = await this.getDataPacketCryptor(); + + let encryptedPacket = await dataPacketCryptor.encrypt( + participantId, + this.keyProvider.getLatestKeyIndex(participantId), + data + ); + + if (!encryptedPacket) { + throw new Error('encryption for packet failed'); + } + return { + uuid: '', //not used + payload: encryptedPacket.payload, + iv: encryptedPacket.iv, + keyIndex: encryptedPacket.keyIndex, + }; + } + + async handleEncryptedData( + payload: Uint8Array, + iv: Uint8Array, + participantIdentity: string, + keyIndex: number + ): Promise< + DecryptDataResponseMessage['data'] | EncryptDataResponseMessage['data'] + > { + let packet = { + payload, + iv, + keyIndex, + } satisfies RTCEncryptedPacket; + + let dataPacketCryptor = await this.getDataPacketCryptor(); + let decryptedData = await dataPacketCryptor.decrypt( + participantIdentity, + packet + ); + + if (!decryptedData) { + throw new Error('decryption for packet failed'); + } + + return { + uuid: '', //not used + payload: decryptedData, + } satisfies DecryptDataResponseMessage['data']; + } + // Utility methods ////////////////////// @@ -171,11 +292,13 @@ export default class RNE2EEManager enabled: boolean, participantIdentity: string ): void { + console.log('setParticipantCryptorEnabled', enabled, participantIdentity); if ( this.encryptionEnabled !== enabled && participantIdentity === this.room?.localParticipant.identity ) { this.encryptionEnabled = enabled; + console.log('setting encryption enabled to ', enabled); this.emit( EncryptionEvent.ParticipantEncryptionStatusChanged, enabled, diff --git a/src/hooks/useE2EEManager.ts b/src/hooks/useE2EEManager.ts index 49b551e0..dbc90b9d 100644 --- a/src/hooks/useE2EEManager.ts +++ b/src/hooks/useE2EEManager.ts @@ -6,6 +6,7 @@ import type { RNKeyProviderOptions } from '../e2ee/RNKeyProvider'; export type UseRNE2EEManagerOptions = { keyProviderOptions?: RNKeyProviderOptions; sharedKey: string | Uint8Array; + dataChannelEncryption?: boolean; }; export interface RNE2EEManagerState { @@ -22,7 +23,9 @@ export function useRNE2EEManager( let [keyProvider] = useState( () => new RNKeyProvider(options.keyProviderOptions ?? {}) ); - let [e2eeManager] = useState(() => new RNE2EEManager(keyProvider)); + let [e2eeManager] = useState( + () => new RNE2EEManager(keyProvider, options.dataChannelEncryption ?? false) + ); useEffect(() => { let setup = async () => { diff --git a/yarn.lock b/yarn.lock index 0cdc1e5d..746712f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2417,7 +2417,7 @@ __metadata: languageName: node linkType: hard -"@livekit/mutex@npm:1.1.1": +"@livekit/mutex@npm:1.1.1, @livekit/mutex@npm:^1.1.1": version: 1.1.1 resolution: "@livekit/mutex@npm:1.1.1" checksum: 44a31eb7a913357ffb57d04eaa20f7507c0a659638c6dfaba9a413c21a3397aa351497f7c77bca3c06a29ac2cbe83698a5f96b9230012a24b86ac8366e9b8666 @@ -2433,16 +2433,16 @@ __metadata: languageName: node linkType: hard -"@livekit/react-native-webrtc@npm:^137.0.1": - version: 137.0.1 - resolution: "@livekit/react-native-webrtc@npm:137.0.1" +"@livekit/react-native-webrtc@npm:^137.0.2": + version: 137.0.2 + resolution: "@livekit/react-native-webrtc@npm:137.0.2" dependencies: base64-js: 1.5.1 debug: 4.3.4 event-target-shim: 6.0.2 peerDependencies: react-native: ">=0.60.0" - checksum: 3341aafe7d9d1deb345e47247faa020b2cb5f1adbded135a244f0f9285eed720c9f62325bbce0e21c1da755ae24ae6c69eb07728cece709560f615908bde43a9 + checksum: c23467701c7dfb74e3b223dd955b20ba1586c2186383f5b2595fff9b65a36b2db0b660de9935e009b69bc5e0bbff047d0f598495f8464daae8db5f8d9f9b7c24 languageName: node linkType: hard @@ -2455,7 +2455,8 @@ __metadata: "@babel/runtime": ^7.20.0 "@commitlint/config-conventional": ^16.2.1 "@livekit/components-react": ^2.8.1 - "@livekit/react-native-webrtc": ^137.0.1 + "@livekit/mutex": ^1.1.1 + "@livekit/react-native-webrtc": ^137.0.2 "@react-native/babel-preset": 0.74.84 "@react-native/eslint-config": 0.74.84 "@react-native/metro-config": 0.74.84 @@ -2482,7 +2483,7 @@ __metadata: react: 18.2.0 react-native: 0.74.2 react-native-builder-bob: ^0.18.2 - react-native-quick-base64: 2.1.1 + react-native-quick-base64: ^2.2.1 react-native-url-polyfill: ^1.3.0 release-it: ^14.2.2 typed-emitter: ^2.1.0 @@ -2491,7 +2492,7 @@ __metadata: web-streams-polyfill: ^4.1.0 well-known-symbols: ^4.1.0 peerDependencies: - "@livekit/react-native-webrtc": ^137.0.1 + "@livekit/react-native-webrtc": ^137.0.2 livekit-client: ^2.15.4 react: "*" react-native: "*" @@ -8800,8 +8801,8 @@ __metadata: linkType: hard "livekit-client@npm:^2.15.4": - version: 2.15.4 - resolution: "livekit-client@npm:2.15.4" + version: 2.15.7 + resolution: "livekit-client@npm:2.15.7" dependencies: "@livekit/mutex": 1.1.1 "@livekit/protocol": 1.39.3 @@ -8814,7 +8815,7 @@ __metadata: webrtc-adapter: ^9.0.1 peerDependencies: "@types/dom-mediacapture-record": ^1 - checksum: 7ae2e2f5326c8f7925b359277fd12ddeefb0bc5069b0563a9e184f2b860351bf91733c1fa5fd7b65467b643776368058d485af888e2771fd68c61c409b950e05 + checksum: e9740963eef9ddb7f50604d2918b7ca9e696c1d190b968cbf2cb75997b1e0d4fce25c795ae60b1c92785cd9a679b6d606688092f31a6a1aaefc27ec92fc87f0f languageName: node linkType: hard @@ -10730,15 +10731,13 @@ __metadata: languageName: node linkType: hard -"react-native-quick-base64@npm:2.1.1": - version: 2.1.1 - resolution: "react-native-quick-base64@npm:2.1.1" - dependencies: - base64-js: ^1.5.1 +"react-native-quick-base64@npm:^2.2.1": + version: 2.2.2 + resolution: "react-native-quick-base64@npm:2.2.2" peerDependencies: react: "*" react-native: "*" - checksum: 13c374b20153418de7c89001ba9c1313aa0e6f1cbb4a829077bd8d4ed88b159e31f3be9a1ed96ac9d1c647be6fafca00aceaf59379bdf2e8ea029907f944dd4c + checksum: 51cd8df0b7f78d4c10996f157927cc075b8a9e85f7d0e48790a4247a033ca335e440c96572969fb6f6b49135d51dbf87c6d659fba509b51b9237957d41a6c580 languageName: node linkType: hard