SEBWIN-634: Implemented custom browser exam key defined by server.

This commit is contained in:
Damian Büchel 2023-03-08 22:24:29 +01:00
parent cdb08798b8
commit b69280731a
11 changed files with 92 additions and 20 deletions

View file

@ -115,16 +115,18 @@ namespace SafeExamBrowser.Browser
if (success) if (success)
{ {
if (settings.UseTemporaryDownAndUploadDirectory) InitializeIntegrityKeys();
{
CreateTemporaryDownAndUploadDirectory();
}
if (settings.DeleteCookiesOnStartup) if (settings.DeleteCookiesOnStartup)
{ {
DeleteCookies(); DeleteCookies();
} }
if (settings.UseTemporaryDownAndUploadDirectory)
{
CreateTemporaryDownAndUploadDirectory();
}
logger.Info("Initialized browser."); logger.Info("Initialized browser.");
} }
else else
@ -362,6 +364,22 @@ namespace SafeExamBrowser.Browser
return cefSettings; 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) private void InitializeProxySettings(CefSettings cefSettings)
{ {
if (settings.Proxy.Policy == ProxyPolicy.Custom) if (settings.Proxy.Policy == ProxyPolicy.Custom)

View file

@ -27,5 +27,10 @@ namespace SafeExamBrowser.Configuration.Contracts.Cryptography
/// Calculates the hash value of the configuration key (CK) for the given URL. /// Calculates the hash value of the configuration key (CK) for the given URL.
/// </summary> /// </summary>
string CalculateConfigurationKeyHash(string configurationKey, string url); string CalculateConfigurationKeyHash(string configurationKey, string url);
/// <summary>
/// Specifies that a custom browser exam key (BEK) should be used.
/// </summary>
void UseCustomBrowserExamKey(string browserExamKey);
} }
} }

View file

@ -67,6 +67,15 @@ namespace SafeExamBrowser.Configuration.Cryptography
return key; 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) private string ComputeBrowserExamKey(string configurationKey, byte[] salt)
{ {
lock (@lock) lock (@lock)

View file

@ -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.Connect()).Returns(new ServerResponse(true)).Callback(() => connect = ++counter);
server.Setup(s => s.Initialize(It.IsAny<ServerSettings>())).Callback(() => initialize = ++counter); server.Setup(s => s.Initialize(It.IsAny<ServerSettings>())).Callback(() => initialize = ++counter);
server.Setup(s => s.GetConnectionInfo()).Returns(connection).Callback(() => getConnection = ++counter); server.Setup(s => s.GetConnectionInfo()).Returns(connection).Callback(() => getConnection = ++counter);
server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse(true)); server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse<string>(true, default));
server server
.Setup(s => s.GetAvailableExams(It.IsAny<string>())) .Setup(s => s.GetAvailableExams(It.IsAny<string>()))
.Returns(new ServerResponse<IEnumerable<Exam>>(true, default(IEnumerable<Exam>))) .Returns(new ServerResponse<IEnumerable<Exam>>(true, default(IEnumerable<Exam>)))
@ -276,7 +276,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
server.Setup(s => s.GetConnectionInfo()).Returns(connection); server.Setup(s => s.GetConnectionInfo()).Returns(connection);
server.Setup(s => s.GetAvailableExams(It.IsAny<string>())).Returns(new ServerResponse<IEnumerable<Exam>>(true, new[] { exam })); server.Setup(s => s.GetAvailableExams(It.IsAny<string>())).Returns(new ServerResponse<IEnumerable<Exam>>(true, new[] { exam }));
server.Setup(s => s.GetConfigurationFor(It.IsAny<Exam>())).Returns(new ServerResponse<Uri>(true, new Uri("file:///configuration.seb"))); server.Setup(s => s.GetConfigurationFor(It.IsAny<Exam>())).Returns(new ServerResponse<Uri>(true, new Uri("file:///configuration.seb")));
server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse(true)); server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse<string>(true, default));
sut.ActionRequired += (args) => Assert.Fail(); sut.ActionRequired += (args) => Assert.Fail();
var result = sut.Perform(); 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.Connect()).Returns(new ServerResponse(true)).Callback(() => connect = ++counter);
server.Setup(s => s.Initialize(It.IsAny<ServerSettings>())).Callback(() => initialize = ++counter); server.Setup(s => s.Initialize(It.IsAny<ServerSettings>())).Callback(() => initialize = ++counter);
server.Setup(s => s.GetConnectionInfo()).Returns(connection).Callback(() => getConnection = ++counter); server.Setup(s => s.GetConnectionInfo()).Returns(connection).Callback(() => getConnection = ++counter);
server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse(true)); server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse<string>(true, default));
server server
.Setup(s => s.GetAvailableExams(It.IsAny<string>())) .Setup(s => s.GetAvailableExams(It.IsAny<string>()))
.Returns(new ServerResponse<IEnumerable<Exam>>(true, default(IEnumerable<Exam>))) .Returns(new ServerResponse<IEnumerable<Exam>>(true, default(IEnumerable<Exam>)))
@ -532,7 +532,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
server.Setup(s => s.GetConnectionInfo()).Returns(connection); server.Setup(s => s.GetConnectionInfo()).Returns(connection);
server.Setup(s => s.GetAvailableExams(It.IsAny<string>())).Returns(new ServerResponse<IEnumerable<Exam>>(true, new[] { exam })); server.Setup(s => s.GetAvailableExams(It.IsAny<string>())).Returns(new ServerResponse<IEnumerable<Exam>>(true, new[] { exam }));
server.Setup(s => s.GetConfigurationFor(It.IsAny<Exam>())).Returns(new ServerResponse<Uri>(true, new Uri("file:///configuration.seb"))); server.Setup(s => s.GetConfigurationFor(It.IsAny<Exam>())).Returns(new ServerResponse<Uri>(true, new Uri("file:///configuration.seb")));
server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse(true)); server.Setup(s => s.SendSelectedExam(It.IsAny<Exam>())).Returns(new ServerResponse<string>(true, default));
sut.ActionRequired += (args) => Assert.Fail(); sut.ActionRequired += (args) => Assert.Fail();
var result = sut.Repeat(); var result = sut.Repeat();

