using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; namespace SafeMobileLib { //Based on https://gist.github.com/Stasonix class GlobalKeyboardHook : IDisposable { public event EventHandler KeyboardPressed; public GlobalKeyboardHook() { _windowsHookHandle = IntPtr.Zero; _user32LibraryHandle = IntPtr.Zero; _hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour. _user32LibraryHandle = LoadLibrary("User32"); if (_user32LibraryHandle == IntPtr.Zero) { int errorCode = Marshal.GetLastWin32Error(); throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}."); } _windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0); if (_windowsHookHandle == IntPtr.Zero) { int errorCode = Marshal.GetLastWin32Error(); throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}."); } } protected virtual void Dispose(bool disposing) { if (disposing) { // because we can unhook only in the same thread, not in garbage collector thread if (_windowsHookHandle != IntPtr.Zero) { if (!UnhookWindowsHookEx(_windowsHookHandle)) { int errorCode = Marshal.GetLastWin32Error(); throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}."); } _windowsHookHandle = IntPtr.Zero; // ReSharper disable once DelegateSubtraction _hookProc -= LowLevelKeyboardProc; } } if (_user32LibraryHandle != IntPtr.Zero) { if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1. { int errorCode = Marshal.GetLastWin32Error(); throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}."); } _user32LibraryHandle = IntPtr.Zero; } } ~GlobalKeyboardHook() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private IntPtr _windowsHookHandle; private IntPtr _user32LibraryHandle; private HookProc _hookProc; delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll")] private static extern IntPtr LoadLibrary(string lpFileName); [DllImport("kernel32.dll", CharSet = CharSet.Auto)] private static extern bool FreeLibrary(IntPtr hModule); /// /// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain. /// You would install a hook procedure to monitor the system for certain types of events. These events are /// associated either with a specific thread or with all threads in the same desktop as the calling thread. /// /// hook type /// hook procedure /// handle to application instance /// thread identifier /// If the function succeeds, the return value is the handle to the hook procedure. [DllImport("USER32", SetLastError = true)] static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId); /// /// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. /// /// handle to hook procedure /// If the function succeeds, the return value is true. [DllImport("USER32", SetLastError = true)] public static extern bool UnhookWindowsHookEx(IntPtr hHook); /// /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain. /// A hook procedure can call this function either before or after processing the hook information. /// /// handle to current hook /// hook code passed to hook procedure /// value passed to hook procedure /// value passed to hook procedure /// If the function succeeds, the return value is true. [DllImport("USER32", SetLastError = true)] static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential)] public struct LowLevelKeyboardInputEvent { /// /// A virtual-key code. The code must be a value in the range 1 to 254. /// public int VirtualCode; /// /// A hardware scan code for the key. /// public int HardwareScanCode; /// /// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level. /// public int Flags; /// /// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message. /// public int TimeStamp; /// /// Additional information associated with the message. /// public IntPtr AdditionalInformation; } public const int WH_KEYBOARD_LL = 13; public enum KeyboardState { KeyDown = 0x0100, KeyUp = 0x0101, SysKeyDown = 0x0104, SysKeyUp = 0x0105 } public const int VkSnapshot = 0x2c; public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam) { bool fEatKeyStroke = false; var wparamTyped = wParam.ToInt32(); if (Enum.IsDefined(typeof(KeyboardState), wparamTyped)) { object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent)); LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o; var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped); EventHandler handler = KeyboardPressed; handler?.Invoke(this, eventArguments); fEatKeyStroke = eventArguments.Handled; } return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); } } class GlobalKeyboardHookEventArgs : HandledEventArgs { public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; } public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; } public GlobalKeyboardHookEventArgs( GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData, GlobalKeyboardHook.KeyboardState keyboardState) { KeyboardData = keyboardData; KeyboardState = keyboardState; } } }