From 0bb9f42a3a20b31142af10eeeae2142a5135aab0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Damian=20B=C3=BCchel?= <damian.buechel@let.ethz.ch>
Date: Tue, 7 Mar 2023 23:12:59 +0100
Subject: [PATCH] SEBWIN-623, SEBWIN-628, SEBWIN-641, #521: Updated Zoom WebSDK
 to version 2.10.1 and changed authentication to use SDK key and JWT token.

---
 .../DataMapping/ProctoringDataMapper.cs       |  22 --
 .../ConfigurationData/Keys.cs                 |   2 -
 .../ProctoringControl.cs                      |   3 +-
 .../ProctoringController.cs                   |   9 +-
 SafeExamBrowser.Proctoring/Zoom/index.html    | 271 ++++++++----------
 .../Events/ProctoringInstructionEventArgs.cs  |   2 +-
 SafeExamBrowser.Server/Parser.cs              |   2 +-
 .../Proctoring/ZoomSettings.cs                |  15 +-
 8 files changed, 138 insertions(+), 188 deletions(-)

diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs
index cf4a2f38..a55f36d6 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs
@@ -92,12 +92,6 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
 				case Keys.Proctoring.Zoom.AllowRaiseHand:
 					MapZoomAllowRaiseHands(settings, value);
 					break;
-				case Keys.Proctoring.Zoom.ApiKey:
-					MapZoomApiKey(settings, value);
-					break;
-				case Keys.Proctoring.Zoom.ApiSecret:
-					MapZoomApiSecret(settings, value);
-					break;
 				case Keys.Proctoring.Zoom.AudioMuted:
 					MapZoomAudioMuted(settings, value);
 					break;
@@ -353,22 +347,6 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
 			}
 		}
 
-		private void MapZoomApiKey(AppSettings settings, object value)
-		{
-			if (value is string key)
-			{
-				settings.Proctoring.Zoom.ApiKey = key;
-			}
-		}
-
-		private void MapZoomApiSecret(AppSettings settings, object value)
-		{
-			if (value is string secret)
-			{
-				settings.Proctoring.Zoom.ApiSecret = secret;
-			}
-		}
-
 		private void MapZoomAudioMuted(AppSettings settings, object value)
 		{
 			if (value is bool muted)
diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
index e14897dc..a6cd5795 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
@@ -258,8 +258,6 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
 				internal const string AllowChat = "zoomFeatureFlagChat";
 				internal const string AllowClosedCaptions = "zoomFeatureFlagCloseCaptions";
 				internal const string AllowRaiseHand = "zoomFeatureFlagRaiseHand";
-				internal const string ApiKey = "zoomApiKey";
-				internal const string ApiSecret = "zoomApiSecret";
 				internal const string AudioMuted = "zoomAudioMuted";
 				internal const string Enabled = "zoomEnable";
 				internal const string MeetingNumber = "zoomRoom";
diff --git a/SafeExamBrowser.Proctoring/ProctoringControl.cs b/SafeExamBrowser.Proctoring/ProctoringControl.cs
index 2d04669c..568cd95f 100644
--- a/SafeExamBrowser.Proctoring/ProctoringControl.cs
+++ b/SafeExamBrowser.Proctoring/ProctoringControl.cs
@@ -96,10 +96,9 @@ namespace SafeExamBrowser.Proctoring
 			}
 			else if (settings.Zoom.Enabled)
 			{
-				credentials.Add(new JProperty("apiKey", settings.Zoom.ApiKey));
-				credentials.Add(new JProperty("apiSecret", settings.Zoom.ApiSecret));
 				credentials.Add(new JProperty("meetingNumber", settings.Zoom.MeetingNumber));
 				credentials.Add(new JProperty("password", settings.Zoom.Password));
+				credentials.Add(new JProperty("sdkKey", settings.Zoom.SdkKey));
 				credentials.Add(new JProperty("signature", settings.Zoom.Signature));
 				credentials.Add(new JProperty("userName", settings.Zoom.UserName));
 			}
diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs
index e6313ead..7c1e936b 100644
--- a/SafeExamBrowser.Proctoring/ProctoringController.cs
+++ b/SafeExamBrowser.Proctoring/ProctoringController.cs
@@ -103,8 +103,7 @@ namespace SafeExamBrowser.Proctoring
 			}
 			else if (settings.Zoom.Enabled)
 			{
-				start = !string.IsNullOrWhiteSpace(settings.Zoom.ApiKey);
-				start &= !string.IsNullOrWhiteSpace(settings.Zoom.ApiSecret) || !string.IsNullOrWhiteSpace(settings.Zoom.Signature);
+				start = !string.IsNullOrWhiteSpace(settings.Zoom.SdkKey) && !string.IsNullOrWhiteSpace(settings.Zoom.Signature);
 				start &= !string.IsNullOrWhiteSpace(settings.Zoom.MeetingNumber);
 				start &= !string.IsNullOrWhiteSpace(settings.Zoom.UserName);
 			}
@@ -168,9 +167,9 @@ namespace SafeExamBrowser.Proctoring
 			settings.JitsiMeet.ServerUrl = args.JitsiMeetServerUrl;
 			settings.JitsiMeet.Token = args.JitsiMeetToken;
 
-			settings.Zoom.ApiKey = args.ZoomApiKey;
 			settings.Zoom.MeetingNumber = args.ZoomMeetingNumber;
 			settings.Zoom.Password = args.ZoomPassword;
+			settings.Zoom.SdkKey = args.ZoomSdkKey;
 			settings.Zoom.Signature = args.ZoomSignature;
 			settings.Zoom.Subject = args.ZoomSubject;
 			settings.Zoom.UserName = args.ZoomUserName;
@@ -272,8 +271,8 @@ namespace SafeExamBrowser.Proctoring
 					Thread.Sleep(2000);
 
 					window.Close();
-					control = default(ProctoringControl);
-					window = default(IProctoringWindow);
+					control = default;
+					window = default;
 					fileSystem.Delete(filePath);
 
 					logger.Info("Stopped proctoring.");
diff --git a/SafeExamBrowser.Proctoring/Zoom/index.html b/SafeExamBrowser.Proctoring/Zoom/index.html
index 515aef40..85f7169d 100644
--- a/SafeExamBrowser.Proctoring/Zoom/index.html
+++ b/SafeExamBrowser.Proctoring/Zoom/index.html
@@ -1,167 +1,148 @@
 <html>
-    <head>
-        <meta charset="utf-8" />
-        <link type="text/css" rel="stylesheet" href="https://source.zoom.us/2.5.0/css/bootstrap.css" />
-        <link type="text/css" rel="stylesheet" href="https://source.zoom.us/2.5.0/css/react-select.css" />
-    </head>
-    <body>
-        <script src="https://source.zoom.us/2.5.0/lib/vendor/react.min.js"></script>
-        <script src="https://source.zoom.us/2.5.0/lib/vendor/react-dom.min.js"></script>
-        <script src="https://source.zoom.us/2.5.0/lib/vendor/redux.min.js"></script>
-        <script src="https://source.zoom.us/2.5.0/lib/vendor/redux-thunk.min.js"></script>
-        <script src="https://source.zoom.us/2.5.0/lib/vendor/lodash.min.js"></script>
-        <script src="https://source.zoom.us/zoom-meeting-2.5.0.min.js"></script>
-        <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js"></script>
-        <script type="text/javascript">
-            var audioJoin = 0;
-            var videoJoin = 0;
-            var join = 0;
+	<head>
+		<link type="text/css" rel="stylesheet" href="https://source.zoom.us/2.10.1/css/bootstrap.css" />
+		<link type="text/css" rel="stylesheet" href="https://source.zoom.us/2.10.1/css/react-select.css" />
+		<meta charset="utf-8" />
+		<meta http-equiv="origin-trial" content="" />
+	</head>
+	<body>
+		<script src="https://source.zoom.us/2.10.1/lib/vendor/react.min.js"></script>
+		<script src="https://source.zoom.us/2.10.1/lib/vendor/react-dom.min.js"></script>
+		<script src="https://source.zoom.us/2.10.1/lib/vendor/redux.min.js"></script>
+		<script src="https://source.zoom.us/2.10.1/lib/vendor/redux-thunk.min.js"></script>
+		<script src="https://source.zoom.us/2.10.1/lib/vendor/lodash.min.js"></script>
+		<script src="https://source.zoom.us/zoom-meeting-2.10.1.min.js"></script>
+		<script type="text/javascript">
+			var audioJoin = 0;
+			var videoJoin = 0;
+			var join = 0;
 
