diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj
index 86d1dbd6..7f770954 100644
--- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj
+++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj
@@ -62,6 +62,7 @@
+
diff --git a/SafeExamBrowser.Configuration/Settings/KeyboardSettings.cs b/SafeExamBrowser.Configuration/Settings/KeyboardSettings.cs
new file mode 100644
index 00000000..e468d724
--- /dev/null
+++ b/SafeExamBrowser.Configuration/Settings/KeyboardSettings.cs
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using SafeExamBrowser.Contracts.Configuration.Settings;
+
+namespace SafeExamBrowser.Configuration.Settings
+{
+ public class KeyboardSettings : IKeyboardSettings
+ {
+ public bool AllowAltTab => false;
+
+ public bool AllowEsc => false;
+
+ public bool AllowF5 => true;
+ }
+}
diff --git a/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs b/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs
index e9e8977b..c0b95eb7 100644
--- a/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs
+++ b/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs
@@ -24,6 +24,7 @@ namespace SafeExamBrowser.Configuration
public SettingsImpl()
{
Browser = new BrowserSettings(this);
+ Keyboard = new KeyboardSettings();
}
public string AppDataFolderName => "SafeExamBrowser";
@@ -35,6 +36,8 @@ namespace SafeExamBrowser.Configuration
public IBrowserSettings Browser { get; private set; }
+ public IKeyboardSettings Keyboard { get; private set; }
+
public string LogFolderPath
{
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataFolderName, "Logs"); }
diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/IKeyboardSettings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/IKeyboardSettings.cs
new file mode 100644
index 00000000..7c200678
--- /dev/null
+++ b/SafeExamBrowser.Contracts/Configuration/Settings/IKeyboardSettings.cs
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+namespace SafeExamBrowser.Contracts.Configuration.Settings
+{
+ public interface IKeyboardSettings
+ {
+ ///
+ /// Determines whether the user may use the ALT+TAB shortcut.
+ ///
+ bool AllowAltTab { get; }
+
+ ///
+ /// Determines whether the user may use the escape key.
+ ///
+ bool AllowEsc { get; }
+
+ ///
+ /// Determines whether the user may use the F5 key.
+ ///
+ bool AllowF5 { get; }
+ }
+}
diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs
index 6c6dae7a..2129ac26 100644
--- a/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs
+++ b/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs
@@ -25,6 +25,11 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings
///
IBrowserSettings Browser { get; }
+ ///
+ /// All keyboard-related settings.
+ ///
+ IKeyboardSettings Keyboard { get; }
+
///
/// The path where the log files are to be stored.
///
diff --git a/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs b/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs
index 8274e586..24583024 100644
--- a/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs
+++ b/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs
@@ -11,8 +11,8 @@ namespace SafeExamBrowser.Contracts.Monitoring
public interface IKeyboardInterceptor
{
///
- /// Returns true if the given key should be blocked, otherwise false.
+ /// Returns true if the given key should be blocked, otherwise false. The key code corresponds to a Win32 Virtual-Key.
///
- bool Block(uint keyCode, KeyModifier modifier);
+ bool Block(int keyCode, KeyModifier modifier, KeyState state);
}
}
diff --git a/SafeExamBrowser.Contracts/Monitoring/KeyModifier.cs b/SafeExamBrowser.Contracts/Monitoring/KeyModifier.cs
index 2892d849..ba36f15a 100644
--- a/SafeExamBrowser.Contracts/Monitoring/KeyModifier.cs
+++ b/SafeExamBrowser.Contracts/Monitoring/KeyModifier.cs
@@ -14,7 +14,7 @@ namespace SafeExamBrowser.Contracts.Monitoring
public enum KeyModifier
{
None = 0,
- Ctrl = 0b01,
- Alt = 0b10
+ Alt = 0b1,
+ Ctrl = 0b10
}
}
diff --git a/SafeExamBrowser.Contracts/Monitoring/KeyState.cs b/SafeExamBrowser.Contracts/Monitoring/KeyState.cs
new file mode 100644
index 00000000..c165e872
--- /dev/null
+++ b/SafeExamBrowser.Contracts/Monitoring/KeyState.cs
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+namespace SafeExamBrowser.Contracts.Monitoring
+{
+ public enum KeyState
+ {
+ None = 0,
+ Pressed,
+ Released
+ }
+}
diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj
index b0065bd9..2b11f3c8 100644
--- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj
+++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj
@@ -67,6 +67,7 @@
+
@@ -85,6 +86,7 @@
+
diff --git a/SafeExamBrowser.Core/Behaviour/Operations/DeviceInterceptionOperation.cs b/SafeExamBrowser.Core/Behaviour/Operations/DeviceInterceptionOperation.cs
new file mode 100644
index 00000000..7315d799
--- /dev/null
+++ b/SafeExamBrowser.Core/Behaviour/Operations/DeviceInterceptionOperation.cs
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using SafeExamBrowser.Contracts.Behaviour;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.Monitoring;
+using SafeExamBrowser.Contracts.UserInterface;
+using SafeExamBrowser.Contracts.WindowsApi;
+
+namespace SafeExamBrowser.Core.Behaviour.Operations
+{
+ public class DeviceInterceptionOperation : IOperation
+ {
+ private IKeyboardInterceptor keyboardInterceptor;
+ private ILogger logger;
+ private INativeMethods nativeMethods;
+
+ public ISplashScreen SplashScreen { private get; set; }
+
+ public DeviceInterceptionOperation(IKeyboardInterceptor keyboardInterceptor, ILogger logger, INativeMethods nativeMethods)
+ {
+ this.keyboardInterceptor = keyboardInterceptor;
+ this.logger = logger;
+ this.nativeMethods = nativeMethods;
+ }
+
+ public void Perform()
+ {
+ logger.Info("Starting keyboard and mouse interception...");
+
+ nativeMethods.RegisterKeyboardHook(keyboardInterceptor);
+ }
+
+ public void Revert()
+ {
+ logger.Info("Stopping keyboard and mouse interception...");
+
+ nativeMethods.UnregisterKeyboardHook(keyboardInterceptor);
+ }
+ }
+}
diff --git a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj
index 938b2f0b..874d383b 100644
--- a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj
+++ b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj
@@ -58,6 +58,7 @@
+
diff --git a/SafeExamBrowser.Monitoring/Keyboard/KeyboardInterceptor.cs b/SafeExamBrowser.Monitoring/Keyboard/KeyboardInterceptor.cs
new file mode 100644
index 00000000..5bfb9722
--- /dev/null
+++ b/SafeExamBrowser.Monitoring/Keyboard/KeyboardInterceptor.cs
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using System;
+using System.Linq;
+using System.Windows.Input;
+using SafeExamBrowser.Contracts.Configuration.Settings;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.Monitoring;
+
+namespace SafeExamBrowser.Monitoring.Keyboard
+{
+ public class KeyboardInterceptor : IKeyboardInterceptor
+ {
+ private IKeyboardSettings settings;
+ private ILogger logger;
+
+ public KeyboardInterceptor(IKeyboardSettings settings, ILogger logger)
+ {
+ this.logger = logger;
+ this.settings = settings;
+ }
+
+ public bool Block(int keyCode, KeyModifier modifier, KeyState state)
+ {
+ var block = false;
+ var key = KeyInterop.KeyFromVirtualKey(keyCode);
+
+ block |= key == Key.Apps;
+ block |= key == Key.F1;
+ block |= key == Key.F2;
+ block |= key == Key.F3;
+ block |= key == Key.F4;
+ block |= key == Key.F6;
+ block |= key == Key.F7;
+ block |= key == Key.F8;
+ block |= key == Key.F9;
+ block |= key == Key.F10;
+ block |= key == Key.F11;
+ block |= key == Key.F12;
+ block |= key == Key.PrintScreen;
+
+ block |= key == Key.Escape && modifier.HasFlag(KeyModifier.Alt);
+ block |= key == Key.Escape && modifier.HasFlag(KeyModifier.Ctrl);
+ block |= key == Key.Space && modifier.HasFlag(KeyModifier.Alt);
+
+ block |= !settings.AllowAltTab && key == Key.Tab && modifier.HasFlag(KeyModifier.Alt);
+ block |= !settings.AllowEsc && key == Key.Escape && modifier == KeyModifier.None;
+ block |= !settings.AllowF5 && key == Key.F5;
+
+ if (block)
+ {
+ Log(key, keyCode, modifier, state);
+ }
+
+ return block;
+ }
+
+ private void Log(Key key, int keyCode, KeyModifier modifier, KeyState state)
+ {
+ var modifierFlags = Enum.GetValues(typeof(KeyModifier)).OfType().Where(m => m != KeyModifier.None && modifier.HasFlag(m));
+ var modifiers = modifierFlags.Any() ? String.Join(" + ", modifierFlags) + " + " : string.Empty;
+
+ logger.Info($"Blocked '{modifiers}{key}' ({key} = {keyCode}) when {state.ToString().ToLower()}.");
+ }
+ }
+}
diff --git a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj
index 4b2e9b30..c2e4117b 100644
--- a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj
+++ b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj
@@ -57,8 +57,10 @@
+
+
@@ -70,7 +72,6 @@
-
diff --git a/SafeExamBrowser.WindowsApi/Constants/Constant.cs b/SafeExamBrowser.WindowsApi/Constants/Constant.cs
index d30f7d46..cedc121f 100644
--- a/SafeExamBrowser.WindowsApi/Constants/Constant.cs
+++ b/SafeExamBrowser.WindowsApi/Constants/Constant.cs
@@ -59,7 +59,7 @@ namespace SafeExamBrowser.WindowsApi.Constants
///
/// Posted to the window with the keyboard focus when a nonsystem key is released. A nonsystem key is a key that is pressed when
- /// the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus.
+ /// the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus.
///
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms646281(v=vs.85).aspx.
///
diff --git a/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs b/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs
index ea18acdf..c9c5a837 100644
--- a/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs
+++ b/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs
@@ -16,6 +16,15 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
{
internal class KeyboardHook
{
+ private const int LEFT_CTRL = 162;
+ private const int RIGHT_CTRL = 163;
+ private const int LEFT_ALT = 164;
+ private const int RIGHT_ALT = 165;
+ private const int DELETE = 46;
+
+ private bool altPressed, ctrlPressed;
+ private HookProc hookProc;
+
internal IntPtr Handle { get; private set; }
internal IKeyboardInterceptor Interceptor { get; private set; }
@@ -28,7 +37,12 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
{
var module = Kernel32.GetModuleHandle(null);
- Handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, LowLevelKeyboardProc, module, 0);
+ // IMORTANT:
+ // Ensures that the hook delegate does not get garbage collected prematurely, as it will be passed to unmanaged code.
+ // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash!
+ hookProc = new HookProc(LowLevelKeyboardProc);
+
+ Handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookProc, module, 0);
}
internal bool Detach()
@@ -40,10 +54,11 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
{
if (nCode >= 0)
{
+ var state = GetState(wParam.ToInt32());
var keyData = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
- var modifier = GetModifiers(keyData);
+ var modifier = GetModifiers(keyData, wParam.ToInt32());
- if (Interceptor.Block(keyData.KeyCode, modifier))
+ if (Interceptor.Block((int) keyData.KeyCode, modifier, state))
{
return (IntPtr) 1;
}
@@ -52,21 +67,64 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
return User32.CallNextHookEx(Handle, nCode, wParam, lParam);
}
- private KeyModifier GetModifiers(KBDLLHOOKSTRUCT keyData)
+ private KeyState GetState(int wParam)
+ {
+ switch (wParam)
+ {
+ case Constant.WM_KEYDOWN:
+ case Constant.WM_SYSKEYDOWN:
+ return KeyState.Pressed;
+ case Constant.WM_KEYUP:
+ case Constant.WM_SYSKEYUP:
+ return KeyState.Released;
+ default:
+ return KeyState.None;
+ }
+ }
+
+ private KeyModifier GetModifiers(KBDLLHOOKSTRUCT keyData, int wParam)
{
var modifier = KeyModifier.None;
- if ((keyData.Flags & KBDLLHOOKSTRUCTFlags.LLKHF_ALTDOWN) == KBDLLHOOKSTRUCTFlags.LLKHF_ALTDOWN)
+ TrackCtrlAndAlt(keyData, wParam);
+
+ if (altPressed || keyData.Flags.HasFlag(KBDLLHOOKSTRUCTFlags.LLKHF_ALTDOWN))
{
modifier |= KeyModifier.Alt;
}
- if (keyData.Flags == 0)
+ if (ctrlPressed)
{
modifier |= KeyModifier.Ctrl;
}
return modifier;
}
+
+ private void TrackCtrlAndAlt(KBDLLHOOKSTRUCT keyData, int wParam)
+ {
+ var keyCode = keyData.KeyCode;
+
+ if (keyCode == LEFT_CTRL || keyCode == RIGHT_CTRL)
+ {
+ ctrlPressed = IsPressed(wParam);
+ }
+ else if (keyCode == LEFT_ALT || keyCode == RIGHT_ALT)
+ {
+ altPressed = IsPressed(wParam);
+ }
+
+ if (ctrlPressed && altPressed && keyCode == DELETE)
+ {
+ // When the Secure Attention Sequence is pressed, the WM_KEYUP / WM_SYSKEYUP messages for CTRL and ALT get lost...
+ ctrlPressed = false;
+ altPressed = false;
+ }
+ }
+
+ private bool IsPressed(int wParam)
+ {
+ return wParam == Constant.WM_KEYDOWN || wParam == Constant.WM_SYSKEYDOWN;
+ }
}
}
diff --git a/SafeExamBrowser.WindowsApi/NativeMethods.cs b/SafeExamBrowser.WindowsApi/NativeMethods.cs
index d6792052..0914e39b 100644
--- a/SafeExamBrowser.WindowsApi/NativeMethods.cs
+++ b/SafeExamBrowser.WindowsApi/NativeMethods.cs
@@ -27,7 +27,7 @@ namespace SafeExamBrowser.WindowsApi
private ConcurrentDictionary KeyboardHooks = new ConcurrentDictionary();
///
- /// Upon finalization, unregister all system events and hooks...
+ /// Upon finalization, unregister all active system events and hooks...
///
~NativeMethods()
{
@@ -144,9 +144,6 @@ namespace SafeExamBrowser.WindowsApi
hook.Attach();
- // IMORTANT:
- // Ensures that the hook does not get garbage collected prematurely, as it will be passed to unmanaged code.
- // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash!
KeyboardHooks[hook.Handle] = hook;
}
diff --git a/SafeExamBrowser/CompositionRoot.cs b/SafeExamBrowser/CompositionRoot.cs
index ea4026a7..78c5d2a6 100644
--- a/SafeExamBrowser/CompositionRoot.cs
+++ b/SafeExamBrowser/CompositionRoot.cs
@@ -21,6 +21,7 @@ using SafeExamBrowser.Core.Behaviour;
using SafeExamBrowser.Core.Behaviour.Operations;
using SafeExamBrowser.Core.I18n;
using SafeExamBrowser.Core.Logging;
+using SafeExamBrowser.Monitoring.Keyboard;
using SafeExamBrowser.Monitoring.Processes;
using SafeExamBrowser.Monitoring.Windows;
using SafeExamBrowser.UserInterface;
@@ -32,10 +33,11 @@ namespace SafeExamBrowser
{
private IApplicationController browserController;
private IApplicationInfo browserInfo;
- private IRuntimeController runtimeController;
+ private IKeyboardInterceptor keyboardInterceptor;
private ILogger logger;
private INativeMethods nativeMethods;
private IProcessMonitor processMonitor;
+ private IRuntimeController runtimeController;
private ISettings settings;
private IText text;
private ITextResource textResource;
@@ -62,6 +64,7 @@ namespace SafeExamBrowser
text = new Text(textResource);
browserController = new BrowserApplicationController(settings, text, uiFactory);
+ keyboardInterceptor = new KeyboardInterceptor(settings.Keyboard, new ModuleLogger(logger, typeof(KeyboardInterceptor)));
processMonitor = new ProcessMonitor(new ModuleLogger(logger, typeof(ProcessMonitor)), nativeMethods);
windowMonitor = new WindowMonitor(new ModuleLogger(logger, typeof(WindowMonitor)), nativeMethods);
workingArea = new WorkingArea(new ModuleLogger(logger, typeof(WorkingArea)), nativeMethods);
@@ -71,6 +74,7 @@ namespace SafeExamBrowser
StartupController = new StartupController(logger, settings, text, uiFactory);
StartupOperations = new Queue();
+ StartupOperations.Enqueue(new DeviceInterceptionOperation(keyboardInterceptor, logger, nativeMethods));
StartupOperations.Enqueue(new WindowMonitorOperation(logger, windowMonitor));
StartupOperations.Enqueue(new ProcessMonitorOperation(logger, processMonitor));
StartupOperations.Enqueue(new WorkingAreaOperation(logger, Taskbar, workingArea));