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));