diff --git a/SafeExamBrowser.Browser/BrowserApplication.cs b/SafeExamBrowser.Browser/BrowserApplication.cs index 942a5760..f5ef88b9 100644 --- a/SafeExamBrowser.Browser/BrowserApplication.cs +++ b/SafeExamBrowser.Browser/BrowserApplication.cs @@ -115,16 +115,18 @@ namespace SafeExamBrowser.Browser if (success) { - if (settings.UseTemporaryDownAndUploadDirectory) - { - CreateTemporaryDownAndUploadDirectory(); - } + InitializeIntegrityKeys(); if (settings.DeleteCookiesOnStartup) { DeleteCookies(); } + if (settings.UseTemporaryDownAndUploadDirectory) + { + CreateTemporaryDownAndUploadDirectory(); + } + logger.Info("Initialized browser."); } else @@ -362,6 +364,22 @@ namespace SafeExamBrowser.Browser return cefSettings; } + private void InitializeIntegrityKeys() + { + logger.Debug($"Browser Exam Key (BEK) transmission is {(settings.SendBrowserExamKey ? "enabled" : "disabled")}."); + logger.Debug($"Configuration Key (CK) transmission is {(settings.SendConfigurationKey ? "enabled" : "disabled")}."); + + if (settings.CustomBrowserExamKey != default) + { + keyGenerator.UseCustomBrowserExamKey(settings.CustomBrowserExamKey); + logger.Debug($"The browser application will be using a custom browser exam key."); + } + else + { + logger.Debug($"The browser application will be using the default browser exam key."); + } + } + private void InitializeProxySettings(CefSettings cefSettings) { if (settings.Proxy.Policy == ProxyPolicy.Custom) diff --git a/SafeExamBrowser.Configuration.Contracts/Cryptography/IKeyGenerator.cs b/SafeExamBrowser.Configuration.Contracts/Cryptography/IKeyGenerator.cs index ad8242bc..be5e9ee3 100644 --- a/SafeExamBrowser.Configuration.Contracts/Cryptography/IKeyGenerator.cs +++ b/SafeExamBrowser.Configuration.Contracts/Cryptography/IKeyGenerator.cs @@ -27,5 +27,10 @@ namespace SafeExamBrowser.Configuration.Contracts.Cryptography /// Calculates the hash value of the configuration key (CK) for the given URL. /// string CalculateConfigurationKeyHash(string configurationKey, string url); + + /// + /// Specifies that a custom browser exam key (BEK) should be used. + /// + void UseCustomBrowserExamKey(string browserExamKey); } } diff --git a/SafeExamBrowser.Configuration/Cryptography/KeyGenerator.cs b/SafeExamBrowser.Configuration/Cryptography/KeyGenerator.cs index 1fc2657e..36a65d02 100644 --- a/SafeExamBrowser.Configuration/Cryptography/KeyGenerator.cs +++ b/SafeExamBrowser.Configuration/Cryptography/KeyGenerator.cs @@ -67,6 +67,15 @@ namespace SafeExamBrowser.Configuration.Cryptography return key; } + public void UseCustomBrowserExamKey(string browserExamKey) + { + if (browserExamKey != default) + { + this.browserExamKey = browserExamKey; + logger.Debug("Initialized custom browser exam key."); + } + } + private string ComputeBrowserExamKey(string configurationKey, byte[] salt) { lock (@lock) diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ServerOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ServerOperationTests.cs index e77690cd..7ef4a093 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ServerOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ServerOperationTests.cs @@ -80,7 +80,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations server.Setup(s => s.Connect()).Returns(new ServerResponse(true)).Callback(() => connect = ++counter); server.Setup(s => s.Initialize(It.IsAny())).Callback(() => initialize = ++counter); server.Setup(s => s.GetConnectionInfo()).Returns(connection).Callback(() => getConnection = ++counter); - server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true)); + server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true, default)); server .Setup(s => s.GetAvailableExams(It.IsAny())) .Returns(new ServerResponse>(true, default(IEnumerable))) @@ -276,7 +276,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations server.Setup(s => s.GetConnectionInfo()).Returns(connection); server.Setup(s => s.GetAvailableExams(It.IsAny())).Returns(new ServerResponse>(true, new[] { exam })); server.Setup(s => s.GetConfigurationFor(It.IsAny())).Returns(new ServerResponse(true, new Uri("file:///configuration.seb"))); - server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true)); + server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true, default)); sut.ActionRequired += (args) => Assert.Fail(); var result = sut.Perform(); @@ -336,7 +336,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations server.Setup(s => s.Connect()).Returns(new ServerResponse(true)).Callback(() => connect = ++counter); server.Setup(s => s.Initialize(It.IsAny())).Callback(() => initialize = ++counter); server.Setup(s => s.GetConnectionInfo()).Returns(connection).Callback(() => getConnection = ++counter); - server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true)); + server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true, default)); server .Setup(s => s.GetAvailableExams(It.IsAny())) .Returns(new ServerResponse>(true, default(IEnumerable))) @@ -532,7 +532,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations server.Setup(s => s.GetConnectionInfo()).Returns(connection); server.Setup(s => s.GetAvailableExams(It.IsAny())).Returns(new ServerResponse>(true, new[] { exam })); server.Setup(s => s.GetConfigurationFor(It.IsAny())).Returns(new ServerResponse(true, new Uri("file:///configuration.seb"))); - server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true)); + server.Setup(s => s.SendSelectedExam(It.IsAny())).Returns(new ServerResponse(true, default)); sut.ActionRequired += (args) => Assert.Fail(); var result = sut.Repeat(); diff --git a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs index 92dc14b3..5a07a129 100644 --- a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs @@ -51,6 +51,7 @@ namespace SafeExamBrowser.Runtime.Operations if (Context.Next.Settings.SessionMode == SessionMode.Server) { + var browserExamKey = default(string); var exam = default(Exam); var exams = default(IEnumerable); var uri = default(Uri); @@ -84,7 +85,12 @@ namespace SafeExamBrowser.Runtime.Operations if (success && result == OperationResult.Success) { - (abort, fallback, success) = TryPerformWithFallback(() => server.SendSelectedExam(exam)); + (abort, fallback, success) = TryPerformWithFallback(() => server.SendSelectedExam(exam), out browserExamKey); + } + + if (browserExamKey != default) + { + Context.Next.Settings.Browser.CustomBrowserExamKey = browserExamKey; } if (abort) diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs index 385101a4..fc227b25 100644 --- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs +++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs @@ -105,9 +105,9 @@ namespace SafeExamBrowser.Server.Contracts ServerResponse RaiseHand(string message = default); /// - /// Sends the selected exam to the server. + /// Sends the selected exam to the server. Optionally returns a custom browser exam key to be used for the active session. /// - ServerResponse SendSelectedExam(Exam exam); + ServerResponse SendSelectedExam(Exam exam); /// /// Sends the given user session identifier of a LMS and thus establishes a connection with the server. diff --git a/SafeExamBrowser.Server/Parser.cs b/SafeExamBrowser.Server/Parser.cs index 3a90451b..b8219a5e 100644 --- a/SafeExamBrowser.Server/Parser.cs +++ b/SafeExamBrowser.Server/Parser.cs @@ -127,6 +127,27 @@ namespace SafeExamBrowser.Server return salt != default; } + internal bool TryParseBrowserExamKey(HttpResponseMessage response, out string browserExamKey) + { + browserExamKey = default; + + try + { + var hasHeader = response.Headers.TryGetValues(Header.BROWSER_EXAM_KEY, out var values); + + if (hasHeader) + { + browserExamKey = values.First(); + } + } + catch (Exception e) + { + logger.Error("Failed to parse browser exam key!", e); + } + + return browserExamKey != default; + } + internal bool TryParseConnectionToken(HttpResponseMessage response, out string connectionToken) { connectionToken = default; diff --git a/SafeExamBrowser.Server/Requests/Header.cs b/SafeExamBrowser.Server/Requests/Header.cs index f8eb8f27..381b3548 100644 --- a/SafeExamBrowser.Server/Requests/Header.cs +++ b/SafeExamBrowser.Server/Requests/Header.cs @@ -13,6 +13,7 @@ namespace SafeExamBrowser.Server.Requests internal const string ACCEPT = "Accept"; internal const string APP_SIGNATURE_KEY_SALT = "SEBExamSalt"; internal const string AUTHORIZATION = "Authorization"; + internal const string BROWSER_EXAM_KEY = "SEBServerBEK"; internal const string CONNECTION_TOKEN = "SEBConnectionToken"; } } diff --git a/SafeExamBrowser.Server/Requests/SelectExamRequest.cs b/SafeExamBrowser.Server/Requests/SelectExamRequest.cs index f0989d56..ec138d41 100644 --- a/SafeExamBrowser.Server/Requests/SelectExamRequest.cs +++ b/SafeExamBrowser.Server/Requests/SelectExamRequest.cs @@ -20,18 +20,20 @@ namespace SafeExamBrowser.Server.Requests { } - internal bool TryExecute(Exam exam, out string message, out string salt) + internal bool TryExecute(Exam exam, out string message, out string appSignatureKeySalt, out string browserExamKey) { var content = $"examId={exam.Id}"; var method = new HttpMethod("PATCH"); var success = TryExecute(method, api.HandshakeEndpoint, out var response, content, ContentType.URL_ENCODED, Authorization, Token); + appSignatureKeySalt = default; + browserExamKey = default; message = response.ToLogString(); - salt = default; if (success) { - parser.TryParseAppSignatureKeySalt(response, out salt); + parser.TryParseAppSignatureKeySalt(response, out appSignatureKeySalt); + parser.TryParseBrowserExamKey(response, out browserExamKey); } return success; diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index c224f9b2..58369c77 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -278,10 +278,10 @@ namespace SafeExamBrowser.Server return new ServerResponse(success, message); } - public ServerResponse SendSelectedExam(Exam exam) + public ServerResponse SendSelectedExam(Exam exam) { var request = new SelectExamRequest(api, httpClient, logger, parser, settings); - var success = request.TryExecute(exam, out var message, out var salt); + var success = request.TryExecute(exam, out var message, out var appSignatureKeySalt, out var browserExamKey); if (success) { @@ -292,17 +292,22 @@ namespace SafeExamBrowser.Server logger.Error("Failed to send selected exam!"); } - if (success && salt != default) + if (success && appSignatureKeySalt != default) { logger.Info("App signature key salt detected, performing key exchange..."); - success = TrySendAppSignatureKey(salt, out message); + success = TrySendAppSignatureKey(appSignatureKeySalt, out message); } else { logger.Info("No app signature key salt detected, skipping key exchange."); } - return new ServerResponse(success, message); + if (browserExamKey != default) + { + logger.Info("Custom browser exam key detected."); + } + + return new ServerResponse(success, browserExamKey, message); } public ServerResponse SendSessionIdentifier(string identifier) diff --git a/SafeExamBrowser.Settings/Browser/BrowserSettings.cs b/SafeExamBrowser.Settings/Browser/BrowserSettings.cs index ab196c19..ec401b86 100644 --- a/SafeExamBrowser.Settings/Browser/BrowserSettings.cs +++ b/SafeExamBrowser.Settings/Browser/BrowserSettings.cs @@ -86,6 +86,11 @@ namespace SafeExamBrowser.Settings.Browser /// public bool ConfirmQuitUrl { get; set; } + /// + /// An optional, custom browser exam key used for integrity checks with server applications (see also ). + /// + public string CustomBrowserExamKey { get; set; } + /// /// The custom user agent to optionally be used for all requests. /// @@ -174,7 +179,7 @@ namespace SafeExamBrowser.Settings.Browser public bool SendConfigurationKey { get; set; } /// - /// Determines whether the browser exam key header is sent with every HTTP request (see also ). + /// Determines whether the browser exam key header is sent with every HTTP request (see also and ). /// public bool SendBrowserExamKey { get; set; }