-            function controlUserInterface(_) {
-                var appSignal = document.getElementById('app-signal');
-                var audioButton = document.getElementsByClassName('join-audio-by-voip__join-btn')[0];
-                var audioContainer = document.getElementsByClassName('join-audio-container')[0];
-                var joinButton = document.getElementsByClassName('joinWindowBtn')[0];
-                var leave = document.getElementsByClassName('footer__leave-btn-container')[0];
-                var more = document.getElementsByClassName('more-button')[0];
-                var participantControls = document.getElementsByClassName('participants-item__buttons')[0];
-                var raiseHandContainer = document.getElementsByClassName('participants-section-container__participants-footer')[0];
-                var videoButton = document.getElementsByClassName('send-video-container__btn')[0];
-                var videoContainer = document.getElementsByClassName('send-video-container')[0];
+			function controlUserInterface(_) {
+				var appSignal = document.getElementById('app-signal');
+				var audioButton = document.getElementsByClassName('join-audio-by-voip__join-btn')[0];
+				var audioContainer = document.getElementsByClassName('join-audio-container')[0];
+				var joinButton = document.getElementsByClassName('joinWindowBtn')[0];
+				var leave = document.getElementsByClassName('footer__leave-btn-container')[0];
+				var more = document.getElementsByClassName('more-button')[0];
+				var participantControls = document.getElementsByClassName('participants-item__buttons')[0];
+				var raiseHandContainer = document.getElementsByClassName('participants-section-container__participants-footer')[0];
+				var videoButton = document.getElementsByClassName('send-video-container__btn')[0];
+				var videoContainer = document.getElementsByClassName('send-video-container')[0];
 
-                if (appSignal) {
-                    appSignal.style.visibility = 'hidden';
-                }
+				if (appSignal) {
+					appSignal.style.visibility = 'hidden';
+				}
 
-                if (audioButton && !'%_AUDIO_MUTED_%' && audioJoin < 500) {
-                    audioButton.click();
-                    audioJoin++;
-                }
+				if (audioButton && !'%_AUDIO_MUTED_%' && audioJoin < 500) {
+					audioButton.click();
+					audioJoin++;
+				}
 
-                if (audioContainer && !'%_AUDIO_MUTED_%') {
-                    audioContainer.style.visibility = 'hidden';
-                }
+				if (audioContainer && !'%_AUDIO_MUTED_%') {
+					audioContainer.style.visibility = 'hidden';
+				}
 
-                if (joinButton && join < 500) {
-                    joinButton.click();
-                    join++;
-                }
+				if (joinButton && join < 500) {
+					joinButton.click();
+					join++;
+				}
 
-                if (leave) {
-                    leave.style.visibility = 'hidden';
-                }
+				if (leave) {
+					leave.style.visibility = 'hidden';
+				}
 
-                if (more) {
-                    more.style.visibility = 'hidden';
-                }
+				if (more) {
+					more.style.visibility = 'hidden';
+				}
 
-                if (participantControls) {
-                    participantControls.style.visibility = 'hidden';
-                }
+				if (participantControls) {
+					participantControls.style.visibility = 'hidden';
+				}
 
-                if (raiseHandContainer && !'%_ALLOW_RAISE_HAND_%') {
-                    raiseHandContainer.style.visibility = 'hidden';
-                }
+				if (raiseHandContainer && !'%_ALLOW_RAISE_HAND_%') {
+					raiseHandContainer.style.visibility = 'hidden';
+				}
 
-                if (videoButton && !'%_VIDEO_MUTED_%' && videoJoin < 500) {
-                    videoButton.click();
-                    videoJoin++;
-                }
+				if (videoButton && !'%_VIDEO_MUTED_%' && videoJoin < 500) {
+					videoButton.click();
+					videoJoin++;
+				}
 
-                if (videoContainer && !'%_VIDEO_MUTED_%') {
-                    videoContainer.style.visibility = 'hidden';
-                }
+				if (videoContainer && !'%_VIDEO_MUTED_%') {
+					videoContainer.style.visibility = 'hidden';
+				}
 
-                requestAnimationFrame(controlUserInterface);
-            }
+				requestAnimationFrame(controlUserInterface);
+			}
 