View file

@ -51,6 +51,7 @@ namespace SafeExamBrowser.Runtime.Operations
if (Context.Next.Settings.SessionMode == SessionMode.Server) if (Context.Next.Settings.SessionMode == SessionMode.Server)
{ {
var browserExamKey = default(string);
var exam = default(Exam); var exam = default(Exam);
var exams = default(IEnumerable<Exam>); var exams = default(IEnumerable<Exam>);
var uri = default(Uri); var uri = default(Uri);
@ -84,7 +85,12 @@ namespace SafeExamBrowser.Runtime.Operations
if (success && result == OperationResult.Success) 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) if (abort)

View file

@ -105,9 +105,9 @@ namespace SafeExamBrowser.Server.Contracts
ServerResponse RaiseHand(string message = default); ServerResponse RaiseHand(string message = default);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
ServerResponse SendSelectedExam(Exam exam); ServerResponse<string> SendSelectedExam(Exam exam);
/// <summary> /// <summary>
/// Sends the given user session identifier of a LMS and thus establishes a connection with the server. /// Sends the given user session identifier of a LMS and thus establishes a connection with the server.

View file

@ -127,6 +127,27 @@ namespace SafeExamBrowser.Server
return salt != default; 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) internal bool TryParseConnectionToken(HttpResponseMessage response, out string connectionToken)
{ {
connectionToken = default; connectionToken = default;

View file

@ -13,6 +13,7 @@ namespace SafeExamBrowser.Server.Requests
internal const string ACCEPT = "Accept"; internal const string ACCEPT = "Accept";
internal const string APP_SIGNATURE_KEY_SALT = "SEBExamSalt"; internal const string APP_SIGNATURE_KEY_SALT = "SEBExamSalt";
internal const string AUTHORIZATION = "Authorization"; internal const string AUTHORIZATION = "Authorization";
internal const string BROWSER_EXAM_KEY = "SEBServerBEK";
internal const string CONNECTION_TOKEN = "SEBConnectionToken"; internal const string CONNECTION_TOKEN = "SEBConnectionToken";
} }
} }

View file

@ -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 content = $"examId={exam.Id}";
var method = new HttpMethod("PATCH"); var method = new HttpMethod("PATCH");
var success = TryExecute(method, api.HandshakeEndpoint, out var response, content, ContentType.URL_ENCODED, Authorization, Token); var success = TryExecute(method, api.HandshakeEndpoint, out var response, content, ContentType.URL_ENCODED, Authorization, Token);
appSignatureKeySalt = default;
browserExamKey = default;
message = response.ToLogString(); message = response.ToLogString();
salt = default;
if (success) if (success)
{ {
parser.TryParseAppSignatureKeySalt(response, out salt); parser.TryParseAppSignatureKeySalt(response, out appSignatureKeySalt);
parser.TryParseBrowserExamKey(response, out browserExamKey);
} }
return success; return success;

View file

@ -278,10 +278,10 @@ namespace SafeExamBrowser.Server
return new ServerResponse(success, message); return new ServerResponse(success, message);
} }
public ServerResponse SendSelectedExam(Exam exam) public ServerResponse<string> SendSelectedExam(Exam exam)
{ {
var request = new SelectExamRequest(api, httpClient, logger, parser, settings); 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) if (success)
{ {
@ -292,17 +292,22 @@ namespace SafeExamBrowser.Server
logger.Error("Failed to send selected exam!"); 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..."); logger.Info("App signature key salt detected, performing key exchange...");
success = TrySendAppSignatureKey(salt, out message); success = TrySendAppSignatureKey(appSignatureKeySalt, out message);
} }
else else
{ {
logger.Info("No app signature key salt detected, skipping key exchange."); 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<string>(success, browserExamKey, message);
} }
public ServerResponse SendSessionIdentifier(string identifier) public ServerResponse SendSessionIdentifier(string identifier)

View file

@ -86,6 +86,11 @@ namespace SafeExamBrowser.Settings.Browser
/// </summary> /// </summary>
public bool ConfirmQuitUrl { get; set; } public bool ConfirmQuitUrl { get; set; }
/// <summary>
/// An optional, custom browser exam key used for integrity checks with server applications (see also <see cref="SendBrowserExamKey"/>).
/// </summary>
public string CustomBrowserExamKey { get; set; }
/// <summary> /// <summary>
/// The custom user agent to optionally be used for all requests. /// The custom user agent to optionally be used for all requests.
/// </summary> /// </summary>
@ -174,7 +179,7 @@ namespace SafeExamBrowser.Settings.Browser
public bool SendConfigurationKey { get; set; } public bool SendConfigurationKey { get; set; }
/// <summary> /// <summary>
/// Determines whether the browser exam key header is sent with every HTTP request (see also <see cref="BrowserExamKeySalt"/>). /// Determines whether the browser exam key header is sent with every HTTP request (see also <see cref="BrowserExamKeySalt"/> and <see cref="CustomBrowserExamKey"/>).
/// </summary> /// </summary>
public bool SendBrowserExamKey { get; set; } public bool SendBrowserExamKey { get; set; }