-            function startMeeting(credentials) {
-                const ATTENDEE = 0;
-                var signature = credentials.signature;
+			function startMeeting(credentials) {
+				var error = function (res) {
+					alert(`Failed to initialize meeting: ${JSON.stringify(res)}`);
+				};
 
-                if (!ZoomMtg.checkSystemRequirements()) {
-                    alert('This system does not meet the necessary requirements for Zoom!');
-                }
+				var success = function () {
+					requestAnimationFrame(controlUserInterface);
 
-                ZoomMtg.setZoomJSLib('https://source.zoom.us/2.5.0/lib', '/av');
-                ZoomMtg.preLoadWasm();
-                ZoomMtg.prepareJssdk();
+					ZoomMtg.join({
+						meetingNumber: new Number(credentials.meetingNumber),
+						passWord: credentials.password,
+						sdkKey: credentials.sdkKey,
+						signature: credentials.signature,
+						userName: credentials.userName,
+						error: function (res) {
+							alert(`Failed to join meeting: ${JSON.stringify(res)}`);
+						}
+					});
+				};
 
-                if (!signature) {
-                    signature = ZoomMtg.generateSignature({
-                        meetingNumber: credentials.meetingNumber,
-                        apiKey: credentials.apiKey,
-                        apiSecret: credentials.apiSecret,
-                        role: ATTENDEE,
-                        error: function (res) {
-                            alert(`Failed to generate signature: ${JSON.stringify(res)}`);
-                        }
-                    });
-                }
+				var params = {
+					audioPanelAlwaysOpen: false,
+					disableCallOut: true,
+					disableInvite: true,
+					disableJoinAudio: false,
+					disableRecord: true,
+					disableReport: true,
+					disableVoIP: false,
+					leaveUrl: 'doesnotexist',
+					isLockBottom: true,
+					isShowJoiningErrorDialog: true,
+					isSupportAV: true,
+					isSupportBreakout: false,
+					isSupportChat: '%_ALLOW_CHAT_%',
+					isSupportCC: '%_ALLOW_CLOSED_CAPTIONS_%',
+					isSupportPolling: false,
+					isSupportQA: false,
+					isSupportNonverbal: false,
+					screenShare: false,
+					sharingMode: 'both',
+					showMeetingHeader: true,
+					showPureSharingContent: false,
+					videoDrag: true,
+					videoHeader: true,
+					meetingInfo: ['topic', 'host', 'participant'],
+					error: error,
+					success: success
+				};
 
-                ZoomMtg.init({
-                    audioPanelAlwaysOpen: false,
-                    disableCallOut: true,
-                    disableInvite: true,
-                    disableJoinAudio: false,
-                    disableRecord: true,
-                    disableReport: true,
-                    disableVoIP: false,
-                    leaveUrl: 'doesnotexist',
-                    isLockBottom: true,
-                    isShowJoiningErrorDialog: true,
-                    isSupportAV: true,
-                    isSupportBreakout: false,
-                    isSupportChat: '%_ALLOW_CHAT_%',
-                    isSupportCC: '%_ALLOW_CLOSED_CAPTIONS_%',
-                    isSupportPolling: false,
-                    isSupportQA: false,
-                    isSupportNonverbal: false,
-                    screenShare: false,
-                    sharingMode: 'both',
-                    showMeetingHeader: true,
-                    showPureSharingContent: false,
-                    videoDrag: true,
-                    videoHeader: true,
-                    meetingInfo: [
-                        'topic',
-                        'host',
-                        'participant',
-                        //'mn',
-                        //'pwd',
-                        //'telPwd',
-                        //'invite',
-                        //'dc'
-                    ],
-                    error: function (res) {
-                        alert(`Failed to initialize meeting: ${JSON.stringify(res)}`);
-                    },
-                    success: function () {
-                        requestAnimationFrame(controlUserInterface);
+				if (!ZoomMtg.checkSystemRequirements()) {
+					alert('This system does not meet the necessary requirements for Zoom!');
+				}
 
-                        ZoomMtg.join({
-                            apiKey: credentials.apiKey,
-                            meetingNumber: credentials.meetingNumber,
-                            passWord: credentials.password,
-                            signature: signature,
-                            userName: credentials.userName,
-                            error: function (res) {
-                                alert(`Failed to join meeting: ${JSON.stringify(res)}`);
-                            }
-                        });
-                    }
-                });
-            }
+				ZoomMtg.setZoomJSLib('https://source.zoom.us/2.10.1/lib', '/av');
+				ZoomMtg.preLoadWasm();
+				ZoomMtg.prepareWebSDK();
+				ZoomMtg.init(params);
+			}
 
-            function webMessageReceived(args) {
-                if ('credentials' in args.data) {
-                    startMeeting(args.data.credentials);
-                }
-            }
+			function webMessageReceived(args) {
+				if ('credentials' in args.data) {
+					startMeeting(args.data.credentials);
+				}
+			}
 
-            window.addEventListener('unload', () => ZoomMtg.leaveMeeting({}));
-            window.chrome.webview.addEventListener('message', webMessageReceived);
-            window.chrome.webview.postMessage('credentials');
-        </script>
-    </body>
+			window.addEventListener('unload', () => ZoomMtg.leaveMeeting({}));
+			window.chrome.webview.addEventListener('message', webMessageReceived);
+			window.chrome.webview.postMessage('credentials');
+		</script>
+	</body>
 </html>
\ No newline at end of file
diff --git a/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionEventArgs.cs b/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionEventArgs.cs
index 2cbfd313..bae66b4b 100644
--- a/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionEventArgs.cs
+++ b/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionEventArgs.cs
@@ -16,9 +16,9 @@ namespace SafeExamBrowser.Server.Contracts.Events
 		public string JitsiMeetRoomName { get; set; }
 		public string JitsiMeetServerUrl { get; set; }
 		public string JitsiMeetToken { get; set; }
-		public string ZoomApiKey { get; set; }
 		public string ZoomMeetingNumber { get; set; }
 		public string ZoomPassword { get; set; }
+		public string ZoomSdkKey { get; set; }
 		public string ZoomSignature { get; set; }
 		public string ZoomSubject { get; set; }
 		public string ZoomUserName { get; set; }
diff --git a/SafeExamBrowser.Server/Parser.cs b/SafeExamBrowser.Server/Parser.cs
index 3ed53020..3c6d7cfd 100644
--- a/SafeExamBrowser.Server/Parser.cs
+++ b/SafeExamBrowser.Server/Parser.cs
@@ -298,9 +298,9 @@ namespace SafeExamBrowser.Server
 					attributes.Instruction.JitsiMeetToken = attributesJson["jitsiMeetToken"].Value<string>();
 					break;
 				case "ZOOM":
-					attributes.Instruction.ZoomApiKey = attributesJson["zoomAPIKey"].Value<string>();
 					attributes.Instruction.ZoomMeetingNumber = attributesJson["zoomRoom"].Value<string>();
 					attributes.Instruction.ZoomPassword = attributesJson["zoomMeetingKey"].Value<string>();
+					attributes.Instruction.ZoomSdkKey = attributesJson["zoomAPIKey"].Value<string>();
 					attributes.Instruction.ZoomSignature = attributesJson["zoomToken"].Value<string>();
 					attributes.Instruction.ZoomSubject = attributesJson["zoomSubject"].Value<string>();
 					attributes.Instruction.ZoomUserName = attributesJson["zoomUserName"].Value<string>();
diff --git a/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs b/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs
index bec6c3d3..fca61ea5 100644
--- a/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs
+++ b/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs
@@ -31,16 +31,6 @@ namespace SafeExamBrowser.Settings.Proctoring
 		/// </summary>
 		public bool AllowRaiseHand { get; set; }
 
-		/// <summary>
-		/// The API key to be used for authentication.
-		/// </summary>
-		public string ApiKey { get; set; }
-
-		/// <summary>
-		/// The API secret to be used for authentication.
-		/// </summary>
-		public string ApiSecret { get; set; }
-
 		/// <summary>
 		/// Determines whether the audio starts muted.
 		/// </summary>
@@ -71,6 +61,11 @@ namespace SafeExamBrowser.Settings.Proctoring
 		/// </summary>
 		public bool ReceiveVideo { get; set; }
 
+		/// <summary>
+		/// The SDK key to be used for authentication.
+		/// </summary>
+		public string SdkKey { get; set; }
+
 		/// <summary>
 		/// Determines whether the audio stream of the user will be sent to the server.
 		/// </summary>